chore: organise project structure and slowly switch out solid for react

This commit is contained in:
LynithDev 2025-05-02 11:42:22 +02:00
parent 37e8e1eaf8
commit 303ac31756
51 changed files with 3980 additions and 4142 deletions

View file

@ -3,7 +3,8 @@
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"files.readonlyInclude": {
"**/node_modules/**": true
"**/node_modules/**": true,
"**/routeTree.gen.ts": true
},
"files.watcherExclude": {
"**/_build": true,
@ -18,7 +19,8 @@
"**/node_modules": true,
"**/.rustup": true,
"**/.cargo": true,
"**/.crates": true
"**/.crates": true,
"**/routeTree.gen.ts": true
},
"rust-analyzer.procMacro.enable": false,
"rust-analyzer.procMacro.attributes.enable": false,

5
apps/oneclient/frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

View file

@ -0,0 +1,3 @@
package-lock.json
pnpm-lock.yaml
yarn.lock

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-tsrouter-app"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>OneClient</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,36 @@
{
"name": "@oneclient/frontend",
"type": "module",
"private": true,
"scripts": {
"dev": "vite --port 3000",
"start": "vite --port 3000",
"build": "vite build && tsc",
"serve": "vite preview",
"lint": "eslint",
"format": "prettier",
"check": "prettier --write . && eslint --fix"
},
"dependencies": {
"@tailwindcss/vite": "^4.0.6",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-query": "^5.66.5",
"@tanstack/react-query-devtools": "^5.66.5",
"@tanstack/react-router": "^1.114.3",
"@tanstack/react-router-devtools": "^1.114.3",
"@tanstack/react-table": "^8.21.2",
"@tanstack/router-plugin": "^1.114.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.6"
},
"devDependencies": {
"@tanstack/eslint-config": "^0.1.0",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"prettier": "^3.5.3",
"typescript": "^5.7.2",
"vite": "^6.1.0"
}
}

View file

@ -0,0 +1,10 @@
// @ts-check
/** @type {import('prettier').Config} */
const config = {
semi: true,
singleQuote: true,
trailingComma: 'all',
};
export default config;

View file

@ -0,0 +1,21 @@
import { Link } from '@tanstack/react-router';
export default function Header() {
return (
<header className="flex justify-between gap-2 bg-white p-2 text-black">
<nav className="flex flex-row">
<div className="px-2 font-bold">
<Link to="/">Home</Link>
</div>
<div className="px-2 font-bold">
<Link to="/demo/table">TanStack Table</Link>
</div>
<div className="px-2 font-bold">
<Link to="/demo/tanstack-query">TanStack Query</Link>
</div>
</nav>
</header>
);
}

View file

@ -0,0 +1,5 @@
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
export default function LayoutAddition() {
return <ReactQueryDevtools buttonPosition="bottom-right" />
}

View file

@ -0,0 +1,15 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
export function getContext() {
return {
queryClient,
}
}
export function Provider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1"
xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 841.9 595.3">
<!-- Generator: Adobe Illustrator 29.3.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 146) -->
<defs>
<style>
.st0 {
fill: #9ae7fc;
}
.st1 {
fill: #61dafb;
}
</style>
</defs>
<g>
<path class="st1" d="M666.3,296.5c0-32.5-40.7-63.3-103.1-82.4,14.4-63.6,8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6,0,8.3.9,11.4,2.6,13.6,7.8,19.5,37.5,14.9,75.7-1.1,9.4-2.9,19.3-5.1,29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50,32.6-30.3,63.2-46.9,84-46.9v-22.3c-27.5,0-63.5,19.6-99.9,53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7,0,51.4,16.5,84,46.6-14,14.7-28,31.4-41.3,49.9-22.6,2.4-44,6.1-63.6,11-2.3-10-4-19.7-5.2-29-4.7-38.2,1.1-67.9,14.6-75.8,3-1.8,6.9-2.6,11.5-2.6v-22.3c-8.4,0-16,1.8-22.6,5.6-28.1,16.2-34.4,66.7-19.9,130.1-62.2,19.2-102.7,49.9-102.7,82.3s40.7,63.3,103.1,82.4c-14.4,63.6-8,114.2,20.2,130.4,6.5,3.8,14.1,5.6,22.5,5.6,27.5,0,63.5-19.6,99.9-53.6,36.4,33.8,72.4,53.2,99.9,53.2,8.4,0,16-1.8,22.6-5.6,28.1-16.2,34.4-66.7,19.9-130.1,62-19.1,102.5-49.9,102.5-82.3zm-130.2-66.7c-3.7,12.9-8.3,26.2-13.5,39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4,14.2,2.1,27.9,4.7,41,7.9zm-45.8,106.5c-7.8,13.5-15.8,26.3-24.1,38.2-14.9,1.3-30,2-45.2,2s-30.2-.7-45-1.9c-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8,6.2-13.4,13.2-26.8,20.7-39.9,7.8-13.5,15.8-26.3,24.1-38.2,14.9-1.3,30-2,45.2-2s30.2.7,45,1.9c8.3,11.9,16.4,24.6,24.2,38,7.6,13.1,14.5,26.4,20.8,39.8-6.3,13.4-13.2,26.8-20.7,39.9zm32.3-13c5.4,13.4,10,26.8,13.8,39.8-13.1,3.2-26.9,5.9-41.2,8,4.9-7.7,9.8-15.6,14.4-23.7,4.6-8,8.9-16.1,13-24.1zm-101.4,106.7c-9.3-9.6-18.6-20.3-27.8-32,9,.4,18.2.7,27.5.7s18.7-.2,27.8-.7c-9,11.7-18.3,22.4-27.5,32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9,3.7-12.9,8.3-26.2,13.5-39.5,4.1,8,8.4,16,13.1,24s9.5,15.8,14.4,23.4zm73.9-208.1c9.3,9.6,18.6,20.3,27.8,32-9-.4-18.2-.7-27.5-.7s-18.7.2-27.8.7c9-11.7,18.3-22.4,27.5-32zm-74,58.9c-4.9,7.7-9.8,15.6-14.4,23.7-4.6,8-8.9,16-13,24-5.4-13.4-10-26.8-13.8-39.8,13.1-3.1,26.9-5.8,41.2-7.9zm-90.5,125.2c-35.4-15.1-58.3-34.9-58.3-50.6s22.9-35.6,58.3-50.6c8.6-3.7,18-7,27.7-10.1,5.7,19.6,13.2,40,22.5,60.9-9.2,20.8-16.6,41.1-22.2,60.6-9.9-3.1-19.3-6.5-28-10.2zm53.8,142.9c-13.6-7.8-19.5-37.5-14.9-75.7,1.1-9.4,2.9-19.3,5.1-29.4,19.6,4.8,41,8.5,63.5,10.9,13.5,18.5,27.5,35.3,41.6,50-32.6,30.3-63.2,46.9-84,46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7,38.2-1.1,67.9-14.6,75.8-3,1.8-6.9,2.6-11.5,2.6-20.7,0-51.4-16.5-84-46.6,14-14.7,28-31.4,41.3-49.9,22.6-2.4,44-6.1,63.6-11,2.3,10.1,4.1,19.8,5.2,29.1zm38.5-66.7c-8.6,3.7-18,7-27.7,10.1-5.7-19.6-13.2-40-22.5-60.9,9.2-20.8,16.6-41.1,22.2-60.6,9.9,3.1,19.3,6.5,28.1,10.2,35.4,15.1,58.3,34.9,58.3,50.6,0,15.7-23,35.6-58.4,50.6zm-264.9-268.7z"/>
<circle class="st1" cx="420.9" cy="296.5" r="45.7"/>
<path class="st1" d="M520.5,78.1"/>
</g>
<circle class="st0" cx="420.8" cy="296.6" r="43"/>
<path class="st1" d="M466.1,296.6c0,25-20.2,45.2-45.2,45.2s-45.2-20.2-45.2-45.2,20.2-45.2,45.2-45.2,45.2,20.2,45.2,45.2ZM386,295.6v-6.3c0-1.1,1.2-5.1,1.8-6.2,1-1.9,2.9-3.5,4.6-4.7l-3.4-3.4c4-3.6,9.4-3.7,13.7-.7,1.9-4.7,6.6-7.1,11.6-6.7l-.8,4.2c5.9.2,13.1,4.1,13.1,10.8s0,.5-.7.7c-1.7.3-3.4-.4-5-.6s-1.2-.4-1.2.3,2.5,4.1,3,5.5,1,3.5.8,5.3c-5.6-.8-10.5-3.2-14.8-6.7.3,2.6,4.1,21.7,5.3,21.9s.8-.6,1-1.1,1.3-6.3,1.3-6.7c0-1-1.7-1.8-2.2-2.8-1.2-2.7,1.3-4.7,3.7-3.3s5.2,6.2,7.5,7.3,13,1.4,14.8,3.3-2.9,4.6-1.5,7.6c6.7-2.6,13.5-3.3,20.6-2.5,3.1-9.7,3.1-20.3-.9-29.8-7.3,0-14.7-3.6-17.2-10.8-2.5-7.2-.7-8.6-1.3-9.3-.8-1-6.3.6-7.4-1.5s.3-1.1-.2-1.4-1.9-.6-2.6-.8c-26-6.4-51.3,15.7-49.7,42.1,0,1.6,1.6,10.3,2.4,11.1s4.8,0,6.3,0,3.7.3,5,.5c2.9.4,7.2,2.4,9.4,2.5s2.4-.8,2.7-2.4c.4-2.6.5-7.4.5-10.1s-1-7.8-1.3-11.6c-.9-.2-.7,0-.9.5-.7,1.3-1.1,3.2-1.9,4.8s-5.2,8.7-5.7,9-.7-.5-.8-.8c-1.6-3.5-2-7.9-1.9-11.8-.9-1-5.4,4.9-6.7,5.3l-.8-.4v-.3h-.2ZM455.6,276.4c1.1-1.2-6-8.9-7.2-10-3-2.7-5.4-4.5-3.5,1.4s5.7,7.8,10.6,8.5h.1ZM410.9,270.1c-.4-.5-6.1,2.9-5.5,4.6,1.9-1.3,5.9-1.7,5.5-4.6ZM400.4,276.4c-.3-2.4-6.3-2.7-7.2-1s1.6,1.4,1.9,1.4c1.8.3,3.5-.6,5.2-.4h.1ZM411.3,276.8c3.8,1.3,6.6,3.6,10.9,3.7s0-3-1.2-3.9c-2.2-1.7-5.1-2.4-7.8-2.4s-1.6-.3-1.4.4c2.8.6,7.3.7,8.4,3.8-2.3-.3-3.9-1.6-6.2-2s-2.5-.5-2.6.3h0ZM420.6,290.3c-.8-5.1-5.7-10.8-10.9-11.6s-1.3-.4-.8.5,4.7,3.2,5.7,4,4.5,4.2,2.1,3.8-8.4-7.8-9.4-6.7c.2.9,1.1,1.9,1.7,2.7,3,3.8,6.9,6.8,11.8,7.4h-.2ZM395.3,279.8c-5,1.1-6.9,6.3-6.7,11,.7.8,5-3.8,5.4-4.5s2.7-4.6,1.1-4-2.9,4.4-4.2,4.6.2-2.1.4-2.5c1.1-1.6,2.9-3.1,4-4.6h0ZM400.4,281.5c-.4-.5-2,1.3-2.3,1.7-2.9,3.9-2.6,10.2-1.5,14.8.8.2.8-.3,1.2-.7,3-3.8,5.5-10.5,4.5-15.4-2.1,3.1-3.1,7.3-3.6,11h-1.3c0-4,1.9-7.7,3-11.4h0ZM426.9,305.9c0-1.7-1.7-1.4-2.5-1.9s-1.3-1.9-3-1.4c1.3,2.1,3,3.2,5.5,3.4h0ZM417.2,308.5c7.6.7,5.5-1.9,1.4-5.5-1.3-.3-1.5,4.5-1.4,5.5ZM437,309.7c-3.5-.3-7.8-2-11.2-2.1s-1.3,0-1.9.7c4,1.3,8.4,1.7,12.1,4l1-2.5h0ZM420.5,312.8c-7.3,0-15.1,3.7-20.4,8.8s-4.8,5.3-4.8,6.2c0,1.8,8.6,6.2,10.5,6.8,12.1,4.8,27.5,3.5,38.2-4.2s3.1-2.7,0-6.2c-5.7-6.6-14.7-11.4-23.4-11.3h-.1ZM398.7,316.9c-1.4-1.4-5-1.9-7-2.1s-5.3-.3-6.9.6l13.9,1.4h0ZM456.9,314.8h-7.4c-.9,0-4.9,1.1-6,1.6s-.8.6,0,.5c2.4,0,5.1-1,7.6-1.3s3.5.2,5.1,0,1.3-.3.6-.8h0Z"/>
<path class="st0" d="M386,295.6l.8.4c1.3-.3,5.8-6.2,6.7-5.3,0,3.9.3,8.3,1.9,11.8s0,1.2.8.8,5.1-7.8,5.7-9,1.3-3.5,1.9-4.8,0-.7.9-.5c.3,3.8,1.2,7.8,1.3,11.6s0,7.5-.5,10.1-1.1,2.4-2.7,2.4-6.5-2.1-9.4-2.5-3.7-.5-5-.5-5.4,1.1-6.3,0-2.2-9.5-2.4-11.1c-1.5-26.4,23.7-48.5,49.7-42.1s2.2.4,2.6.8,0,1,.2,1.4c1.1,2,6.5.5,7.4,1.5s.4,6.9,1.3,9.3c2.5,7.2,10,10.9,17.2,10.8,4,9.4,4,20.1.9,29.8-7.2-.7-13.9,0-20.6,2.5-1.3-3.1,4.1-5.1,1.5-7.6s-11.8-1.9-14.8-3.3-5.4-6.1-7.5-7.3-4.9.6-3.7,3.3,2.1,1.8,2.2,2.8-1,6.2-1.3,6.7-.3,1.3-1,1.1c-1.1-.3-5-19.3-5.3-21.9,4.3,3.5,9.2,5.9,14.8,6.7.2-1.9-.3-3.5-.8-5.3s-3-5.1-3-5.5c0-.8.9-.3,1.2-.3,1.6,0,3.3.8,5,.6s.7.3.7-.7c0-6.6-7.2-10.6-13.1-10.8l.8-4.2c-5.1-.3-9.6,2-11.6,6.7-4.3-3-9.8-3-13.7.7l3.4,3.4c-1.8,1.3-3.5,2.8-4.6,4.7s-1.8,5.1-1.8,6.2v6.6h.2ZM431.6,265c7.8,2.1,8.7-3.5.2-1.3l-.2,1.3ZM432.4,270.9c.3.6,6.4-.4,5.8-2.3s-4.6.6-5.7.6l-.2,1.7h.1ZM434.5,276c.8,1.2,5.7-1.8,5.5-2.7-.4-1.9-6.6,1.2-5.5,2.7ZM442.9,276.4c-.9-.9-5,2.8-4.6,4,.6,2.4,5.7-3,4.6-4ZM445.1,279.9c-.3.2-3.1,4.6-1.5,5s3.5-3.4,3.5-4-1.3-1.3-2-.9h0ZM448.9,287.4c2.1.8,3.8-5.1,2.3-5.5-1.9-.6-2.6,5.1-2.3,5.5ZM457.3,288.6c.5-1.7,1.1-4.7-1-5.5-1,.3-.6,3.9-.6,4.8l.3.5,1.3.2h0Z"/>
<path class="st0" d="M455.6,276.4c-5-.8-9.1-3.6-10.6-8.5s.5-4,3.5-1.4,8.3,8.7,7.2,10h-.1Z"/>
<path class="st0" d="M420.6,290.3c-4.9-.6-8.9-3.6-11.8-7.4s-1.5-1.8-1.7-2.7c1-1,8.5,6.6,9.4,6.7,2.4.4-1.8-3.5-2.1-3.8-1-.8-5.4-3.5-5.7-4-.4-.8.5-.5.8-.5,5.2.8,10.1,6.6,10.9,11.6h.2Z"/>
<path class="st0" d="M400.4,281.5c-1.1,3.7-3,7.3-3,11.4h1.3c.5-3.7,1.5-7.8,3.6-11,1,4.8-1.5,11.6-4.5,15.4s-.4.8-1.2.7c-1.1-4.5-1.3-10.8,1.5-14.8s1.9-2.2,2.3-1.7h0Z"/>
<path class="st0" d="M411.3,276.8c0-.8,2.1-.4,2.6-.3,2.4.4,4,1.7,6.2,2-1.2-3.1-5.7-3.2-8.4-3.8,0-.8.9-.4,1.4-.4,2.8,0,5.6.7,7.8,2.4,2.2,1.7,4,4,1.2,3.9-4.3,0-7.1-2.4-10.9-3.7h0Z"/>
<path class="st0" d="M395.3,279.8c-1.1,1.6-3,3-4,4.6s-1.9,2.8-.4,2.5,2.8-4,4.2-4.6-.9,3.6-1.1,4c-.4.7-4.7,5.2-5.4,4.5-.2-4.6,1.8-9.9,6.7-11h0Z"/>
<path class="st0" d="M437,309.7l-1,2.5c-3.6-2.3-8-2.8-12.1-4,.5-.7,1.1-.7,1.9-.7,3.4,0,7.8,1.8,11.2,2.1h0Z"/>
<path class="st0" d="M417.2,308.5c0-1,0-5.8,1.4-5.5,4,3.5,6.1,6.2-1.4,5.5Z"/>
<path class="st0" d="M400.4,276.4c-1.8-.3-3.5.7-5.2.4s-2.3-.8-1.9-1.4c.8-1.6,6.9-1.4,7.2,1h-.1Z"/>
<path class="st0" d="M410.9,270.1c.4,3-3.6,3.3-5.5,4.6-.6-1.8,5-5.1,5.5-4.6Z"/>
<path class="st0" d="M426.9,305.9c-2.5-.2-4.1-1.3-5.5-3.4,1.7-.4,2,.8,3,1.4s2.6.3,2.5,1.9h0Z"/>
<path class="st1" d="M432.4,270.9l.2-1.7c1.1,0,5.1-2.2,5.7-.6s-5.5,2.9-5.8,2.3h-.1Z"/>
<path class="st1" d="M431.6,265l.2-1.3c8.4-2.1,7.7,3.4-.2,1.3Z"/>
<path class="st1" d="M434.5,276c-1.1-1.5,5.1-4.6,5.5-2.7s-4.6,4-5.5,2.7Z"/>
<path class="st1" d="M442.9,276.4c1.1,1.1-4,6.4-4.6,4s3.7-4.9,4.6-4Z"/>
<path class="st1" d="M445.1,279.9c.7-.4,2.1,0,2,.9s-2.4,4.4-3.5,4,1.3-4.8,1.5-5h0Z"/>
<path class="st1" d="M448.9,287.4c-.3-.3.4-6.1,2.3-5.5,1.4.4-.2,6.2-2.3,5.5Z"/>
<path class="st1" d="M457.3,288.6l-1.3-.2-.3-.5c0-.9-.4-4.6.6-4.8,2.1.8,1.5,3.8,1,5.5h0Z"/>
<path class="st0" d="M420.5,312.8c8.9,0,17.9,4.7,23.4,11.3,5.6,6.6,3.8,3.5,0,6.2-10.7,7.7-26.1,9-38.2,4.2-1.9-.8-10.5-5.1-10.5-6.8s4-5.3,4.8-6.2c5.3-5,13.1-8.6,20.4-8.8h.1Z"/>
<path class="st0" d="M398.7,316.9l-13.9-1.4c1.7-1,5-.8,6.9-.6s5.6.7,7,2.1h0Z"/>
<path class="st0" d="M456.9,314.8c.7.5,0,.8-.6.8-1.6.2-3.5-.2-5.1,0-2.4.3-5.2,1.2-7.6,1.3s-1.1,0,0-.5,5.1-1.6,6-1.6h7.4,0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -0,0 +1,47 @@
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import * as TanstackQuery from './integrations/tanstack-query/root-provider'
// Import the generated route tree
import { routeTree } from './routeTree.gen'
import './styles.css'
// Create a new router instance
const router = createRouter({
routeTree,
context: {
...TanstackQuery.getContext(),
},
defaultPreload: 'intent',
scrollRestoration: true,
defaultStructuralSharing: true,
defaultPreloadStaleTime: 0,
})
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
// Render the app
const rootElement = document.getElementById('app')
if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(
<StrictMode>
<TanstackQuery.Provider>
<RouterProvider router={router} />
</TanstackQuery.Provider>
</StrictMode>,
)
}
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

View file

@ -0,0 +1,25 @@
import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import Header from '../components/Header'
import TanstackQueryLayout from '../integrations/tanstack-query/layout'
import type { QueryClient } from '@tanstack/react-query'
interface MyRouterContext {
queryClient: QueryClient
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => (
<>
<Header />
<Outlet />
<TanStackRouterDevtools />
<TanstackQueryLayout />
</>
),
})

View file

@ -0,0 +1,39 @@
import { createFileRoute } from '@tanstack/react-router'
import logo from '../logo.svg'
export const Route = createFileRoute('/')({
component: App,
})
function App() {
return (
<div className="text-center">
<header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)]">
<img
src={logo}
className="h-[40vmin] pointer-events-none animate-[spin_20s_linear_infinite]"
alt="logo"
/>
<p>
Edit <code>src/routes/index.tsx</code> and save to reload.
</p>
<a
className="text-[#61dafb] hover:underline"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
<a
className="text-[#61dafb] hover:underline"
href="https://tanstack.com"
target="_blank"
rel="noopener noreferrer"
>
Learn TanStack
</a>
</header>
</div>
)
}

View file

@ -0,0 +1,15 @@
@import "tailwindcss";
body {
@apply m-0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

View file

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"baseUrl": ".",
"module": "ESNext",
/* Bundler mode */
"moduleResolution": "bundler",
"paths": {
"@/*": ["./src/*"]
},
"types": ["vite/client"],
"allowImportingTsExtensions": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noEmit": true,
"verbatimModuleSyntax": true,
/* Linting */
"skipLibCheck": true,
"noUncheckedSideEffectImports": true
},
"include": ["**/*.ts", "**/*.tsx", "prettier.config.js", "vite.config.js"]
}

View file

@ -0,0 +1,20 @@
import { defineConfig } from "vite";
import viteReact from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import { resolve } from "node:path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [TanStackRouterVite({ autoCodeSplitting: true }), viteReact(), tailwindcss()],
test: {
globals: true,
environment: "jsdom",
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
}
});

View file

@ -1,5 +0,0 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore
# This is done so that Tauri never complains that '../dist does not exist'

View file

@ -44,10 +44,7 @@
},
"devDependencies": {
"@sentry/vite-plugin": "^2.22.4",
"@solidjs/testing-library": "^0.8.10",
"@tauri-apps/cli": "^2.0.4",
"@testing-library/jest-dom": "^6.5.0",
"jsdom": "^25.0.1",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.79.4",
"unocss": "^0.63.6",

View file

@ -1,14 +1,15 @@
import { getProgramInfo } from '@onelauncher/client';
import { type ParentProps, Show, createMemo } from 'solid-js';
import { Transition } from 'solid-transition-group';
import type {TransitionProps} from 'solid-transition-group';
import useSettings from '~ui/hooks/useSettings';
import { createMemo, type ParentProps, Show } from 'solid-js';
import { Transition, type TransitionProps } from 'solid-transition-group';
type AnimationTypes = 'default' | 'fade';
export interface AnimatedProps {
animation?: AnimationTypes;
};
const animations: { [key in AnimationTypes]: { before: Keyframe; after: Keyframe } } = {
const animations: Record<AnimationTypes, { before: Keyframe; after: Keyframe }> = {
default: {
before: {
transform: 'translateX(-85px)',

View file

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
@ -12,11 +12,8 @@
"paths": {
"~*": ["./src/*"]
},
"resolveJsonModule": true,
"types": [
"vitest/globals",
"@testing-library/jest-dom",
"vite-plugin-solid-svg/types"
],
"allowImportingTsExtensions": true,

View file

@ -1,3 +0,0 @@
import shared from '../../shared.config';
export default shared`frontend`.vitest;

29
eslint.config.js Normal file
View file

@ -0,0 +1,29 @@
import { defineConfig } from '@flowr/eslint';
// @ts-ignore -- No types for this package
import { tanstackConfig } from '@tanstack/eslint-config';
export default defineConfig(
{
typescript: true,
react: true,
query: true,
ignores: ['*.rs', '**/migrations/**'],
rules: {
'ts/no-use-before-define': 'off',
},
toml: {
overrides: {
'toml/array-element-newline': ['error', 'always'],
'toml/no-mixed-type-in-array': ['off'],
},
},
},
// Hack to fix an eslint error with plugin names
// @ts-ignore -- No types for this package
tanstackConfig.map((config) => {
if (config.rules && config.rules['@stylistic/js/spaced-comment'])
delete config.rules['@stylistic/js/spaced-comment'];
return config;
}),
);

View file

@ -1,17 +0,0 @@
import { defineConfig } from '@flowr/eslint-config';
export default defineConfig({
typescript: true,
solid: true,
unocss: true,
ignores: ['*.rs', '**/migrations/**'],
rules: {
'ts/no-use-before-define': 'off',
},
toml: {
overrides: {
'toml/array-element-newline': ['error', 'always'],
'toml/no-mixed-type-in-array': ['off'],
},
},
});

View file

@ -15,37 +15,30 @@
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"engines": {
"pnpm": ">=9.0.0",
"pnpm": ">=10.0.0",
"npm": "pnpm",
"yarn": "pnpm",
"node": ">=18.18 <19 || >=20"
"node": ">=22"
},
"scripts": {
"prep": "pnpm scripts prep",
"typecheck": "pnpm -r typecheck",
"bootstrap": "cargo clean && ./packages/scripts/setup.sh && pnpm install && pnpm prep && pnpm codegen",
"autoformat": "./packages/scripts/format.sh",
"test": "pnpm test:vi && pnpm --filter=./* run test",
"format": "pnpm format:es && pnpm format:rs",
"lint": "pnpm lint:es && pnpm lint:rs",
"migrate": "sea-orm-cli migrate -d ./packages/migration",
"entity:gen": "sea-orm-cli generate entity -o packages/entity/src/entities --with-serde both --model-extra-attributes 'onelauncher_macro::specta' -t",
"frontend": "pnpm --filter @onelauncher/frontend -- ",
"onelauncher:frontend": "pnpm --filter @onelauncher/frontend -- ",
"onelauncher:desktop": "pnpm --filter @onelauncher/desktop -- ",
"oneclient:frontend": "pnpm --filter @oneclient/frontend -- ",
"oneclient:desktop": "pnpm --filter @oneclient/desktop -- ",
"client": "pnpm --filter @onelauncher/client -- ",
"core": "pnpm --filter @onelauncher/core -- ",
"scripts": "pnpm --filter @onelauncher/scripts -- ",
"tauri": "pnpm desktop tauri-ext",
"postupdate": "cargo update",
"test:vi": "pnpm vitest run",
"test:update": "pnpm vitest -u",
"test:ui": "pnpm vitest --ui",
"lint:es": "eslint . --cache --flag unstable_ts_config",
"format": "pnpm format:es && pnpm format:rs",
"format:es": "pnpm lint:es --fix",
"lint:rs": "cargo +nightly fmt --check && cargo clippy -- -D warnings",
"format:rs": "cargo +nightly fmt && cargo clippy --fix"
"format:rs": "cargo +nightly fmt && cargo clippy --fix",
"lint": "pnpm lint:es && pnpm lint:rs",
"lint:es": "eslint . --cache --flag unstable_ts_config",
"lint:rs": "cargo +nightly fmt --check && cargo clippy -- -D warnings"
},
"workspaces": {
"packages": [
@ -54,18 +47,20 @@
]
},
"devDependencies": {
"@flowr/eslint-config": "^4.4.1",
"@eslint-react/eslint-plugin": "^1.48.5",
"@flowr/eslint": "^6.0.0",
"@tanstack/eslint-config": "^0.1.0",
"@tanstack/eslint-plugin-query": "5.74.7",
"@tanstack/eslint-plugin-router": "1.115.0",
"@types/node": "^22.7.9",
"@unocss/eslint-plugin": "^0.63.6",
"@vitest/ui": "^2.1.3",
"eslint": "^9.13.0",
"eslint-plugin-solid": "^0.14.3",
"@unocss/eslint-plugin": "^66.0.0",
"eslint": "^9.20.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-solid": "^0.14.5",
"jiti": "^2.3.3",
"rimraf": "^6.0.1",
"ts-node": "^10.9.2",
"tsx": "^4.19.1",
"typescript": "^5.6.3",
"vite": "^5.4.10",
"vitest": "^2.1.3"
"tsx": "^4.19.4",
"typescript": "^5.8.3",
"vite": "^6.3.4"
}
}

View file

@ -1,3 +0,0 @@
# wip library expecting rspc but can work with tauri-specta too
h

View file

@ -1,44 +0,0 @@
{
"name": "@onelauncher/client",
"type": "module",
"version": "0.1.0-alpha.2",
"private": true,
"description": "RSPC Client and Utilities for OneLauncher",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git@github.com:polyfrost/onelauncher",
"directory": "packages/client"
},
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"exports": {
".": {
"import": "./src/index.ts"
},
"./bindings": {
"import": "./src/bindings.ts"
},
"./utils": {
"import": "./src/utils/index.ts"
}
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@solid-primitives/deep": "^0.2.9",
"@tanstack/solid-query": "^5.56.2",
"@tauri-apps/api": "^2.0.3",
"highlight.js": "^11.10.0",
"markdown-it": "^14.1.0",
"plausible-tracker": "^0.3.9",
"solid-js": "^1.9.1",
"xss": "^1.0.15"
},
"devDependencies": {
"@types/markdown-it": "^14.1.2"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,2 +0,0 @@
export const WEBSITE = 'https://polyfrost.org/';
export const TRANSLATION_WEBSITE = 'https://weblate.polyfrost.org/';

View file

@ -1,3 +0,0 @@
export * from './useDebug';
export * from './usePlausible';
export * from './useQuery';

View file

@ -1,36 +0,0 @@
import { createEffect, createSignal } from 'solid-js';
import { createMutable } from 'solid-js/store';
import { createPersistedMutable, useSolidStore } from '../library';
export interface DebugState {
enabled: boolean;
rspcLogger: boolean;
shareFullTelemetry: boolean;
telemetryLogging: boolean;
}
export const debugState = createPersistedMutable(
'onelauncher-debugState',
createMutable<DebugState>({
enabled: globalThis.isDevelopment,
rspcLogger: false,
shareFullTelemetry: false,
telemetryLogging: false,
}),
);
export const useDebugState = () => useSolidStore(debugState);
export function useDebugToggle(): () => void {
const [toggled, setToggled] = createSignal(0);
createEffect(() => {
if (toggled() >= 5)
debugState.enabled = true;
const timeout = setTimeout(() => setToggled(0), 1000);
return () => clearTimeout(timeout);
});
return () => setToggled(toggled() + 1);
}

View file

@ -1,309 +0,0 @@
import type { PlausibleOptions as PlausibleTrackerOptions } from 'plausible-tracker';
import type { ProgramInfo } from '../bindings';
import Plausible from 'plausible-tracker';
import { createEffect, createSignal } from 'solid-js';
import { createMutable } from 'solid-js/store';
import { createPersistedMutable, useSolidStore } from '../library';
import { getProgramInfo } from '../utils';
import { useDebugState } from './useDebug';
/**
* Possible platform types for Plausible.
*/
export type PlausiblePlatformType = 'desktop' | 'unknown';
/**
* The Plausible analytics state.
*/
export interface PlausibleState {
shareFullTelemetry: boolean;
platform: PlausiblePlatformType;
programInfo: ProgramInfo;
}
/**
* The mutable solid {@link PlausibleState} holder.
*/
export const plausibleState: PlausibleState = createPersistedMutable(
'onelauncher-layout',
createMutable<PlausibleState>({
shareFullTelemetry: false, // false by default
platform: 'unknown',
programInfo: getProgramInfo(),
}),
);
/**
* A solid hook for using the solid store {@link plausibleState}.
*/
export const usePlausibleState = (): PlausibleState => useSolidStore(plausibleState);
const PlausibleProvider = Plausible({
trackLocalhost: true,
domain: 'app.polyfrost.org',
apiHost: 'https://analytics.polyfrost.org',
});
/**
* Defines all possible options that may be provided by events upon submission.
*
* Extends the {@link PlausibleTrackerOptions} provided by the `plausible-tracker`
* package with additional options for custom functionality.
*/
export type PlausibleOptions = PlausibleTrackerOptions & { telemetryOverride?: boolean };
/**
* A base Plausible event, that all other events are derived from for type safety.
*/
interface BasePlausibleEventWithOption<T, O extends keyof PlausibleOptions> {
type: T;
plausibleOptions: Required<{ [K in O]: PlausibleOptions[O]; }>;
}
interface BasePlausibleEventWithoutOption<T> { type: T }
type BasePlausibleEvent<T, O = void> = O extends keyof PlausibleOptions
? BasePlausibleEventWithOption<T, O>
: BasePlausibleEventWithoutOption<T>;
// Plausible events
type PageViewEvent = BasePlausibleEvent<'pageview', 'url'>;
type ClusterCreateEvent = BasePlausibleEvent<'clusterCreate'>;
type ClusterDeleteEvent = BasePlausibleEvent<'clusterDelete'>;
type ModAddedEvent = BasePlausibleEvent<'modAdded'>;
type ModRemovedEvent = BasePlausibleEvent<'modRemoved'>;
type PingEvent = BasePlausibleEvent<'ping'>;
/** All plausible events, added as goals for the currently active domain. */
type PlausibleEvent =
| PageViewEvent
| ClusterCreateEvent
| ClusterDeleteEvent
| ModAddedEvent
| ModRemovedEvent
| PingEvent;
/**
* A wrapper for Plausible events to be handled internally.
*
* Allows for in-depth event logging to console and Plausible.
*/
interface PlausibleTrackerEvent {
eventName: string;
props: {
platform: PlausiblePlatformType;
fullTelemetry: boolean;
coreVersion: string;
commit: string;
debug: boolean;
};
options: PlausibleTrackerOptions;
readonly callback?: () => void;
}
interface SubmitEventProps {
/** The {@link PlausibleEvent plausible event} to submit. */
event: PlausibleEvent;
/** The current {@link PlausiblePlatformType platform type}. */
platformType: PlausiblePlatformType;
/** An optional screen width. Defaults to `window.screen.width` */
screenWidth?: number;
/** Whether or not full telemetry is enabled for the current client. `usePlausibleState().shareFullTelemetry` */
shareFullTelemetry: boolean;
/** Whether or not debug is enabled for the current client. */
debugState: {
enabled: boolean;
shareFullTelemetry: boolean;
telemetryLogging: boolean;
};
programInfo: ProgramInfo;
}
/**
* Submits an event directly to our Plausible server.
*
* **Avoid using this directly, only send telemetry when it is certain that it has been
* allowed by the user. Prefer the {@link usePlausibleEvent `usePlausibleEvent`} hook.**
*
* @remarks
* If any of the conditions are met, this will return and no data will be submitted:
*
* - The app is in debug or development mode.
* - A telemetry override is present and is not true.
* - No telemetry override is present, and telemetry sharing is not true.
*
* Telemetry sharing settings are never matched to `=== false` but rather to
* `!== true`. This is so we can gaurentee that **nothing** will be sent
* unless the user explicitly allows it.
*
* @see {@link https://plausible.io/docs/custom-event-goals Plausible custom events}
* @see {@link https://plausible-tracker.netlify.app/#tracking-custom-events-and-goals Tracking custom Plausible events}
*/
async function submitPlausibleEvent({ event, debugState, ...props }: SubmitEventProps): Promise<void> {
if (props.platformType === 'unknown')
return;
if (debugState.enabled && debugState.shareFullTelemetry !== true)
return;
if (
'plausibleOptions' in event && 'telemetryOverride' in event.plausibleOptions
? event.plausibleOptions.telemetryOverride !== true
: props.shareFullTelemetry !== true && event.type !== 'ping'
)
return;
const fullEvent: PlausibleTrackerEvent = {
eventName: event.type,
props: {
platform: props.platformType,
fullTelemetry: props.shareFullTelemetry,
coreVersion: props.programInfo.launcher_version,
commit: props.programInfo.tauri_version,
debug: debugState.enabled,
},
options: {
domain: 'app.polyfrost.org',
deviceWidth: props.screenWidth ?? window.screen.width,
referrer: '',
...('plausibleOptions' in event ? event.plausibleOptions : undefined),
},
callback: debugState.telemetryLogging
? () => {
const { callback: _, ...event } = fullEvent;
// eslint-disable-next-line no-console -- debug info
console.log(event);
}
: undefined,
};
PlausibleProvider.trackEvent(
fullEvent.eventName,
{
props: fullEvent.props,
callback: fullEvent.callback,
},
fullEvent.options,
);
}
interface EventSubmissionCallbackProps {
/** The {@link PlausibleEvent Plausible event} to be submitted. */
event: PlausibleEvent;
}
/**
* Submits a Plausible Analytics event with a hook.
*
* The returned callback will only the fired once as to avoid flooding analytics.
*
* Certain events provide functionality to override telemetry sharing. This is because
* it should only be used in contexts where telemetry sharing must be allowed by external means.
*
* @remarks
* If any of the conditions are met, this will return and no data will be submitted:
*
* - The app is in debug or development mode.
* - A telemetry override is present and is not true.
* - No telemetry override is present, and telemetry sharing is not true.
*
* @returns a callback that, once executed, will submit the event.
*
* @example
* ```ts
* const submitPlausibleEvent = usePlausibleEvent();
*
* const createdCluster = commands.createCluster('myCluster').then(cluster => {
* submitPlausibleEvent({
* event: {
* type: 'clusterCreate',
* },
* });
* return cluster;
* });
* ```
*/
export function usePlausibleEvent(): (props: EventSubmissionCallbackProps) => Promise<void> {
const debugState = useDebugState();
const plausibleState = usePlausibleState();
const [previousEvent, setPreviousEvent] = createSignal<BasePlausibleEvent<string> | null>(null);
return async (props: EventSubmissionCallbackProps) => {
if (previousEvent() === props.event)
return;
setPreviousEvent(props.event);
await submitPlausibleEvent({
debugState,
shareFullTelemetry: plausibleState.shareFullTelemetry,
platformType: plausibleState.platform,
programInfo: plausibleState.programInfo,
...props,
});
};
}
export interface PlausibleMonitorProps {
/** A sanitized string containing the current active path the client is viewing. */
currentPath: string;
}
/**
* Submits a Plausible Analytics `'pageview'` event with a hook.
*
* This should watch the Solid router's current path, and send an event if the path is changed.
*
* @remarks
* If any of the conditions are met, this will return and no data will be submitted:
*
* - The app is in debug or development mode.
* - A telemetry override is present and is not true.
* - No telemetry override is present, and telemetry sharing is not true.
*/
export function usePlausiblePageview({ currentPath }: PlausibleMonitorProps): void {
const plausibleEvent = usePlausibleEvent();
createEffect(() => {
plausibleEvent({
event: {
type: 'pageview',
plausibleOptions: { url: currentPath },
},
});
});
}
/**
* Submits a Plausible Analytics `'ping'` event with a hook.
*
* This should watch the Solid router's current path, and send an event if the path is changed.
*
* @remarks
* If any of the conditions are met, this will return and no data will be submitted:
*
* - The app is in debug or development mode.
* - A telemetry override is present and is not true.
* - No telemetry override is present, and telemetry sharing is not true.
*/
export function usePlausiblePing() {
const plausibleEvent = usePlausibleEvent();
createEffect(() => {
plausibleEvent({
event: {
type: 'ping',
},
});
});
}
export interface PlausibleInitProps {
platformType: PlausiblePlatformType;
programInfo: ProgramInfo;
}
export function initializePlausible(props: PlausibleInitProps): void {
plausibleState.platform = props.platformType;
plausibleState.programInfo = props.programInfo;
}

View file

@ -1,2 +0,0 @@
export { createQuery } from '@tanstack/solid-query';
export { createQuery as default } from '@tanstack/solid-query';

View file

@ -1,20 +0,0 @@
declare global {
// eslint-disable-next-line vars-on-top, no-var -- global patched variable
var isDevelopment: boolean;
// eslint-disable-next-line vars-on-top, no-var -- global patched variable
var onHotReload: (cb: () => void) => void | undefined;
}
if (globalThis.isDevelopment === undefined || globalThis.localStorage === undefined)
throw new Error('please ensure you have patched `globalThis` before importing the client');
declare global {
// global patched function because tauri/webkit does weird promise polyfills
export function confirm(): boolean | Promise<boolean>;
}
export * from './constants';
export * from './hooks';
export * from './library';
export type * from './types';
export * from './utils';

View file

@ -1,42 +0,0 @@
import type { StoreNode } from 'solid-js/store';
import { trackDeep } from '@solid-primitives/deep';
import { createEffect, createRoot } from 'solid-js';
export interface CreatePersistedMutableOpts<T> {
onSave?: (value: T) => T;
}
// copied from `makePersisted` in `@solid-primitives/storage` with `solid-js/store`.
export function createPersistedMutable<T extends StoreNode>(
key: string,
mutable: T,
opts?: CreatePersistedMutableOpts<T>,
): T {
try {
const value = localStorage.getItem(key);
if (value) {
const persisted = JSON.parse(value);
Object.assign(mutable, persisted);
}
}
catch (err) {
console.error(`failed to load persisted state from localStorage['${key}']: ${err}`);
}
const dispose = createRoot((dispose) => {
createEffect(() => {
trackDeep(mutable);
const item = opts?.onSave
? JSON.stringify(opts.onSave(mutable))
: JSON.stringify(mutable);
localStorage.setItem(key, item);
});
return dispose;
});
if ('onHotReload' in globalThis)
globalThis?.onHotReload(dispose);
return mutable;
}

View file

@ -1,3 +0,0 @@
export * from './createPersistedMutable';
export * from './useObserver';
export * from './useSolidStore';

View file

@ -1,68 +0,0 @@
import type { Owner } from 'solid-js';
import { createEffect, createReaction, createRoot, createSignal, onCleanup, runWithOwner } from 'solid-js';
export interface Observer {
dispose: () => void;
track: (fn: () => void) => void;
};
// copied from `useObserver` in `react-solid-state`, ported to work with new versions.
// https://github.com/solidjs/react-solid-state/issues/4
export function useObserver<T>(fn: () => T): T {
const [tick, setTick] = createSignal(0);
const state = {
onUpdate: (): void => {
state.firedDuringRender = true;
},
firstRenderFired: false,
firedDuringRender: false,
};
let reaction: Observer | undefined;
reaction = createRoot(dispose => ({
dispose,
track: createReaction(() => state.onUpdate()),
}));
createEffect(() => {
if (state.firedDuringRender)
setTick(tick() + 1);
state.onUpdate = () => setTick(tick() + 1);
state.firstRenderFired = true;
onCleanup(() => {
state.onUpdate = () => {
state.firedDuringRender = false;
};
if (!state.firstRenderFired) {
reaction?.dispose();
reaction = undefined;
}
});
});
let rendering!: T;
reaction.track(() => (rendering = fn()));
return rendering;
}
export interface OwnedObserver { track: (fn: () => void) => void }
export function useObserverWithOwner<T>(owner: Owner, fn: () => T): T {
const [tick, setTick] = createSignal(0);
const state = { onUpdate: () => {} };
const reaction: OwnedObserver = runWithOwner(owner, () => ({
track: createReaction(() => state.onUpdate()),
}))!;
createEffect(() => {
state.onUpdate = () => setTick(tick() + 1);
});
let rendering!: T;
reaction.track(() => (rendering = fn()));
return rendering;
}

View file

@ -1,13 +0,0 @@
import type { Store } from 'solid-js/store';
import { useObserver } from './useObserver';
/**
* A wrapper for {@link useObserver} to integrate with `solid-js/store`.
*/
export function useSolidStore<T extends object = object>(store: Store<T>): T {
const state = useObserver(() => ({ ...store }));
return new Proxy(state, {
get: (target, prop) => Reflect.get(target, prop),
set: (_, prop, value) => Reflect.set(store, prop, value),
});
}

View file

@ -1 +0,0 @@
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

View file

@ -1,51 +0,0 @@
import hljs from 'highlight.js/lib/core';
import { configuredXss, md, noop } from './markdown';
function langFactory(lang: string) {
return (imp: typeof import('highlight.js/lib/languages/*')) => {
hljs.registerLanguage(lang, imp.default);
};
}
export function registerLanguages() {
const _languages = [
import('highlight.js/lib/languages/javascript').then(langFactory('javascript')),
import('highlight.js/lib/languages/python').then(langFactory('python')),
import('highlight.js/lib/languages/lua').then(langFactory('lua')),
import('highlight.js/lib/languages/kotlin').then(langFactory('kotlin')),
import('highlight.js/lib/languages/scala').then(langFactory('scala')),
import('highlight.js/lib/languages/groovy').then(langFactory('groovy')),
import('highlight.js/lib/languages/gradle').then(langFactory('gradle')),
import('highlight.js/lib/languages/json').then(langFactory('json')),
import('highlight.js/lib/languages/ini').then(langFactory('ini')),
import('highlight.js/lib/languages/yaml').then(langFactory('yaml')),
import('highlight.js/lib/languages/properties').then(langFactory('properties')),
];
}
registerLanguages();
hljs.registerAliases(['js'], { languageName: 'javascript' });
hljs.registerAliases(['py'], { languageName: 'python' });
hljs.registerAliases(['kt', 'kts'], { languageName: 'kotlin' });
hljs.registerAliases(['json5'], { languageName: 'json' });
hljs.registerAliases(['toml'], { languageName: 'ini' });
hljs.registerAliases(['yml'], { languageName: 'yaml' });
hljs.registerAliases(['html', 'htm', 'xhtml', 'mcui', 'fxml'], { languageName: 'xml' });
export function renderHighlightedString(src: string): string {
return configuredXss.process(
md({
highlight(str, lang) {
if (lang && hljs.getLanguage(lang))
try {
return hljs.highlight(str, { language: lang }).value;
}
catch (err) {
noop(err);
}
return '';
},
}).render(src),
);
}

View file

@ -1,6 +0,0 @@
export * from './highlight';
export * from './markdown';
export * from './modrinth';
export * from './programInfo';
export * from './sorting';
export * from './timer';

View file

@ -1,165 +0,0 @@
import MarkdownIt, { type Options as MarkdownOptions } from 'markdown-it';
import { escapeAttrValue, FilterXSS, safeAttrValue, whiteList } from 'xss';
// from https://github.com/modrinth/code/blob/main/packages/utils/parse.ts for parsing modrinth style markdown
export const configuredXss: FilterXSS = new FilterXSS({
whiteList: {
...whiteList,
summary: [],
h1: ['id'],
h2: ['id'],
h3: ['id'],
h4: ['id'],
h5: ['id'],
h6: ['id'],
kbd: ['id'],
input: ['checked', 'disabled', 'type'],
iframe: ['width', 'height', 'allowfullscreen', 'frameborder', 'start', 'end'],
img: [...(whiteList.img || []), 'usemap', 'style', 'align'],
map: ['name'],
area: [...(whiteList.a || []), 'coords'],
a: [...(whiteList.a || []), 'rel'],
td: [...(whiteList.td || []), 'style'],
th: [...(whiteList.th || []), 'style'],
picture: [],
source: ['media', 'sizes', 'src', 'srcset', 'type'],
p: [...(whiteList.p || []), 'align'],
div: [...(whiteList.p || []), 'align'],
// Other
center: [],
code: [],
strike: [],
strong: [],
em: [],
b: [],
blockquote: [],
table: [],
tbody: [],
tr: [],
video: [],
},
css: {
whiteList: {
'image-rendering': /^pixelated$/,
'text-align': /^center|left|right$/,
'float': /^left|right$/,
},
},
onIgnoreTagAttr: (tag, name, value) => {
if (tag === 'iframe' && name === 'src') {
const allowed = [
{
url: /^https?:\/\/(www\.)?youtube(-nocookie)?\.com\/embed\/[\w-]{11}/,
allowedParameters: [/start=\d+/, /end=\d+/],
},
{
url: /^https?:\/\/(www\.)?discord\.com\/widget/,
allowedParameters: [/id=\d{18,19}/],
},
];
const url = new URL(value);
for (const src of allowed) {
if (!src.url.test(url.href))
continue;
const newSearchParams = new URLSearchParams();
url.searchParams.forEach((v, k) => {
if (!src.allowedParameters.some(p => p.test(`${k}=${v}`)))
newSearchParams.delete(k);
});
url.search = newSearchParams.toString();
return `${name}="${escapeAttrValue(url.toString())}"`;
}
}
if (name === 'class' && ['code', 'pre', 'span'].includes(tag)) {
const allowedClasses: string[] = [];
for (const className of value.split(/\s/g))
if (className.startsWith('hljs-') || className.startsWith('language-'))
allowedClasses.push(className);
return `${name}="${escapeAttrValue(allowedClasses.join(' '))}"`;
}
return undefined;
},
safeAttrValue: (tag, name, value, cssFilter) => {
if (tag === 'img' && name === 'src' && !value.startsWith('data:'))
try {
const url = new URL(value);
if (url.hostname.includes('wsrv.nl'))
url.searchParams.delete('errorredirect');
const allowed = [
'imgur.com',
'i.imgur.com',
'cdn-raw.modrinth.com',
'cdn.modrinth.com',
'staging-cdn-raw.modrinth.com',
'staging-cdn.modrinth.com',
'github.com',
'raw.githubusercontent.com',
'img.shields.io',
'wsrv.nl',
'cf.way2muchnoise.eu',
'bstats.org',
];
if (!allowed.includes(url.hostname))
return safeAttrValue(
tag,
name,
`https://wsrv.nl/?url=${encodeURIComponent(
url.toString().replaceAll('&amp;', '&'),
)}&n=-1`,
cssFilter,
);
return safeAttrValue(tag, name, url.toString(), cssFilter);
}
catch (err) {
noop(err);
}
return safeAttrValue(tag, name, value, cssFilter);
},
});
export function md(options: MarkdownOptions = {}): MarkdownIt {
const md = new MarkdownIt('default', { html: true, linkify: true, ...options });
const defaultLinkOpenRenderer
= md.renderer.rules.link_open
|| ((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options));
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
const token = tokens[idx]!;
const index = token.attrIndex('href');
if (token.attrs && index !== -1) {
const href = token.attrs[index]![1];
try {
const url = new URL(href);
const allowed = ['modrinth.com'];
if (allowed.includes(url.hostname))
return defaultLinkOpenRenderer(tokens, idx, options, env, self);
}
catch (err) {
noop(err);
}
}
tokens[idx]!.attrSet('rel', 'noopener nofollow ugc');
return defaultLinkOpenRenderer(tokens, idx, options, env, self);
};
return md;
}
export const renderString = (src: string): string => configuredXss.process(md().render(src));
export function noop(_t: any) {}

View file

@ -1,8 +0,0 @@
export const MR_STAFF_ROLS = ['moderator', 'admin'];
export const MR_APPROVED = ['approved', 'archived', 'unlisted', 'private'];
export const MR_LISTED = ['approved', 'archived'];
export const MR_UNLISTED = ['unlisted', 'withheld'];
export const MR_PRIVATE = ['private', 'rejected', 'processing'];
export const MR_REJECTED = ['rejected', 'withheld'];
export const MR_REVIEWING = ['processing'];
export const MR_DRAFT = ['draft'];

View file

@ -1,30 +0,0 @@
import { commands, type ProgramInfo } from '../bindings';
let _programInfo: ProgramInfo = {
arch: 'unknown',
platform: 'unknown',
dev_build: true,
launcher_version: 'unknown',
tauri_version: 'unknown',
webview_version: 'unknown',
};
export async function initProgramInfo() {
const result = await commands.getProgramInfo();
if (result.status === 'ok')
_programInfo = result.data;
setBodyAttributes();
}
export const getProgramInfo = () => _programInfo;
function setBodyAttributes() {
document.body.setAttribute('data-platform', _programInfo.platform);
if (_programInfo.platform === 'linux') {
const minorVersion = Number.parseInt(_programInfo.webview_version.split('.')[1] || 'NaN');
// Check if WebKitGTK version is 2.46 or higher (Skia renderer implemented which fixes some issues)
document.body.setAttribute('data-skia-renderer', (!Number.isNaN(minorVersion) && minorVersion >= 46).toString());
}
}

View file

@ -1,39 +0,0 @@
import { type Accessor, createSignal, type Setter } from 'solid-js';
export type SortingFunction<T> = (a: T, b: T) => number;
export type Sortables<T> = Record<string, SortingFunction<T>>;
interface CreateSortable<T, S extends Sortables<T> = Sortables<T>> {
list: Accessor<T[]>;
key: Accessor<keyof S>;
setList: Setter<T[]>;
setKey: (val: keyof S | number) => void;
sortables: S;
}
export function createSortable<T, S extends Sortables<T> = Sortables<T>>(initList: T[], initSortables: S): CreateSortable<T, S> {
const sortables = initSortables;
const [list, setList] = createSignal<T[]>(initList);
const [key, setKey] = createSignal<keyof S>(Object.keys(sortables)[0]!);
return {
list,
key,
setList,
setKey: (val: keyof S | number) => {
if (typeof val === 'string') {
setKey(val);
}
else if (typeof val === 'number') {
const key = Object.keys(sortables).at(val);
if (key === undefined)
throw new Error('Key cannot be undefined!');
setKey(key);
}
},
sortables,
};
}
export default createSortable;

View file

@ -1,59 +0,0 @@
export class PausableTimer {
private _timeout: NodeJS.Timeout;
private _paused: boolean = false;
private _unixStarted: number = 0;
private _unixStopped: number = 0;
constructor(
private readonly callback: () => unknown,
private readonly duration: number,
private readonly interval: boolean = false,
) {
this._timeout = this._createTimeout();
}
private _createTimeout(duration: number = this.duration): NodeJS.Timeout {
this._unixStarted = Date.now();
if (this.interval)
return setInterval(this.callback, duration);
else
return setTimeout(this.callback, duration);
}
private _clearTimeout() {
this._unixStopped = Date.now();
if (this.interval)
clearInterval(this._timeout);
else
clearTimeout(this._timeout);
}
public resume(restart: boolean = false) {
const elapsed = this._unixStopped - this._unixStarted;
const duration = this.interval || restart ? this.duration : this.duration - elapsed;
this._paused = false;
this._timeout = this._createTimeout(duration);
}
public pause() {
this._clearTimeout();
this._paused = true;
}
public stop() {
this._clearTimeout();
}
public get timeout() {
return this._timeout;
}
public get paused() {
return this._paused;
}
}
export default function createPausableTimer(callback: () => unknown, duration: number): PausableTimer {
return new PausableTimer(callback, duration);
}

View file

@ -1,10 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"exactOptionalPropertyTypes": false,
"emitDeclarationOnly": false,
"outDir": "./dist"
},
"include": ["src/**/*.ts", "src/**/*.d.ts"]
}

View file

@ -1,3 +0,0 @@
import shared from '../../shared.config';
export default shared`client`.vitest;

5501
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
packages:
- apps/*
- apps/oneclient/*
- apps/onelauncher/*
- packages/*
- .github/actions/*

View file

@ -1,6 +1,6 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"types": ["vitest/globals", "vite/client", "node"]
"types": ["vite/client", "node"]
}
}

View file

@ -1 +1 @@
export { default } from './apps/frontend/uno.config';
export { default } from './apps/onelauncher/frontend/uno.config';