fix: test card double click

This commit is contained in:
Tomáš Mládek 2026-01-20 20:53:22 +01:00
parent 1cedd1da8d
commit ca05630583
20 changed files with 482 additions and 437 deletions

View file

@ -11,3 +11,5 @@ node_modules
pnpm-lock.yaml
package-lock.json
yarn.lock
.dagger

View file

@ -2,3 +2,4 @@
pnpm-lock.yaml
package-lock.json
yarn.lock
.dagger

View file

@ -3,6 +3,6 @@
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

175
AGENTS.md Normal file
View file

@ -0,0 +1,175 @@
# AGENTS
This file orients coding agents for the test-card repo.
Stack: SvelteKit + Vite + TypeScript + Vitest + ESLint + Prettier.
Package scripts are in `package.json` at repo root.
## Repository layout
- `src/` SvelteKit app source.
- `src/routes/` route and load files.
- `src/lib/` shared code exposed via `$lib`.
- `tests/` Vitest tests and helpers.
- `av-sync/` small Vite + Svelte app for AV sync tools.
- `assets/` generated assets (see `npm run generate-assets`).
- `tests/output/` visual regression artifacts (baseline/current/diff).
## Setup
- Use the existing package manager for scripts; root scripts assume `npm`.
- Install deps with `npm install` if needed.
- Do not add new dependencies without explicit permission.
## Common commands
- Dev server: `npm run dev`.
- Build: `npm run build`.
- Preview build: `npm run preview`.
- Typecheck: `npm run check`.
- Typecheck (watch): `npm run check:watch`.
- Lint: `npm run lint`.
- Format: `npm run format`.
- Generate assets: `npm run generate-assets`.
- AV sync dev server: `npm run av:dev` (runs inside `av-sync/`).
- AV render video: `npm run av:render:video -- <args>`.
- AV render audio: `npm run av:render:audio`.
## Tests
- Test runner: Vitest (no `test` script defined).
- Run all tests once: `npx vitest --run`.
- Run all tests in watch mode: `npx vitest`.
- Run a single file: `npx vitest --run tests/test-card.test.ts`.
- Run a single test by name: `npx vitest --run -t "matches baseline"`.
- Filter by file and name: `npx vitest --run tests/test-card.test.ts -t "matches baseline"`.
### Visual regression test notes
- The test starts its own dev server on port 5888.
- It uses Puppeteer; first run creates `tests/output/testcard-baseline.png`.
- If the baseline changes on purpose, delete the baseline and rerun.
- Diffs are written to `tests/output/testcard-diff.png` when mismatches exist.
## Formatting
- Prettier is the source of truth.
- Tabs for indentation (`useTabs: true`).
- Single quotes (`singleQuote: true`).
- No trailing commas (`trailingComma: none`).
- Max line width 100 (`printWidth: 100`).
- Svelte files use the Svelte parser.
## Linting
- ESLint with TypeScript + Svelte recommended rules and Prettier.
- ESLint ignores `av-sync/**` and all `*.js` files.
- Prefer fixing lint at the source rather than disabling rules.
## TypeScript and SvelteKit
- `tsconfig.json` is strict; keep types accurate.
- Prefer `type` imports: `import type { Foo } from '...'`.
- Keep `export const` config in SvelteKit route files.
- Use `+page.ts`/`+layout.ts` conventions and `$types` imports.
- Use `$lib` alias for shared code inside `src/lib/`.
- Keep server-only logic in `*.server.ts` or server load functions.
## Import style
- Order imports: external packages, internal `$lib` or alias, relative paths.
- Separate type-only imports from runtime imports when it improves clarity.
- Avoid deep relative paths when `$lib` can be used.
- Keep named imports sorted when it improves readability.
## Naming conventions
- Components: `PascalCase.svelte`.
- Functions/variables: `camelCase`.
- Constants: `UPPER_SNAKE_CASE` for true constants.
- Routes: use SvelteKit folder conventions; keep names short and descriptive.
- Files: favor `kebab-case` for non-component utilities.
## Code structure
- Use `const` by default; `let` only when reassigned.
- Prefer `async/await` over `.then()` chains.
- Keep functions small and focused; extract helpers to `src/lib/`.
- Keep Svelte components thin; move heavy logic to modules.
## Error handling
- Throw `Error` objects with clear messages.
- Catch errors only when you can add context or recover.
- Avoid silent failures; log or rethrow with added context.
- For async utilities, reject with `Error` (not strings).
## Testing style
- Use Vitest `describe/it/expect`.
- Keep tests deterministic; avoid time-based flakiness.
- Store artifacts under `tests/output/` only.
- Use helper functions in `tests/utils.ts` for shared setup.
## Styling and UI
- This project uses Svelte; prefer local component styles.
- Keep global styles minimal and intentional.
- If editing fonts or assets, ensure they remain licensed in dependencies.
## Configuration files
- `vite.config.ts` defines the `@assets` alias and inlang plugin.
- `svelte.config.js` uses the static adapter.
- Keep config changes minimal and documented in code when needed.
## Dependency policy
- Do not add, remove, or upgrade deps without explicit permission.
- If you want a new dependency, explain why and ask first.
## Cursor/Copilot rules
- No `.cursor/rules/`, `.cursorrules`, or `.github/copilot-instructions.md` found.
## Agent workflow tips
- Prefer small, focused edits with Prettier-friendly formatting.
- Run `npm run lint` and `npx vitest --run` for changes touching logic.
- For UI-only tweaks, at least run `npm run lint`.
- For config changes, run `npm run check`.
- For AV sync changes, validate via `npm run av:dev`.
## When adding new files
- Follow the existing folder structure and naming conventions.
- Update tests if behavior changes.
- Avoid introducing new top-level folders without discussion.
## Logging
- Use `debug` for structured logs in Node utilities.
- Avoid noisy `console.log` in production code.
## Performance
- Avoid unnecessary re-renders in Svelte components.
- Prefer derived values over repeated computation.
## Accessibility
- Keep semantic HTML in Svelte templates.
- Ensure interactive elements are keyboard reachable.
## Data and assets
- Generated assets live under `assets/generated/`.
- Do not edit generated assets by hand.
## Build output
- `npm run build` uses Vite and SvelteKit static adapter.
- `npm run preview` serves the build for verification.
## Notes
- Keep this file updated when commands or conventions change.

124
bun.lock
View file

@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "testcard",
@ -23,6 +24,7 @@
"@inlang/paraglide-js": "^2.0.0",
"@inlang/plugin-m-function-matcher": "^2.1.0",
"@inlang/plugin-message-format": "^4.0.0",
"@tailwindcss/vite": "^4.1.18",
"@tsconfig/svelte": "^5.0.4",
"@types/debug": "^4.1.12",
"@types/eslint": "8.56.0",
@ -31,6 +33,7 @@
"@types/wait-on": "^5.3.4",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"autoprefixer": "^10.4.23",
"commander": "^12.1.0",
"concurrently": "^8.2.2",
"eslint": "^8.57.1",
@ -39,10 +42,13 @@
"node-wav": "^0.0.2",
"pixelmatch": "^7.1.0",
"pngjs": "^7.0.0",
"postcss": "^8.5.6",
"prettier": "^3.5.0",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.7.2",
"puppeteer": "^22.15.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.1.18",
"vitest": "^3.2.4",
"wait-on": "^9.0.1",
},
@ -231,6 +237,36 @@
"@tabler/icons-webfont": ["@tabler/icons-webfont@2.47.0", "", { "dependencies": { "@tabler/icons": "2.47.0" } }, "sha512-yfV9zDal0iYDmyGz4BS9IlhaaMydtLdyOrY2UAZToP65sVWj7AFIi6symNzsoBaX867xAZWVHdKcocah0BfSog=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
"@tsconfig/svelte": ["@tsconfig/svelte@5.0.4", "", {}, "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q=="],
@ -321,6 +357,8 @@
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="],
"axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@ -341,12 +379,16 @@
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg=="],
"basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="],
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
@ -357,6 +399,8 @@
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@ -415,6 +459,8 @@
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="],
"devtools-protocol": ["devtools-protocol@0.0.1312386", "", {}, "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA=="],
@ -425,10 +471,14 @@
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="],
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
@ -517,6 +567,8 @@
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@ -543,6 +595,8 @@
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
@ -593,6 +647,8 @@
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"joi": ["joi@18.0.1", "", { "dependencies": { "@hapi/address": "^5.1.1", "@hapi/formula": "^3.0.2", "@hapi/hoek": "^11.0.7", "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", "@standard-schema/spec": "^1.0.0" } }, "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA=="],
"js-sha256": ["js-sha256@0.11.1", "", {}, "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg=="],
@ -623,6 +679,30 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
@ -663,12 +743,14 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"node-wav": ["node-wav@0.0.2", "", {}, "sha512-M6Rm/bbG6De/gKGxOpeOobx/dnGuP0dz40adqx38boqHhlWssBJZgLCPBNtb9NkrmnKYiV04xELq+R6PFOnoLA=="],
"normalize.css": ["normalize.css@8.0.1", "", {}, "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg=="],
@ -711,7 +793,7 @@
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
"postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
@ -721,12 +803,16 @@
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.5.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA=="],
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.3.3", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw=="],
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.2", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
@ -819,6 +905,10 @@
"svelte-eslint-parser": ["svelte-eslint-parser@0.43.0", "", { "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "postcss": "^8.4.39", "postcss-scss": "^4.0.9" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA=="],
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tar-fs": ["tar-fs@3.0.8", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg=="],
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
@ -863,6 +953,8 @@
"unplugin": ["unplugin@2.3.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"urlpattern-polyfill": ["urlpattern-polyfill@10.0.0", "", {}, "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="],
@ -915,22 +1007,50 @@
"@inlang/paraglide-js/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="],
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"eslint-plugin-svelte/postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"svelte-eslint-parser/postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="],
"unplugin/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"vite/postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="],
"vite-node/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"vitest/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"wait-on/rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"@tailwindcss/node/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"eslint-plugin-svelte/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"svelte-eslint-parser/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"vite/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
}
}

View file

@ -19,6 +19,7 @@
"@inlang/paraglide-js": "^2.0.0",
"@inlang/plugin-m-function-matcher": "^2.1.0",
"@inlang/plugin-message-format": "^4.0.0",
"@tailwindcss/vite": "^4.1.18",
"@tsconfig/svelte": "^5.0.4",
"@types/debug": "^4.1.12",
"@types/eslint": "8.56.0",
@ -27,6 +28,7 @@
"@types/wait-on": "^5.3.4",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"autoprefixer": "^10.4.23",
"commander": "^12.1.0",
"concurrently": "^8.2.2",
"eslint": "^8.57.1",
@ -35,10 +37,13 @@
"node-wav": "^0.0.2",
"pixelmatch": "^7.1.0",
"pngjs": "^7.0.0",
"postcss": "^8.5.6",
"prettier": "^3.5.0",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.7.2",
"puppeteer": "^22.15.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.1.18",
"vitest": "^3.2.4",
"wait-on": "^9.0.1"
},

View file

@ -1,81 +1,72 @@
body,
html {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
@import 'tailwindcss';
color: white;
background-color: black;
font-family:
@theme {
--font-sans:
'Atkinson Hyperlegible', 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
font-size: 20px;
}
* {
box-sizing: border-box;
}
@layer base {
body,
html {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
color: white;
background-color: black;
font-size: 20px;
@apply font-sans;
}
a {
color: white;
}
* {
box-sizing: border-box;
}
h1,
h2,
h3,
h4 {
margin-top: 0;
}
a {
color: white;
}
button,
.button {
display: inline-flex;
align-items: center;
gap: 0.25em;
text-decoration: none;
border: 1px solid white;
cursor: pointer;
padding: 0.25em 0.5em;
border-radius: 0.25em;
background: black;
color: white;
&:disabled {
opacity: 0.7;
h1,
h2,
h3,
h4 {
margin-top: 0;
}
}
input[type='number'],
input[type='search'],
input[type='text'] {
background: transparent;
color: white;
border: 1px solid white;
border-radius: 4px;
padding: 0.2em;
@layer components {
button,
.button {
@apply inline-flex cursor-pointer items-center gap-[0.25em] border border-white no-underline;
@apply rounded-[0.25em] px-[0.5em] py-[0.25em];
@apply bg-black text-white;
&:focus {
outline: solid rgba(255, 255, 255, 0.66);
&:disabled {
@apply opacity-70;
}
}
&:disabled {
opacity: 0.7;
pointer-events: none;
}
}
select {
background: black;
color: white;
padding: 0.25em 0.5em;
border-radius: 0.25em;
border: 1px solid white;
&:disabled {
opacity: 0.7;
input[type='number'],
input[type='search'],
input[type='text'] {
@apply rounded border border-white bg-transparent text-white;
padding: 0.2em;
&:focus {
outline: solid rgba(255, 255, 255, 0.66);
}
&:disabled {
@apply pointer-events-none opacity-70;
}
}
select {
@apply rounded-[0.25em] border border-white bg-black px-[0.5em] py-[0.25em] text-white;
&:disabled {
@apply opacity-70;
}
}
}

View file

@ -43,7 +43,10 @@
--column-width: {columnWidth};
--column-height: {columnHeight};
--left-column: {leftColumn};"
ondblclick={() => onfocus?.() && document.body.requestFullscreen()}
ondblclick={() => {
onfocus?.();
document.body.requestFullscreen();
}}
>
<BackgroundGrid onchange={(detail) => (sizes = detail)} subdued={bg} />

View file

@ -7,28 +7,9 @@
let { children }: Props = $props();
</script>
<a href=".." class="hide-idle"><i class="ti ti-arrow-back"></i> {m.common_back()}</a>
<a
href=".."
class="hide-idle absolute top-8 right-8 z-50 flex items-center gap-2 rounded border border-white bg-black px-4 py-2 no-underline shadow-lg"
><i class="ti ti-arrow-back"></i> {m.common_back()}</a
>
{@render children?.()}
<style>
a {
position: absolute;
top: 2rem;
right: 2rem;
background: black;
border: 1px solid white;
border-radius: 0.2em;
padding: 0.5em 1em;
box-shadow: 0 0 0.5em rgba(255, 255, 255, 0.5);
display: flex;
gap: 0.5em;
align-items: center;
text-decoration: none;
z-index: 99;
}
</style>

View file

@ -11,43 +11,13 @@
</script>
<TestCard bg onfocus={() => goto('/card')} />
<main class:sub={!page.data.root}>
<a href=".." class="button button-back"><i class="ti ti-arrow-back"></i>{m.common_back()}</a>
<main
class="absolute top-1/2 left-1/2 flex h-[90vh] w-[90vw] -translate-x-1/2 -translate-y-1/2 flex-col rounded-lg border border-white bg-black/85 p-4"
>
<a
href=".."
class="button button-back absolute top-4 right-4 opacity-66 transition-opacity hover:opacity-100"
class:hidden={page.data.root}><i class="ti ti-arrow-back"></i>{m.common_back()}</a
>
{@render children?.()}
</main>
<style>
main {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 90vh;
width: 90vw;
background: rgba(0, 0, 0, 0.85);
border-radius: 0.5rem;
border: 1px solid white;
padding: 1rem;
display: flex;
flex-direction: column;
}
.button-back {
position: absolute;
top: 1rem;
right: 1rem;
opacity: 0.66;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
}
main:not(.sub) .button-back {
display: none;
}
</style>

View file

@ -199,14 +199,17 @@
}
</script>
<h1>Total Tech Test</h1>
<h1 class="m-4 text-center text-5xl font-bold tracking-wide uppercase">Total Tech Test</h1>
<div class="locale-select">
<div
class="absolute top-4 right-4 inline-flex items-center gap-2 self-end opacity-50 transition-opacity hover:opacity-100"
>
<label for="locale-picker"><i class="ti ti-language"></i></label>
<select
id="locale-picker"
bind:value={selectedLang}
onchange={(e) => changeLocale((e.target as HTMLSelectElement).value)}
class="rounded border border-current bg-white/8 px-2 py-1 text-sm text-inherit"
>
{#each locales as l}
<option value={l.tag}>{l.native}</option>
@ -214,51 +217,58 @@
</select>
</div>
<nav>
<nav class="flex grow flex-col gap-4 overflow-hidden px-16">
<!-- svelte-ignore a11y_autofocus -->
<input type="search" placeholder={m.search()} bind:value={search} autofocus />
<input type="search" placeholder={m.search()} bind:value={search} autofocus class="w-full px-2" />
<div class="options">
<div class="flex items-center justify-around gap-8 text-xl">
{#each superCategories as category}
<button
onclick={() => setFilter(category.id)}
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
class="super"
class="flex flex-col border-0 bg-transparent opacity-50 transition-opacity duration-200 ease-in-out hover:opacity-100"
class:opacity-100={!filteredCategories.length || filteredCategories.includes(category.id)}
>
<i class="ti {category.icon}"></i>
<i class="ti {category.icon} block text-5xl"></i>
{m[`category_${category.id}`]()}
</button>
{/each}
<div class="separator"></div>
<div class="h-12 border-l border-current opacity-80"></div>
{#each categories as category}
<button
onclick={() => setFilter(category.id)}
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
class="flex flex-col border-0 bg-transparent opacity-50 transition-opacity duration-200 ease-in-out hover:opacity-100"
class:opacity-100={!filteredCategories.length || filteredCategories.includes(category.id)}
>
<i class="ti {category.icon}"></i>
<i class="ti {category.icon} block text-5xl"></i>
{m[`category_${category.id}`]()}
</button>
{/each}
</div>
<div class="tests">
<div class="overflow-y-auto">
{#each nonEmptyCategories as category}
{#if tests.filter( (test) => (test.categories as readonly Category[]).includes(category.id) ).length > 0}
<h2 transition:fade={{ duration: 200 }}>{m[`category_${category.id}`]()}</h2>
<h2 transition:fade={{ duration: 200 }} class="not-first:mt-4 text-2xl">
{m[`category_${category.id}`]()}
</h2>
{#each filteredTests.filter((test) => (test.categories as readonly Category[]).includes(category.id) && filteredCategories.every( (f) => (test.categories as readonly Category[]).includes(f) )) as test}
<a
class="test"
class="mb-1 grid items-center gap-x-1 gap-y-0 text-base/tight text-inherit no-underline"
style="grid-template-columns: auto 1fr;"
href={test.id}
class:disabled={(test as Test).disabled}
class:opacity-50={(test as Test).disabled}
class:pointer-events-none={(test as Test).disabled}
transition:fade={{ duration: 200 }}
>
<i class="ti {test.icon}"></i>
<div class="label">
<span class="name">{m[`tests_${test.id}_label`]()}</span>
<div class="inline-flex items-baseline gap-1">
<span>{m[`tests_${test.id}_label`]()}</span>
{#each test.categories as category}
<span class="category"><i class="ti {categoriesToIcons.get(category)}"></i></span>
<span class="opacity-85"><i class="ti {categoriesToIcons.get(category)}"></i></span>
{/each}
</div>
<div class="description">
<div class="col-start-2 opacity-70">
{m[`tests_${test.id}_description`]()}
</div>
</a>
@ -271,139 +281,9 @@
{/each}
</div>
</nav>
<footer>
<a href="https://git.thm.place/thm/test-card"
<footer class="mt-4 text-center opacity-60">
<a href="https://git.thm.place/thm/test-card" class="no-underline"
>testcard v{version}
{#if version.startsWith('0')}({buildDate}){/if}</a
>
</footer>
<style>
nav {
display: flex;
flex-direction: column;
gap: 1rem;
flex-grow: 1;
padding: 0 4rem;
overflow: hidden;
& input[type='search'] {
padding: 0.5em;
width: 100%;
}
}
.tests {
overflow-y: auto;
}
h1 {
text-align: center;
font-size: 3rem;
margin: 1rem;
text-transform: uppercase;
}
h2 {
margin: 0.25em 0;
}
.options {
display: flex;
justify-content: space-evenly;
align-items: center;
gap: 2em;
& button {
border: none;
background: none;
display: flex;
flex-direction: column;
opacity: 0.5;
transition: opacity 0.2s ease-in-out;
&.active {
opacity: 1;
}
}
& .separator {
border-left: 1px solid currentColor;
height: 3rem;
opacity: 0.8;
}
& .ti {
display: block;
font-size: 3rem;
}
}
.locale-select {
display: inline-flex;
align-items: center;
gap: 0.5rem;
align-self: flex-end;
position: absolute;
right: 1rem;
top: 1rem;
opacity: 0.5;
transition: opacity 0.2s ease-in-out;
}
.locale-select:hover {
opacity: 1;
}
.locale-select select {
background: rgba(255, 255, 255, 0.08);
color: inherit;
border: 1px solid currentColor;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.test {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 0 0.25em;
margin-bottom: 0.25em;
text-decoration: none;
color: inherit;
& .label {
display: inline-flex;
align-items: baseline;
gap: 0.25em;
}
& .label .category,
& .description {
opacity: 0.85;
}
& .description {
font-size: 0.85em;
grid-column-start: 2;
}
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
footer {
text-align: center;
opacity: 0.6;
margin-top: 1rem;
& a {
text-decoration: none;
}
}
</style>

View file

@ -9,27 +9,11 @@
let speakersEl: HTMLElement | undefined = $state();
</script>
<div class="test">
<div class="speakers" bind:this={speakersEl}>
<div class="my-2 flex items-center gap-4">
<div class="mr-4 flex gap-4 text-xl" bind:this={speakersEl}>
<Speaker src={leftUrl} left inline>{m.audio_channel_left()}</Speaker>
<Speaker src={centerUrl} center inline>{m.audio_channel_center()}</Speaker>
<Speaker src={rightUrl} right inline>{m.audio_channel_right()}</Speaker>
</div>
<CycleButton element={speakersEl} />
</div>
<style>
.test {
display: flex;
gap: 1em;
align-items: center;
margin: 0.5em 0;
}
.speakers {
display: flex;
gap: 1em;
font-size: 1.25em;
margin-right: 1em;
}
</style>

View file

@ -6,13 +6,13 @@
<article>
<h3>{m.audio_channelTests()}</h3>
<h4>{m.audio_stereo()}</h4>
<section>
<h4 class="mb-0">{m.audio_stereo()}</h4>
<section class="my-4">
<StereoTest />
</section>
<h4>{m.audio_surroundAudio()}</h4>
<section>
<ul>
<h4 class="mb-0">{m.audio_surroundAudio()}</h4>
<section class="my-4">
<ul class="m-0 inline-flex list-none gap-4 p-0">
<li><a class="button" href="channels-5.1">{m.audio_surround51()}</a></li>
<li><a class="button" href="channels-7.1">{m.audio_surround71()}</a></li>
</ul>
@ -20,22 +20,3 @@
<h3>{m.audio_phaseTest()}</h3>
<PhaseTest />
</article>
<style>
h4 {
margin-bottom: 0;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
display: inline-flex;
gap: 1em;
}
section {
margin: 1em 0;
}
</style>

View file

@ -52,36 +52,23 @@
}
</script>
<div class="test">
<div class="my-4">
<label>
{m.audio_frequency()}
<input type="number" bind:value={frequency} min="20" max="20000" disabled={playing} />Hz
<input
type="number"
bind:value={frequency}
min="20"
max="20000"
disabled={playing}
class="w-20"
/>Hz
</label>
<div class="controls">
<div class="mt-2">
<button onclick={() => start('inPhase')}>{m.audio_inPhase()}</button>
<button onclick={() => start('outOfPhase')}>{m.audio_outOfPhase()}</button>
<button class="stop" onclick={stop} disabled={!playing}>{m.audio_stop()}</button>
<button onclick={stop} disabled={!playing} class="ml-4" class:bg-red-900={!playing}
>{m.audio_stop()}</button
>
</div>
</div>
<style>
.test {
margin: 1em 0;
}
.controls {
margin-top: 0.5em;
}
.stop {
margin-left: 1em;
&:not(:disabled) {
background: darkred;
}
}
input {
width: 5em;
}
</style>

View file

@ -4,28 +4,13 @@
<h2><i class="ti ti-world"></i> {m.internet_title()}</h2>
<div class="test">
<iframe src="//openspeedtest.com/speedtest" title="OpenSpeedTest Embed"></iframe>
<div class="flex flex-grow flex-col justify-center">
<iframe
src="//openspeedtest.com/speedtest"
title="OpenSpeedTest Embed"
class="max-h-[50vh] flex-grow border-none"
></iframe>
</div>
<div class="attribution">
<div class="text-right">
Provided by <a href="https://openspeedtest.com">OpenSpeedtest.com</a>
</div>
<style>
iframe {
border: none;
flex-grow: 1;
max-height: 50vh;
}
.test {
display: flex;
flex-direction: column;
justify-content: center;
flex-grow: 1;
}
.attribution {
text-align: right;
}
</style>

View file

@ -17,44 +17,18 @@
<h2>{m.keyboard_title()}</h2>
<p>{m.keyboard_instruction()}</p>
<div class="current">
<div class="flex">
{#if key}
<span>{key}</span>
{/if}
{#if code}
<span class="code">({code})</span>
<span class="ml-4 opacity-80">({code})</span>
{/if}
</div>
<p>{m.keyboard_pressedKeys()}</p>
<ul>
<ul class="flex list-none flex-wrap gap-1 p-0">
{#each pressedKeys as key}
<li>{key}</li>
<li class="m-0 inline-block p-0">{key}</li>
{/each}
</ul>
<style>
.current {
display: flex;
}
.code {
margin-left: 1em;
opacity: 0.8;
}
ul {
list-style: none;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.2em;
}
li {
margin: 0;
padding: 0;
display: inline-block;
}
</style>

View file

@ -37,39 +37,10 @@
</script>
<h2><i class="ti ti-alarm"></i> {m.timer_title()}</h2>
<div class="display">
<div class="time">{time}</div>
<div class="fps">{displayFps} {m.timer_fps()}</div>
<div class="flex flex-grow flex-col justify-center" style="font-variant-numeric: tabular-nums;">
<div class="mb-4 text-center text-[12rem] leading-none select-none">{time}</div>
<div class="mb-4 text-center text-4xl leading-none select-none">{displayFps} {m.timer_fps()}</div>
</div>
<button onclick={restart}>{m.timer_restart()}</button>
<style>
div,
button {
text-align: center;
margin-bottom: 1rem;
user-select: none;
line-height: 1;
}
.display {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
font-variant-numeric: tabular-nums;
}
.time {
font-size: 12rem;
}
.fps {
font-size: 4rem;
}
button {
align-self: center;
font-size: 2rem;
}
</style>
<button onclick={restart} class="mb-4 self-center text-center text-2xl leading-none select-none"
>{m.timer_restart()}</button
>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import 'normalize.css/normalize.css';
import '@fontsource/atkinson-hyperlegible';
import '@fontsource/atkinson-hyperlegible/700.css';
import '@tabler/icons-webfont/tabler-icons.css';

34
tailwind.config.ts Normal file
View file

@ -0,0 +1,34 @@
import type { Config } from 'tailwindcss';
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
fontFamily: {
sans: [
'Atkinson Hyperlegible',
'B612',
'IBM Plex Sans',
'Helvetica Neue',
'Arial',
'sans-serif'
]
},
extend: {
colors: {
black: '#000000',
white: '#ffffff'
},
spacing: {
'0.25em': '0.25em',
'0.5em': '0.5em',
'0.2em': '0.2em'
},
opacity: {
66: '0.66',
85: '0.85',
8: '0.08'
}
}
},
plugins: []
} satisfies Config;

View file

@ -1,10 +1,12 @@
import { paraglideVitePlugin } from '@inlang/paraglide-js';
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import * as path from 'path';
export default defineConfig({
plugins: [
tailwindcss(),
sveltekit(),
paraglideVitePlugin({
project: './project.inlang',