From a0da079500fc462ca4d01d2dcc5a0b46f2dddec5 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Thu, 30 Apr 2026 22:27:26 +0700 Subject: [PATCH] feat: port 3 interactive geometry lessons + geom-engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add pure geom-engine module (vec, triangle, circle, ticks) with 34 vitest tests - Add 3 lessons under /hinh-hoc/: tam-giac-bang-nhau (SSS), tam-giac-dong-dang (similarity), goc-noi-tiep (inscribed angle) - Add reactive draggable Svelte action with arrow-key a11y - Add per-lesson colocated i18n + site chrome + lesson registry - Enable Hình học topic card on landing; keep Số học/Đại số as Sắp ra mắt - Codify pedagogical tick palette as Tailwind colors.pair.{1,2,3} - Add Be Vietnam Pro via @fontsource --- README.md | 17 +- jsconfig.json | 13 +- package-lock.json | 2966 +++++++++++++++++ package.json | 6 +- ...ainstorm-260430-2207-improved-port-spec.md | 311 ++ .../xia-260430-2207-port-try-gstack.md | 204 ++ src/app.css | 11 + src/lib/actions/draggable.svelte.js | 92 + src/lib/geom-engine/circle.js | 44 + src/lib/geom-engine/circle.test.js | 61 + src/lib/geom-engine/index.js | 16 + src/lib/geom-engine/ticks.js | 49 + src/lib/geom-engine/ticks.test.js | 31 + src/lib/geom-engine/triangle.js | 36 + src/lib/geom-engine/triangle.test.js | 39 + src/lib/geom-engine/vec.js | 55 + src/lib/geom-engine/vec.test.js | 103 + src/lib/i18n/index.js | 9 + src/lib/i18n/site.vi.js | 52 + src/lib/lessons/goc-noi-tiep/copy.vi.js | 19 + src/lib/lessons/registry.js | 21 + src/lib/lessons/tam-giac-bang-nhau/copy.vi.js | 19 + src/lib/lessons/tam-giac-dong-dang/copy.vi.js | 22 + src/lib/utils/svg.js | 28 + src/routes/+page.svelte | 143 +- src/routes/hinh-hoc/+page.svelte | 59 + src/routes/hinh-hoc/goc-noi-tiep/+page.svelte | 119 + .../hinh-hoc/tam-giac-bang-nhau/+page.svelte | 159 + .../hinh-hoc/tam-giac-dong-dang/+page.svelte | 145 + tailwind.config.js | 10 +- vitest.config.js | 8 + 31 files changed, 4761 insertions(+), 106 deletions(-) create mode 100644 package-lock.json create mode 100644 plans/reports/brainstorm-260430-2207-improved-port-spec.md create mode 100644 plans/reports/xia-260430-2207-port-try-gstack.md create mode 100644 src/lib/actions/draggable.svelte.js create mode 100644 src/lib/geom-engine/circle.js create mode 100644 src/lib/geom-engine/circle.test.js create mode 100644 src/lib/geom-engine/index.js create mode 100644 src/lib/geom-engine/ticks.js create mode 100644 src/lib/geom-engine/ticks.test.js create mode 100644 src/lib/geom-engine/triangle.js create mode 100644 src/lib/geom-engine/triangle.test.js create mode 100644 src/lib/geom-engine/vec.js create mode 100644 src/lib/geom-engine/vec.test.js create mode 100644 src/lib/i18n/index.js create mode 100644 src/lib/i18n/site.vi.js create mode 100644 src/lib/lessons/goc-noi-tiep/copy.vi.js create mode 100644 src/lib/lessons/registry.js create mode 100644 src/lib/lessons/tam-giac-bang-nhau/copy.vi.js create mode 100644 src/lib/lessons/tam-giac-dong-dang/copy.vi.js create mode 100644 src/lib/utils/svg.js create mode 100644 src/routes/hinh-hoc/+page.svelte create mode 100644 src/routes/hinh-hoc/goc-noi-tiep/+page.svelte create mode 100644 src/routes/hinh-hoc/tam-giac-bang-nhau/+page.svelte create mode 100644 src/routes/hinh-hoc/tam-giac-dong-dang/+page.svelte create mode 100644 vitest.config.js diff --git a/README.md b/README.md index 60e02d1..ac6e13d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,11 @@ Toán tương tác cho học sinh THCS Việt Nam (lớp 6-9). Số học, Đạ ## Status -Scaffold only (v0.0.1.0). Landing page liệt kê 3 chủ đề với "Sắp ra mắt". Modules tương tác sẽ ra mắt từng phần. +3 bài hình học đầu tiên đã ra mắt. Số học và Đại số vẫn "Sắp ra mắt". + +- Lớp 7 — Tam giác bằng nhau (SSS): `/hinh-hoc/tam-giac-bang-nhau/` +- Lớp 8 — Tam giác đồng dạng: `/hinh-hoc/tam-giac-dong-dang/` +- Lớp 9 — Góc nội tiếp: `/hinh-hoc/goc-noi-tiep/` ## Develop @@ -13,6 +17,8 @@ Yêu cầu: Node 24+, npm 11+. ```sh npm install npm run dev # http://localhost:5173/mathmax/ +npm run test # Vitest (geom-engine unit tests) +npm run check # svelte-check + JSDoc strict npm run build # Static output → build/ npm run preview # Serve build/ ``` @@ -26,9 +32,12 @@ Live URL: https://tiennm99.github.io/mathmax/ ## Architecture - **Static**: SvelteKit + `@sveltejs/adapter-static`, `paths.base = '/mathmax'`, output `build/`. -- **Styling**: Tailwind 3 (PostCSS), system font stack (Be Vietnam Pro sẽ thêm khi có module đầu tiên). -- **Language**: JavaScript only (Svelte 5, JSDoc qua `jsconfig.json`). -- **i18n**: Hiện chỉ có tiếng Việt. English thêm khi cần. +- **Styling**: Tailwind 3 (PostCSS) + Be Vietnam Pro (woff2 qua `@fontsource`). Tick palette `colors.pair.{1,2,3}` được khai báo trong `tailwind.config.js`. +- **Language**: JavaScript only (Svelte 5, JSDoc qua `jsconfig.json` với `checkJs: true`). +- **Math engine**: `src/lib/geom-engine/` — module thuần (không phụ thuộc DOM): `vec`, `triangle`, `circle`, `ticks`. Vitest unit tests đi kèm. +- **Lessons**: mỗi bài là một `+page.svelte`; copy tiếng Việt colocate trong `src/lib/lessons//copy.vi.js`. +- **Drag**: Svelte action `use:draggable` (`src/lib/actions/draggable.svelte.js`) — Pointer Events + bàn phím mũi tên cho a11y. +- **i18n**: Hiện chỉ có tiếng Việt. Site chrome ở `src/lib/i18n/site.vi.js`. English thêm sau bằng cách tạo `*.en.js` song song. ## License diff --git a/jsconfig.json b/jsconfig.json index 3e24382..4a2f626 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,14 +1,7 @@ { + "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { - "moduleResolution": "bundler", - "target": "ESNext", - "module": "ESNext", "checkJs": true, - "allowJs": true, - "paths": { - "$lib": ["src/lib"], - "$lib/*": ["src/lib/*"] - } - }, - "include": ["src/**/*", ".svelte-kit/ambient.d.ts", ".svelte-kit/types/**/$types.d.ts"] + "allowJs": true + } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bb04325 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2966 @@ +{ + "name": "mathmax", + "version": "0.0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mathmax", + "version": "0.0.1.0", + "dependencies": { + "@fontsource/be-vietnam-pro": "^5.2.0", + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/kit": "^2.5.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "svelte": "^5.0.0", + "tailwindcss": "^3.4.0", + "vite": "^5.4.0" + }, + "devDependencies": { + "prettier": "^3.3.0", + "prettier-plugin-svelte": "^3.2.0", + "svelte-check": "^4.0.0", + "vitest": "^3.2.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fontsource/be-vietnam-pro": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/be-vietnam-pro/-/be-vietnam-pro-5.2.8.tgz", + "integrity": "sha512-lqTtfpkAy0GdFH0fqwR0sW4VP+3CNGaQV6SpW2TSZaNmixZMlou+2dp5ErGidrS1MPiDDPfPbfW2i358PVFLZA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.58.0.tgz", + "integrity": "sha512-kT9GCN8yJTkCK1W+Gi/bvGooWAM7y7WXP+yd+rf6QOIjyoK1ERPrMwSufXJUNu2pMWIqruhFvmz+LbOqsEmKmA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.6.4", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz", + "integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", + "debug": "^4.3.7", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.12", + "vitefu": "^1.0.3" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0-next.96 || ^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz", + "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", + "svelte": "^5.0.0-next.96 || ^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz", + "integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz", + "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.345", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.345.tgz", + "integrity": "sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg==", + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz", + "integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz", + "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.55.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.5.tgz", + "integrity": "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.4", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.6.tgz", + "integrity": "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json index bf64c8f..4d075a2 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,12 @@ "build": "vite build", "preview": "vite preview", "check": "svelte-check --tsconfig ./jsconfig.json", + "test": "vitest run", + "test:watch": "vitest", "format": "prettier --write ." }, "dependencies": { + "@fontsource/be-vietnam-pro": "^5.2.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.5.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", @@ -24,6 +27,7 @@ "devDependencies": { "svelte-check": "^4.0.0", "prettier": "^3.3.0", - "prettier-plugin-svelte": "^3.2.0" + "prettier-plugin-svelte": "^3.2.0", + "vitest": "^3.2.0" } } diff --git a/plans/reports/brainstorm-260430-2207-improved-port-spec.md b/plans/reports/brainstorm-260430-2207-improved-port-spec.md new file mode 100644 index 0000000..648af21 --- /dev/null +++ b/plans/reports/brainstorm-260430-2207-improved-port-spec.md @@ -0,0 +1,311 @@ +# Brainstorm — Improved port spec + +- Pressure-tests: `plans/reports/xia-260430-2207-port-try-gstack.md` +- Mode: brutal-honesty advisory. No code. +- Date: 2026-04-30 + +## TL;DR — what xia got wrong + +1. **URL/IA**: xia recommended `//lop-N//`. Wrong — premature 3-level depth for 3 lessons. Use `///` with grade as metadata. +2. **"svg-utils.js" lump**: xia merged pure math + DOM mutation + Svelte action into one util. They have different lifetimes/dependencies. Split into 3. +3. **`renderTicks` / `setLine` ported as imperative**: xia kept the imperative DOM-mutation shape. In Svelte you compute positions (pure) and render via `{#each}` — never `createElementNS`. +4. **`congruentSSSAnyPerm` "for future quizzes"**: YAGNI violation. Don't add until a consumer exists. +5. **Component split (`*.svelte` + `+page.svelte`)**: premature abstraction at 3 lessons. Inline logic in the route file until a second consumer appears. +6. **`@testing-library/svelte` "optional"**: drop entirely. Engine tests are pure; component tests deferred. +7. **A11y silent**: arrow-key vertex movement and `prefers-reduced-motion` are real gaps the source already has — don't carry them over. +8. **Palette underbaked**: didn't propose a Tailwind theme extension to name the tick colors as data. + +xia got these *right*: engine port 1:1, drop `data-*` querying, drop `astro:before-swap`, defer KaTeX/WAAPI, keep JSDoc, ship Vitest, port engine tests, keep position-strict SSS for the badge. + +## 1 · Component shape + +**Decision**: lesson route owns state. Pointer wiring via `use:draggable` action. No sub-components for sides/ticks. + +**Rejected — heavy decomposition** (``, ``, ``): premature for 3 lessons. `` + attrs is shorter than wrapping it in a component. + +**Rejected — separate `*.svelte` component imported by route**: xia suggested this. At 3 lessons with no second consumer, the route file IS the component. Move out only when something else needs to embed it. + +**Adopted — flat route file**: +- `src/routes///+page.svelte` owns: + - SVG markup + - `$state` for vertices (one per draggable point) + - `$derived` for sides, lengths, angles, congruence/similarity flags + - `use:draggable={vertex}` on each draggable `` + - Tick rendering via `{#each tickPositions(p1, p2, n) as t}` + +**State granularity**: +```js +// per draggable point +let a = $state({ x: 60, y: 80 }); +let b = $state({ x: 180, y: 80 }); +// sides derived +let sides = $derived({ + ab: dist(a, b), bc: dist(b, c), ca: dist(c, a) +}); +// flag derived +let congruent = $derived(congruentSSS(t1, t2)); +``` +Mutate vertex via `a.x = newX; a.y = newY`. No `$state.raw` (we want fine-grained reactivity on x/y). + +**Action API**: +```js +// $lib/actions/draggable.svelte.js +// use:draggable={{ point, viewBox: {w,h}, projector?, padding? }} +// point: $state-bound {x,y}; mutated in place on pointer events +// projector: optional (raw)=>projected — used by inscribed-angle to snap to circle +// padding: clamp distance from viewBox edge (default 16; 0 disables) +``` + +The action handles `pointerdown/move/up/cancel`, `setPointerCapture`, `clientToSvg` math, optional projection, optional clamping. Returns Svelte action `{ destroy }` — no `AbortController` needed; Svelte cleans up. + +## 2 · Shared utilities — three layers, not one + +xia's "svg-utils.js" is wrong because it mixes concerns with different test surfaces and dependencies. + +| Concern | Location | Pure? | Tested? | +| --- | --- | --- | --- | +| Tick positions math | `$lib/geom-engine/ticks.js` | yes | unit | +| Client→SVG coord conversion | `$lib/utils/svg.js` | needs DOMRect | not (trivial) | +| Pointer wiring + capture | `$lib/actions/draggable.svelte.js` | side-effectful | manual | + +**Pure math shape**: +```js +// $lib/geom-engine/ticks.js +// tickPositions(p1, p2, count, opts?) → Array<{x1,y1,x2,y2}> +// Replaces source's `renderTicks`. Pure. Easy to test. +``` +**Drop entirely**: `setLine` (replaced by template binding ``), `createElementNS` (replaced by `{#each}`). + +**Reject** `` Svelte component — pure overkill for one ``. + +## 3 · i18n shape — hybrid + +**Adopted**: site chrome global, lesson copy colocated, central index re-exports. + +``` +src/lib/i18n/ + site.vi.js // {site: {title, tagline, description}, hub: {...}, status: {...}} + index.js // exports t() — merges site + per-lesson via lesson registry + +src/lib/lessons/ + registry.js // { 'tam-giac-bang-nhau': { topic, grade, copy, component } } + tam-giac-bang-nhau/ + copy.vi.js // { vi: { title, intro, instruction, ... } } + tam-giac-dong-dang/ + copy.vi.js + goc-noi-tiep/ + copy.vi.js +``` + +**Rejected — single mega-`vi`**: xia tolerated this; brutal: it works at 3 lessons, dies at 10+. Translator pain (one giant file) is a hypothetical we'll never hit before adding a second locale. + +**Rejected — separate i18n folder for every lesson**: too far. Each lesson exports its own copy module; central `t()` reads from registry. One indirection. + +**Drop**: `type Locale = typeof vi`. Use plain object with string keys. When `en.js` exists, parallel-shape it manually (a missing key falls back to the slug, not a type error). + +## 4 · URL / IA — `///` + +**Adopted**: +- `/` — landing with 3 topic cards (current mathmax IA, just enable Hình học) +- `/hinh-hoc/` — topic page listing all geometry lessons, sortable/filterable by grade (UI tab, not URL) +- `/hinh-hoc//` — lesson page (e.g. `/hinh-hoc/tam-giac-bang-nhau/`) +- Grade lives in the lesson's `copy.vi.js` and registry metadata; rendered as a badge on cards/headers. + +**Rejected — `//lop-N//` (xia's pick)**: 3-level depth, premature. Twelve possible intermediate pages, only 3 lessons. URLs longer with no SEO benefit. + +**Rejected — flat `/lop-N/-/`**: ugly compound slug. Mixes axes. + +**Rejected — pure topic-only** without grade visible: students DO think "I'm in lớp 7." Surface grade as a card badge + filter; just don't put it in the URL. + +**Vietnamese slugs**: keep them. `tam-giac-bang-nhau` is more readable than `sss-congruence` for the audience. Slugs are stable. + +## 5 · KaTeX, WAAPI, Vitest + +| Tool | v1 decision | Trigger to add | +| --- | --- | --- | +| Vitest | **include** | engine has tests already; mechanical port | +| KaTeX | **defer** | first lesson needing `\frac`, `\sqrt`, `\sum`, or matrix | +| WAAPI | **defer** | first multi-step animated proof or transitional state | +| `@fontsource/be-vietnam-pro` | **include** | mathmax already plans Vietnamese typography | +| `@testing-library/svelte` | **drop** | engine tests are DOM-free | +| `prettier-plugin-svelte` | **already in mathmax** | keep | +| `eslint` | **drop** | mathmax doesn't have it; YAGNI | +| `fast-check` (property tests) | **defer** | try-gstack noted "to be added with first canvas module" — same here, defer | + +Add `npm test` script wiring Vitest. CI integration: out of scope for the port; the existing GitHub Pages action stays unchanged. + +## 6 · Engine API — port-as-is, nothing more + +**Adopted — exact 1:1 port** of: +- `vec.js`: vec, add, sub, scale, dot, len, dist, normalize, approxEqualLen, EPSILON_LEN, EPSILON_ANGLE_DEG +- `triangle.js`: triangle, sides, congruentSSS (position-strict) +- `circle.js`: circle, projectToCircle, pointOnCircle, angleAtVertex +- `ticks.js`: NEW — tickPositions (extracted from `renderTicks` mutation, returns positions) + +**Rejected — `congruentSSSAnyPerm`** (xia suggested): YAGNI. No consumer in v1. Position-strict matches the colored-tick correspondence visible to the student. + +**Rejected — `similarSAS`/`similarSSS`**: similarity v1 is a fixed scaler `k` slider; no comparison API needed. Add when free-drag similarity lesson lands. + +**Rejected — adding `Triangle.area`, `centroid`, `circumcenter` etc.** for "completeness": YAGNI. + +JSDoc `@typedef` for `Vec2`, `Triangle`, `Circle`, `SideLengths`. Use `@param`/`@returns` everywhere. `// @ts-check` not needed if `jsconfig.json` has `checkJs` (SvelteKit default true). + +## 7 · JSDoc vs TS — JSDoc + +**Adopted — JS + JSDoc throughout**. README declares "JS only"; honor it. svelte-check + JSDoc covers types in editor and CI. No `tsconfig.json`. No `.ts` files. + +**Rejected — TS for engine only**: language inconsistency for marginal benefit. The engine is 130 LOC; JSDoc carries it. + +## 8 · Palette — codify ticks as Tailwind theme + +**Adopted**: +```js +// tailwind.config.js — extend +theme: { + extend: { + colors: { + pair: { + 1: '#D7263D', // Side AB ↔ A'B' — red + 2: '#1B998B', // Side BC ↔ B'C' — teal + 3: '#F46036', // Side CA ↔ C'A' — orange + }, + brand: { + DEFAULT: 'theme(colors.indigo.600)', // chrome links/accents + }, + success: 'theme(colors.emerald.600)', // congruence badge + }, + }, +} +``` + +Lesson SVGs reference `class="stroke-pair-1"` / `text-pair-2`. Self-documenting. Brand chrome stays `text-indigo-600`. Badge uses `bg-emerald-50 text-emerald-700`. + +## 9 · Additional gaps xia missed + +### Accessibility + +- **Arrow-key vertex movement**: source has `tabindex="0"` on M but no key handler. Add `keydown` handler in `draggable` action: arrow keys move ±2 px/step (or grid-snapped); shift+arrow = ±10. Each lesson page has a one-line instruction "Dùng phím mũi tên hoặc kéo điểm" beside the canvas. +- **ARIA**: each `` draggable point needs `role="slider"`, `aria-label="Đỉnh A — kéo hoặc dùng phím mũi tên"`, `aria-valuenow`/`aria-valuemin`/`aria-valuemax` for the canvas viewBox. Readouts (lengths, angles) need `aria-live="polite"` so screen readers announce updates. +- **Reduced motion**: any future fade/transition gated on `@media (prefers-reduced-motion: no-preference)`. +- **Focus ring**: `focus-visible` outline on draggable circles. + +### Mobile + +- `touch-action: none` on the SVG (source has it; preserve). +- Pointer Events unify mouse/touch/pen (already chosen). +- Min vertex hit target: 14px radius (source uses 6 — too small for touch). Bump to 12-14 for v1. + +### SEO/meta + +- Per-lesson `` with ``, `<meta name="description">`, OG tags — extracted from copy module. +- `og:image` deferred (no image generator yet). +- Canonical URL via `$page.url`. + +## 10 · Improved decision matrix (replaces xia §7) + +| Decision | xia recommended | **Improved** | Why | +| --- | --- | --- | --- | +| Component shape | `*.svelte` + route | inline in route file | YAGNI; no second consumer | +| Shared utils | one `svg-utils.js` | 3-way split (engine/utils/action) | different concerns, lifetimes | +| `renderTicks` shape | port as imperative | rewrite as `tickPositions` (pure) + `{#each}` | no `createElementNS` in Svelte | +| i18n shape | colocate per-lesson | hybrid: site global + lesson colocated + registry | translator + encapsulation balance | +| URL | `/<topic>/lop-N/<slug>/` | `/<topic>/<slug>/` | premature depth | +| Grade in URL | yes | no — metadata + filter | not a navigation axis | +| Slug language | Vietnamese | Vietnamese | audience-readable | +| Engine API | strict + add perm-invariant | strict only | YAGNI | +| Type system | JSDoc | JSDoc | honor README | +| Vitest | include | include | tests exist | +| `@testing-library/svelte` | optional | drop | DOM-free engine | +| KaTeX/WAAPI | defer | defer | no v1 trigger | +| Palette | "reconcile" (vague) | Tailwind theme extension `pair-{1,2,3}` | semantic naming | +| A11y (arrow keys, ARIA) | not mentioned | required for v1 | source gap; we can fix it during port | +| Mobile hit target | 6px (source) | 12-14px | touch ergonomics | +| Animation | none v1 | none v1, gate future on `prefers-reduced-motion` | KISS + a11y | + +## 11 · Final file plan (replaces xia §9) + +**Create — engine** + +``` +src/lib/geom-engine/ + vec.js + vec.test.js + triangle.js + triangle.test.js + circle.js + circle.test.js + ticks.js + ticks.test.js + index.js # barrel +``` + +**Create — actions, utils, i18n** + +``` +src/lib/actions/draggable.svelte.js +src/lib/utils/svg.js # clientToSvg +src/lib/i18n/site.vi.js +src/lib/i18n/index.js +``` + +**Create — lessons (each colocated)** + +``` +src/lib/lessons/ + registry.js + tam-giac-bang-nhau/copy.vi.js + tam-giac-dong-dang/copy.vi.js + goc-noi-tiep/copy.vi.js +``` + +**Create — routes** + +``` +src/routes/hinh-hoc/+page.svelte # topic page (lesson list) +src/routes/hinh-hoc/tam-giac-bang-nhau/+page.svelte +src/routes/hinh-hoc/tam-giac-dong-dang/+page.svelte +src/routes/hinh-hoc/goc-noi-tiep/+page.svelte +``` + +**Create — config** + +``` +vitest.config.js +``` + +**Modify** + +- `package.json` — add `@fontsource/be-vietnam-pro`, `vitest`. Scripts: `test`, `test:watch`. +- `src/routes/+page.svelte` — Hình học card → enabled, links to `/hinh-hoc/`. Số học / Đại số stay "Sắp ra mắt". +- `src/routes/+layout.svelte` — add `<svelte:head>` with default meta + lang, font import. +- `src/app.css` — Be Vietnam Pro family, body class. +- `tailwind.config.js` — pair colors, brand alias, success token, font family. +- `README.md` — lessons section + run `npm test`. + +## 12 · Risk score (revised) + +| Risk | xia | Improved spec | +| --- | --- | --- | +| Engine port | LOW | LOW | +| Lesson rewrite | MEDIUM | LOW-MEDIUM (less abstraction = less to break) | +| i18n re-shape | LOW | LOW | +| URL/IA | MEDIUM | LOW (simpler IA, less reshuffling later) | +| Decisions deferred (KaTeX/WAAPI) | LOW | LOW | +| A11y additions (NEW) | — | LOW (action handles it once, lessons inherit) | +| Mobile hit-target adjustment | — | LOW | + +Overall: **LOW-MEDIUM**, down from MEDIUM. + +## 13 · Open questions (for /ck:plan) + +1. Topic page `/hinh-hoc/`: ship with grade-filter tabs in v1, or just a sorted list? Recommend list-only v1. +2. Be Vietnam Pro weights: source uses 400/500/700. Confirm same for mathmax to match design intent? +3. Landing card for "Số học" / "Đại số" stays disabled with "Sắp ra mắt"? (assumed yes) +4. Do we want `next/prev lesson` navigation footer, or bare? Source has hard-coded teaser strings; recommend deferring to a registry-driven nav after lesson 4. +5. Apache-2.0 LICENSE on mathmax vs unlicensed try-gstack — confirm we're keeping Apache-2.0 (per memory, that's the default). + +--- + +**Status:** DONE +**Summary:** Improved spec ready for `/ck:plan`. 8 deviations from xia: simpler URL, 3-way utility split, pure tick math (not imperative), no premature engine extras, no `@testing-library/svelte`, route-inline lesson logic, palette as Tailwind theme, a11y baked in. +**Concerns:** Open question #4 (next/prev navigation) is a small UX call but affects each lesson's footer. Default to bare; revisit at lesson 4. diff --git a/plans/reports/xia-260430-2207-port-try-gstack.md b/plans/reports/xia-260430-2207-port-try-gstack.md new file mode 100644 index 0000000..5bd9acd --- /dev/null +++ b/plans/reports/xia-260430-2207-port-try-gstack.md @@ -0,0 +1,204 @@ +# Xia Report — Port try-gstack → mathmax + +- Source: `/config/workspace/tiennm99/try-gstack` (commit: workdir, v0.0.4.0) +- Target: `/config/workspace/tiennm99/mathmax` (workdir, v0.0.1.0) +- Mode: `--port` (rewrite idiomatically). Stop at report; brainstorm next. +- Date: 2026-04-30 + +## TL;DR + +try-gstack ships 3 working SGK lessons + a tested geom-engine on Astro/TS. mathmax is a SvelteKit/JS scaffold. Stack mismatch is moderate (SSG → SSG, Vite under both). Port the engine 1:1 (pure module), rewrite each lesson as a Svelte component, replace the imperative `setupX(selector)` + `data-*` querying pattern with reactive `$state`/`$effect`. Drop the `astro:before-swap` teardown hook entirely (SvelteKit handles unmount). Several design smells worth fixing before porting — see Challenge. + +## 1 · Source manifest + +| File | LOC | Role | +| --- | --- | --- | +| `src/geom-engine/vec.ts` | 48 | Pure 2D vector math — Vec2, add/sub/scale/dot/len/dist/normalize, ε constants | +| `src/geom-engine/triangle.ts` | 41 | Triangle + sides + position-strict SSS congruence | +| `src/geom-engine/circle.ts` | 40 | Circle, projectToCircle, pointOnCircle, angleAtVertex | +| `src/geom-engine/*.test.ts` | ~250 | Vitest unit suites for vec/triangle/circle | +| `src/components/congruence-sss.ts` | 213 | Lop-7: 6 draggable vertices, 2 triangles, ticks, congruence badge | +| `src/components/similarity-scale.ts` | 209 | Lop-8: 1 fixed △, 1 scaled △ via `k` slider, ratio readouts | +| `src/components/inscribed-angle.ts` | 92 | Lop-9: M draggable on circle, inscribed vs central angle | +| `src/i18n/vi.ts` | 102 | Single `vi` const with site/hub/grade/moduleN copy | +| `src/i18n/index.ts` | 11 | `t()` returns `locales[defaultLocale]`. No locale switching. | +| `src/pages/index.astro` | 67 | Landing — 3 grade cards | +| `src/pages/lop-{7,8,9}/*.astro` | ~90 each | Lesson pages, inline SVG, `<script>` calls `setupX('#canvas')` | +| `src/layouts/BaseLayout.astro` | 36 | HTML head, OG meta, lang=vi | + +Stack: Astro 5 SSG, base `/try-gstack/`, Be Vietnam Pro (woff2 subset, deferred until added), Tailwind 3, Vitest. No KaTeX yet (planned, not shipped). No animation lib yet. + +## 2 · Target state + +mathmax has: SvelteKit 2 + Svelte 5 (runes), JS only (`jsconfig.json`), Tailwind 3, `adapter-static`, base `/mathmax`. Single landing route with 3 disabled topic cards. No engine, no lessons, no i18n, no math rendering, no test runner. + +## 3 · Dependency matrix + +| Source piece | Local equivalent | Decision | +| --- | --- | --- | +| `geom-engine/*.ts` | NEW | Port to `src/lib/geom-engine/*.js` + JSDoc types | +| `geom-engine/*.test.ts` | NEW | Add `vitest` + `@testing-library/svelte` (not strictly needed for engine), keep tests near impl | +| `components/*.ts` (imperative DOM) | NEW | **Rewrite** as `*.svelte` reactive components — do not transplant the `data-*` querying pattern | +| `pages/lop-X/*.astro` | NEW | New routes `src/routes/lop-X/<slug>/+page.svelte` | +| `pages/index.astro` | EXISTS | Replace 3 disabled cards with 3 enabled lesson cards (or keep topic→lesson nesting — see Challenge Q5) | +| `layouts/BaseLayout.astro` | EXISTS (`+layout.svelte`) | Extend current `+layout.svelte` with head meta, lang, OG tags | +| `i18n/vi.ts` + `t()` | NEW | Port to `src/lib/i18n/vi.js` (frozen object) + `t()`. Keep schema. | +| `astro:before-swap` teardown | N/A | DELETE — SvelteKit unmount + `$effect` cleanup replaces it | +| Pointer Events + `setPointerCapture` | NEW | Same browser API; move into Svelte action `use:draggable` | +| Be Vietnam Pro fontsource | NEW | Add `@fontsource/be-vietnam-pro` | +| Tailwind 3 config | EXISTS | Extend with font family + Vietnamese-friendly defaults | +| KaTeX | NEW (planned) | Add `katex` only when first formula appears (deferred) | +| Vitest config | NEW | Add `vitest` + `vitest.config.js` (engine-only smoke) | + +## 4 · Stack-translation notes + +| Astro / TS pattern | SvelteKit / Svelte 5 / JS pattern | +| --- | --- | +| `.astro` file with `---` frontmatter + HTML + `<script>` | `+page.svelte` with `<script>` (runes) + markup | +| `import '~/x'` alias | SvelteKit `$lib/x` (or vite alias if needed) | +| TS `interface` / `type` | JSDoc `@typedef` blocks | +| Imperative `setupX(svgSelector)` querying `data-*` | Svelte component owns the SVG; vertex coords via `$state`; computed via `$derived`; side effects via `$effect` | +| `document.querySelector` for readouts/badges | Bind directly to component state — no DOM lookup | +| `AbortController` + `astro:before-swap` listener teardown | `$effect` returns cleanup fn; SvelteKit handles route changes | +| Astro `import.meta.env.BASE_URL` | SvelteKit `import { base } from '$app/paths'` | +| `<BaseLayout title=… description=…>` | `<svelte:head>` per page; layout owns shell | +| Module-level constants (e.g. `INITIAL`, `VIEW_W`) | Same — keep at top of `<script>` | + +## 5 · Source anatomy — design smells + +The user flagged "these features are not good." Confirmed: the components carry pre-port debt that we should fix before, not after, porting. + +1. **DOM-querying imperative API.** Every component does `document.querySelector('[data-readout-…]')` to find side-table cells the page template authored. The component depends on the page's HTML structure via string selectors. Brittle, hard to reuse, untestable without jsdom + a real page. Svelte fixes this by making the component own its readouts. +2. **Duplicated tick rendering.** `renderTicks` is copy-pasted in `congruence-sss.ts` and `similarity-scale.ts` (lines 60–85 vs 65–89, byte-identical). Should live in the engine or a shared SVG util. +3. **Duplicated `setLine` and `clientToSvg`.** Same story — three copies across components. +4. **Hand-rolled SVG element creation via `createElementNS`.** Imperative DOM building inside what should be declarative geometry. +5. **No labeled-axis abstraction.** Each lesson re-derives label offsets with magic numbers (`offsetX = id === 'b' ? 12 : id === 'c' ? -4 : -16`). Should be data on the vertex, not control flow. +6. **Position-strict SSS only.** `congruentSSS` requires AB↔A'B' correspondence — pedagogy choice, but the hub copy says "kéo các đỉnh để các cặp cạnh có cùng độ dài" without telling the student which pair maps to which. Either add the perm-invariant variant or make the correspondence visually obvious (we already have color ticks — confirm wording matches). +7. **Single-locale i18n with the wrong shape.** `module1/2/3` instead of keying by lesson slug. Adding lesson 4 means renaming everything. Re-key by slug (`'lop-7-tam-giac-bang-nhau'`) before porting. +8. **No KaTeX yet, but page copy already uses Unicode `∠`, `△`, prime marks.** Fine for now, but inconsistent with the locked decision in README. Either commit to KaTeX from lesson #1 or formally drop the decision. +9. **`type Locale = typeof vi`** assumes any future locale will match `vi` exactly — including marketing copy strings. This is rigid; English will likely diverge. Use string keys with a runtime lookup instead. +10. **Three "live" lessons + one landing — no progression scaffolding.** No "next lesson" graph beyond a hard-coded `nextTeaser` string per page. Manageable now; tech debt at 10 lessons. + +## 6 · Challenge questions (5+) + +### Q1. Should the lesson UI live as a single Svelte component, or stay split across page-template HTML and a "controller" module? + +- Source: page authors SVG markup + readout cells; controller module wires `data-*` selectors. +- Local: Svelte 5 lets the component own SVG markup, state, and DOM events together — `$state` for vertex coords, `$derived` for sides/angles, `$effect` for pointer wiring, regular `{#each}` for ticks. +- Risk if wrong: copying the imperative pattern into Svelte gives us all the DOM-fragility downsides without any of the structure benefits. +- **Recommendation:** consolidate. One component per lesson, no `data-*` selectors. + +### Q2. Should ticks/setLine/clientToSvg/draggable be utilities or live inline? + +- Source: each component re-implements them. +- Local: a single `$lib/geom-engine/svg-utils.js` (or `$lib/components/draggable.svelte.js` for the action) covers all three lessons. +- Risk: if we don't extract, we triple the surface area to fix when KaTeX/animation lib lands. +- **Recommendation:** extract — but keep utilities pure and small (no Svelte deps). + +### Q3. Should we keep the `t()` "marketing strings + lesson body" mega-object, or split copy from chrome? + +- Source: one nested object, breaks if a key is missing. +- Local options: + - (a) keep one frozen `vi.js` exported as `t()` — minimum churn + - (b) per-lesson copy module colocated with each component (`lessons/lop-7/sss/copy.vi.js`) +- Risk of (a): every new lesson grows one global file; rename pain at lesson 5+. +- Risk of (b): mild duplication (status labels, theorem/example header), but bounded. +- **Recommendation:** (b) — colocate. Site-level chrome (header/footer/site title/status) stays global. + +### Q4. Should we ship KaTeX in lesson 1, defer to a "first formula" trigger, or stay Unicode-only? + +- Source: planned but unshipped. Page copy uses `∠AOB = 120°`, `△ABC = △A′B′C′`, `AB/A′B′`. +- Local: bundle adds ~280KB if SSR-rendered + 60KB CSS. For 3 short lessons with no real LaTeX, that's overkill. +- **Recommendation:** start Unicode-only. Add KaTeX only when first lesson actually needs `\frac`, sums, or radicals. Do not pre-bundle. + +### Q5. Should the mathmax landing become a "grade picker" (try-gstack pattern) or keep "topic picker" (current)? + +- Source: 3 grade cards, geometry-only. +- Local: 3 topic cards (Số học / Đại số / Hình học), 4 grades (6, 7, 8, 9) — broader scope. +- Risk: porting blindly forces mathmax to mimic try-gstack's narrower IA. +- **Recommendation:** keep mathmax's topic IA. Lessons live at `/<topic>/<grade>/<slug>` (`/hinh-hoc/lop-7/tam-giac-bang-nhau`). Landing keeps the 3 topic cards but enables Hình học (since SSS is shipping). + +### Q6. Should `congruentSSS` stay position-strict, or also expose a permutation-invariant check? + +- Source: strict only — AB↔A'B', BC↔B'C', CA↔C'A'. +- Local pedagogy: SGK lesson labels vertices, so strict is correct for the badge. But student intuition says "two triangles are equal if their three sides match in any order." +- **Recommendation:** keep strict for the badge (matches the tick coloring); add a `congruentSSSAnyPerm` variant in engine for future quizzes. + +### Q7. Vitest now or only when complexity demands it? + +- Source: 3 test files for the engine, no component tests. +- Local: engine port = ~20 functions, all pure. Tests are cheap insurance and the source already has them — port them. +- **Recommendation:** add Vitest with engine tests on day 1. Component tests deferred. + +### Q8. Animations — Web Animations API as locked, or none for v1? + +- Source: locked on WAAPI but unshipped. +- Local: SSS/similarity/inscribed don't *need* animation — drag = animation. Adding a "snap to congruent" tween or a "central-angle wedge fades when M near A or B" is polish, not core. +- **Recommendation:** ship without animation. Wire WAAPI when a lesson needs it (e.g. step-by-step proof animation). + +## 7 · Decision matrix + +| Decision | Source's way | Recommended for mathmax | +| --- | --- | --- | +| Lesson component shape | imperative `setup()` + `data-*` | self-contained `.svelte` component | +| Shared SVG utils | duplicated inline | `$lib/geom-engine/svg-utils.js` + `$lib/actions/draggable.svelte.js` | +| i18n shape | one big `vi` object | site-level global + per-lesson colocated copy | +| Math rendering | KaTeX (planned) | Unicode for v1; KaTeX deferred | +| URL structure | `/lop-N/<slug>/` | `/<topic>/lop-N/<slug>/` (topic-first) | +| Type system | TS strict | JS + JSDoc (`@typedef`, `@param`) | +| Engine tests | Vitest, near impl | port 1:1 | +| Animation | WAAPI locked | none in v1 | +| Teardown | `astro:before-swap` + AbortController | Svelte `$effect` cleanup | +| Locale type | `typeof vi` | string-keyed lookup with `?? key` fallback | + +## 8 · Risk score + +- **Engine port:** LOW. Pure functions, ε constants, deterministic tests. +- **Lesson rewrite:** MEDIUM. Different paradigm (declarative vs imperative). Bigger refactor, but each lesson is < 200 LOC after extracting utilities. +- **i18n re-shape:** LOW (decision Q3 chosen, not yet implemented). +- **URL/IA decision:** MEDIUM. Affects landing route and 3 lesson routes simultaneously. +- **KaTeX / WAAPI deferral:** LOW (we're explicitly *not* doing them). + +Overall: MEDIUM. Buffer 1.5× for the rewrite vs straight transplant. + +## 9 · Files to create / modify (preview, not a plan) + +**Create** + +- `src/lib/geom-engine/vec.js` (+ JSDoc) +- `src/lib/geom-engine/triangle.js` +- `src/lib/geom-engine/circle.js` +- `src/lib/geom-engine/svg-utils.js` (renderTicks, setLine, clientToSvg) +- `src/lib/geom-engine/{vec,triangle,circle}.test.js` +- `src/lib/actions/draggable.svelte.js` +- `src/lib/i18n/vi.js`, `src/lib/i18n/index.js` +- `src/lib/lessons/<slug>/copy.vi.js` (× 3) +- `src/routes/hinh-hoc/lop-7/tam-giac-bang-nhau/+page.svelte` +- `src/routes/hinh-hoc/lop-8/tam-giac-dong-dang/+page.svelte` +- `src/routes/hinh-hoc/lop-9/goc-noi-tiep/+page.svelte` +- `src/lib/components/CongruenceSSS.svelte` +- `src/lib/components/SimilarityScale.svelte` +- `src/lib/components/InscribedAngle.svelte` +- `vitest.config.js` + +**Modify** + +- `package.json` (add `@fontsource/be-vietnam-pro`, `vitest`, `@testing-library/svelte` optional) +- `src/routes/+page.svelte` (enable Hình học card, keep others "Sắp ra mắt") +- `src/routes/+layout.svelte` (head/meta/lang, font import) +- `tailwind.config.js` (font family, Vietnamese hyphenation) +- `README.md` (lessons section) + +## 10 · Open questions + +1. URL slug: keep Vietnamese (`hinh-hoc/lop-7/tam-giac-bang-nhau`) or use ASCII-safe (`geometry/grade-7/sss-congruence`)? README is Vietnamese-first; recommend Vietnamese slugs. +2. Add a "next lesson" graph data structure now (small JSON), or keep hard-coded teaser strings? Defer until lesson 4. +3. Should the engine be a JSDoc'd JS module, or do we go TS for the engine only and JS for components? README says "JavaScript only" — recommend honoring that, all JS + JSDoc. +4. Permutation-invariant SSS — engine API now or wait until a quiz lesson asks for it? +5. Mathmax already has indigo branding; try-gstack uses red/teal/orange tick palette. Reconcile palette before porting visuals. + +--- + +**Status:** DONE +**Summary:** Extraction + adaptation report ready. 3 lessons + engine + i18n are portable; imperative DOM + duplicated utilities + rigid `t()` shape are the things to fix during the port, not after. Decision matrix and 8 challenge questions ready for `/ck:brainstorm`. +**Concerns:** Source has copy-pasted utility code across 3 components; porting 1:1 would propagate the duplication into Svelte. Bake the consolidation into the port. diff --git a/src/app.css b/src/app.css index 7a43db1..268ad9b 100644 --- a/src/app.css +++ b/src/app.css @@ -1,3 +1,7 @@ +@import '@fontsource/be-vietnam-pro/400.css'; +@import '@fontsource/be-vietnam-pro/500.css'; +@import '@fontsource/be-vietnam-pro/700.css'; + @tailwind base; @tailwind components; @tailwind utilities; @@ -11,5 +15,12 @@ html { -webkit-text-size-adjust: 100%; + font-family: 'Be Vietnam Pro', system-ui, sans-serif; + } + + /* Visible focus ring for keyboard users on draggable SVG vertices. */ + svg [role='slider']:focus-visible { + stroke: #4f46e5; + stroke-width: 3; } } diff --git a/src/lib/actions/draggable.svelte.js b/src/lib/actions/draggable.svelte.js new file mode 100644 index 0000000..1d60c53 --- /dev/null +++ b/src/lib/actions/draggable.svelte.js @@ -0,0 +1,92 @@ +import { clientToSvg, clampToViewBox } from '$lib/utils/svg.js'; + +/** + * @typedef {{x: number, y: number}} MutablePoint + * @typedef {{ + * point: MutablePoint; + * svg: SVGSVGElement | (() => SVGSVGElement | null); + * viewBox: { w: number; h: number }; + * projector?: (p: import('$lib/geom-engine/vec.js').Vec2) => import('$lib/geom-engine/vec.js').Vec2; + * pad?: number; + * keyStep?: number; + * keyShiftStep?: number; + * onChange?: () => void; + * }} DraggableParams + */ + +/** + * Svelte action: makes the host element draggable inside an SVG viewBox. + * Mutates `params.point.x/y` so reactivity propagates through `$state`. + * Also wires arrow-key movement for accessibility. + * + * @param {SVGGraphicsElement} node + * @param {DraggableParams} params + */ +export function draggable(node, params) { + let active = -1; // pointerId, -1 = idle + let current = params; + + const getSvg = () => (typeof current.svg === 'function' ? current.svg() : current.svg); + + const apply = (/** @type {{x:number,y:number}} */ raw) => { + const projected = current.projector ? current.projector(raw) : raw; + const clamped = current.pad === 0 ? projected : clampToViewBox(projected, current.viewBox.w, current.viewBox.h, current.pad); + current.point.x = clamped.x; + current.point.y = clamped.y; + current.onChange?.(); + }; + + const onPointerDown = (/** @type {PointerEvent} */ e) => { + active = e.pointerId; + node.setPointerCapture(e.pointerId); + e.preventDefault(); + }; + + const onPointerMove = (/** @type {PointerEvent} */ e) => { + if (active !== e.pointerId) return; + const svg = getSvg(); + if (!svg) return; + apply(clientToSvg(svg, e.clientX, e.clientY, current.viewBox.w, current.viewBox.h)); + }; + + const onPointerEnd = (/** @type {PointerEvent} */ e) => { + if (active !== e.pointerId) return; + if (node.hasPointerCapture(e.pointerId)) node.releasePointerCapture(e.pointerId); + active = -1; + }; + + const onKeyDown = (/** @type {KeyboardEvent} */ e) => { + const step = e.shiftKey ? (current.keyShiftStep ?? 10) : (current.keyStep ?? 2); + let dx = 0; + let dy = 0; + switch (e.key) { + case 'ArrowLeft': dx = -step; break; + case 'ArrowRight': dx = step; break; + case 'ArrowUp': dy = -step; break; + case 'ArrowDown': dy = step; break; + default: return; + } + e.preventDefault(); + apply({ x: current.point.x + dx, y: current.point.y + dy }); + }; + + node.addEventListener('pointerdown', onPointerDown); + node.addEventListener('pointermove', onPointerMove); + node.addEventListener('pointerup', onPointerEnd); + node.addEventListener('pointercancel', onPointerEnd); + node.addEventListener('keydown', onKeyDown); + + return { + /** @param {DraggableParams} next */ + update(next) { + current = next; + }, + destroy() { + node.removeEventListener('pointerdown', onPointerDown); + node.removeEventListener('pointermove', onPointerMove); + node.removeEventListener('pointerup', onPointerEnd); + node.removeEventListener('pointercancel', onPointerEnd); + node.removeEventListener('keydown', onKeyDown); + }, + }; +} diff --git a/src/lib/geom-engine/circle.js b/src/lib/geom-engine/circle.js new file mode 100644 index 0000000..1749c87 --- /dev/null +++ b/src/lib/geom-engine/circle.js @@ -0,0 +1,44 @@ +import { add, dot, len, normalize, scale, sub, vec } from './vec.js'; + +/** + * @typedef {import('./vec.js').Vec2} Vec2 + * @typedef {Readonly<{center: Vec2, radius: number}>} Circle + */ + +/** @param {number} cx @param {number} cy @param {number} r @returns {Circle} */ +export function circle(cx, cy, r) { + return { center: vec(cx, cy), radius: r }; +} + +/** @param {Vec2} point @param {Circle} c @returns {Vec2} */ +export function projectToCircle(point, c) { + const dir = sub(point, c.center); + const d = len(dir); + if (d === 0) { + // Point at center; pick +x direction by convention. + return add(c.center, vec(c.radius, 0)); + } + return add(c.center, scale(normalize(dir), c.radius)); +} + +/** @param {Circle} c @param {number} angleDeg @returns {Vec2} */ +export function pointOnCircle(c, angleDeg) { + const rad = (angleDeg * Math.PI) / 180; + return vec(c.center.x + c.radius * Math.cos(rad), c.center.y + c.radius * Math.sin(rad)); +} + +/** + * Unsigned angle at `vertex` between rays to `a` and `b`, in degrees [0,180]. + * Returns 0 if vertex coincides with a or b. + * @param {Vec2} a @param {Vec2} vertex @param {Vec2} b + */ +export function angleAtVertex(a, vertex, b) { + const va = sub(a, vertex); + const vb = sub(b, vertex); + const lenA = len(va); + const lenB = len(vb); + if (lenA === 0 || lenB === 0) return 0; + // Clamp guards float drift outside [-1, 1] which would NaN the acos. + const cosTheta = Math.max(-1, Math.min(1, dot(va, vb) / (lenA * lenB))); + return (Math.acos(cosTheta) * 180) / Math.PI; +} diff --git a/src/lib/geom-engine/circle.test.js b/src/lib/geom-engine/circle.test.js new file mode 100644 index 0000000..9192d59 --- /dev/null +++ b/src/lib/geom-engine/circle.test.js @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; +import { vec } from './vec.js'; +import { angleAtVertex, circle, pointOnCircle, projectToCircle } from './circle.js'; + +describe('pointOnCircle', () => { + it('places 0° at +x', () => { + const p = pointOnCircle(circle(0, 0, 10), 0); + expect(p.x).toBeCloseTo(10); + expect(p.y).toBeCloseTo(0); + }); + + it('places 90° at +y', () => { + const p = pointOnCircle(circle(0, 0, 10), 90); + expect(p.x).toBeCloseTo(0); + expect(p.y).toBeCloseTo(10); + }); +}); + +describe('projectToCircle', () => { + it('keeps direction, sets magnitude to radius', () => { + const c = circle(0, 0, 5); + const p = projectToCircle(vec(3, 4), c); + expect(p.x).toBeCloseTo(3); + expect(p.y).toBeCloseTo(4); + }); + + it('falls back to +x when point is at center', () => { + const c = circle(0, 0, 5); + expect(projectToCircle(vec(0, 0), c)).toEqual(vec(5, 0)); + }); + + it('projects far points back to the circle', () => { + const c = circle(0, 0, 5); + const p = projectToCircle(vec(100, 0), c); + expect(p).toEqual(vec(5, 0)); + }); +}); + +describe('angleAtVertex', () => { + it('right angle is 90°', () => { + expect(angleAtVertex(vec(1, 0), vec(0, 0), vec(0, 1))).toBeCloseTo(90); + }); + + it('straight angle is 180°', () => { + expect(angleAtVertex(vec(-1, 0), vec(0, 0), vec(1, 0))).toBeCloseTo(180); + }); + + it('returns 0 when vertex coincides with one ray endpoint', () => { + expect(angleAtVertex(vec(0, 0), vec(0, 0), vec(1, 1))).toBe(0); + }); + + it('inscribed angle theorem — half the central angle', () => { + const c = circle(0, 0, 10); + const a = pointOnCircle(c, 0); + const b = pointOnCircle(c, 120); + const m = pointOnCircle(c, 250); // any point on the major arc + const central = angleAtVertex(a, c.center, b); + const inscribed = angleAtVertex(a, m, b); + expect(inscribed).toBeCloseTo(central / 2, 1); + }); +}); diff --git a/src/lib/geom-engine/index.js b/src/lib/geom-engine/index.js new file mode 100644 index 0000000..c6dff1e --- /dev/null +++ b/src/lib/geom-engine/index.js @@ -0,0 +1,16 @@ +export { + EPSILON_LEN, + EPSILON_ANGLE_DEG, + vec, + add, + sub, + scale, + dot, + len, + dist, + normalize, + approxEqualLen, +} from './vec.js'; +export { triangle, sides, congruentSSS } from './triangle.js'; +export { circle, projectToCircle, pointOnCircle, angleAtVertex } from './circle.js'; +export { tickPositions } from './ticks.js'; diff --git a/src/lib/geom-engine/ticks.js b/src/lib/geom-engine/ticks.js new file mode 100644 index 0000000..e82044f --- /dev/null +++ b/src/lib/geom-engine/ticks.js @@ -0,0 +1,49 @@ +/** + * @typedef {import('./vec.js').Vec2} Vec2 + * @typedef {Readonly<{x1: number, y1: number, x2: number, y2: number}>} TickSegment + */ + +const DEFAULT_LEN = 6; +const DEFAULT_SPACING = 5; + +/** + * Compute tick-mark segments perpendicular to side p1→p2, centered on the + * midpoint. `count` ticks indicate side identity (1/2/3 = side group). + * Returns [] for degenerate sides (length < 1). + * + * @param {Vec2} p1 @param {Vec2} p2 + * @param {1|2|3} count + * @param {{length?: number, spacing?: number}} [opts] + * @returns {TickSegment[]} + */ +export function tickPositions(p1, p2, count, opts = {}) { + const length = opts.length ?? DEFAULT_LEN; + const spacing = opts.spacing ?? DEFAULT_SPACING; + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + const len = Math.hypot(dx, dy); + if (len < 1) return []; + + const dirX = dx / len; + const dirY = dy / len; + const perpX = -dirY; + const perpY = dirX; + const midX = (p1.x + p2.x) / 2; + const midY = (p1.y + p2.y) / 2; + const start = -((count - 1) * spacing) / 2; + + /** @type {TickSegment[]} */ + const out = []; + for (let i = 0; i < count; i++) { + const offset = start + i * spacing; + const cx = midX + dirX * offset; + const cy = midY + dirY * offset; + out.push({ + x1: cx + perpX * length, + y1: cy + perpY * length, + x2: cx - perpX * length, + y2: cy - perpY * length, + }); + } + return out; +} diff --git a/src/lib/geom-engine/ticks.test.js b/src/lib/geom-engine/ticks.test.js new file mode 100644 index 0000000..160adee --- /dev/null +++ b/src/lib/geom-engine/ticks.test.js @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { vec } from './vec.js'; +import { tickPositions } from './ticks.js'; + +describe('tickPositions', () => { + it('returns N segments for count=N', () => { + const out = tickPositions(vec(0, 0), vec(100, 0), 3); + expect(out).toHaveLength(3); + }); + + it('returns [] for degenerate sides', () => { + expect(tickPositions(vec(5, 5), vec(5, 5), 2)).toEqual([]); + expect(tickPositions(vec(5, 5), vec(5.5, 5), 2)).toEqual([]); + }); + + it('places single tick at midpoint, perpendicular', () => { + const [t] = tickPositions(vec(0, 0), vec(100, 0), 1, { length: 10, spacing: 5 }); + // Midpoint is (50,0); perpendicular is ±y of length 10. + expect((t.x1 + t.x2) / 2).toBeCloseTo(50); + expect((t.y1 + t.y2) / 2).toBeCloseTo(0); + expect(Math.abs(t.y1 - t.y2)).toBeCloseTo(20); + }); + + it('spaces multiple ticks evenly along the side direction', () => { + const ticks = tickPositions(vec(0, 0), vec(100, 0), 2, { length: 10, spacing: 5 }); + const mids = ticks.map((t) => (t.x1 + t.x2) / 2); + // Two ticks centered on midpoint 50, spacing 5 → 47.5 and 52.5. + expect(mids[0]).toBeCloseTo(47.5); + expect(mids[1]).toBeCloseTo(52.5); + }); +}); diff --git a/src/lib/geom-engine/triangle.js b/src/lib/geom-engine/triangle.js new file mode 100644 index 0000000..55f46b5 --- /dev/null +++ b/src/lib/geom-engine/triangle.js @@ -0,0 +1,36 @@ +import { dist, EPSILON_LEN } from './vec.js'; + +/** + * @typedef {import('./vec.js').Vec2} Vec2 + * @typedef {Readonly<{a: Vec2, b: Vec2, c: Vec2}>} Triangle + * @typedef {Readonly<{ab: number, bc: number, ca: number}>} SideLengths + */ + +/** @param {Vec2} a @param {Vec2} b @param {Vec2} c @returns {Triangle} */ +export function triangle(a, b, c) { + return { a, b, c }; +} + +/** @param {Triangle} t @returns {SideLengths} */ +export function sides(t) { + return { + ab: dist(t.a, t.b), + bc: dist(t.b, t.c), + ca: dist(t.c, t.a), + }; +} + +/** + * Position-strict SSS: corresponding sides must match (AB↔A'B', BC↔B'C', + * CA↔C'A'). Matches the colored-tick correspondence shown to the student. + * @param {Triangle} t1 @param {Triangle} t2 @param {number} [eps] + */ +export function congruentSSS(t1, t2, eps = EPSILON_LEN) { + const s1 = sides(t1); + const s2 = sides(t2); + return ( + Math.abs(s1.ab - s2.ab) < eps && + Math.abs(s1.bc - s2.bc) < eps && + Math.abs(s1.ca - s2.ca) < eps + ); +} diff --git a/src/lib/geom-engine/triangle.test.js b/src/lib/geom-engine/triangle.test.js new file mode 100644 index 0000000..4a650d1 --- /dev/null +++ b/src/lib/geom-engine/triangle.test.js @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { vec } from './vec.js'; +import { congruentSSS, sides, triangle } from './triangle.js'; + +describe('sides', () => { + it('measures all three side lengths', () => { + const t = triangle(vec(0, 0), vec(3, 0), vec(0, 4)); + const s = sides(t); + expect(s.ab).toBe(3); + expect(s.bc).toBe(5); + expect(s.ca).toBe(4); + }); +}); + +describe('congruentSSS', () => { + it('detects identical triangles', () => { + const t = triangle(vec(0, 0), vec(3, 0), vec(0, 4)); + expect(congruentSSS(t, t)).toBe(true); + }); + + it('detects translated triangles as congruent', () => { + const t1 = triangle(vec(0, 0), vec(3, 0), vec(0, 4)); + const t2 = triangle(vec(10, 10), vec(13, 10), vec(10, 14)); + expect(congruentSSS(t1, t2)).toBe(true); + }); + + it('rejects scaled triangles', () => { + const t1 = triangle(vec(0, 0), vec(3, 0), vec(0, 4)); + const t2 = triangle(vec(0, 0), vec(6, 0), vec(0, 8)); + expect(congruentSSS(t1, t2)).toBe(false); + }); + + it('is position-strict (different vertex correspondence fails)', () => { + const t1 = triangle(vec(0, 0), vec(3, 0), vec(0, 4)); + // Same shape, but vertex labels rotated. + const t2 = triangle(vec(3, 0), vec(0, 4), vec(0, 0)); + expect(congruentSSS(t1, t2)).toBe(false); + }); +}); diff --git a/src/lib/geom-engine/vec.js b/src/lib/geom-engine/vec.js new file mode 100644 index 0000000..a4106e9 --- /dev/null +++ b/src/lib/geom-engine/vec.js @@ -0,0 +1,55 @@ +/** + * @typedef {Readonly<{x: number, y: number}>} Vec2 + */ + +export const EPSILON_LEN = 0.5; +export const EPSILON_ANGLE_DEG = 0.5; + +/** @param {number} x @param {number} y @returns {Vec2} */ +export function vec(x, y) { + return { x, y }; +} + +/** @param {Vec2} a @param {Vec2} b @returns {Vec2} */ +export function add(a, b) { + return { x: a.x + b.x, y: a.y + b.y }; +} + +/** @param {Vec2} a @param {Vec2} b @returns {Vec2} */ +export function sub(a, b) { + return { x: a.x - b.x, y: a.y - b.y }; +} + +/** @param {Vec2} a @param {number} k @returns {Vec2} */ +export function scale(a, k) { + // `+ 0` normalizes IEEE-754 -0 → +0 so === / Object.is comparisons don't + // see a signed-zero ghost when k=0. + return { x: a.x * k + 0, y: a.y * k + 0 }; +} + +/** @param {Vec2} a @param {Vec2} b */ +export function dot(a, b) { + return a.x * b.x + a.y * b.y; +} + +/** @param {Vec2} a */ +export function len(a) { + return Math.hypot(a.x, a.y); +} + +/** @param {Vec2} a @param {Vec2} b */ +export function dist(a, b) { + return Math.hypot(a.x - b.x, a.y - b.y); +} + +/** @param {Vec2} a @returns {Vec2} */ +export function normalize(a) { + const l = len(a); + if (l === 0) return { x: 0, y: 0 }; + return { x: a.x / l, y: a.y / l }; +} + +/** @param {number} a @param {number} b @param {number} [eps] */ +export function approxEqualLen(a, b, eps = EPSILON_LEN) { + return Math.abs(a - b) < eps; +} diff --git a/src/lib/geom-engine/vec.test.js b/src/lib/geom-engine/vec.test.js new file mode 100644 index 0000000..b276cb7 --- /dev/null +++ b/src/lib/geom-engine/vec.test.js @@ -0,0 +1,103 @@ +import { describe, expect, it } from 'vitest'; +import { + add, + approxEqualLen, + dist, + dot, + EPSILON_LEN, + len, + normalize, + scale, + sub, + vec, +} from './vec.js'; + +describe('add', () => { + it('is commutative', () => { + const a = vec(2, 3); + const b = vec(-1, 4); + expect(add(a, b)).toEqual(add(b, a)); + }); + + it('does not mutate inputs', () => { + const a = vec(1, 2); + const b = vec(3, 4); + const before = { ...a }; + add(a, b); + expect(a).toEqual(before); + }); +}); + +describe('sub', () => { + it('is the inverse of add', () => { + const a = vec(5, 7); + const b = vec(2, 1); + expect(sub(add(a, b), b)).toEqual(a); + }); +}); + +describe('scale', () => { + it('multiplies both components by k', () => { + expect(scale(vec(2, 3), 4)).toEqual(vec(8, 12)); + }); + + it('handles k = 0 without signed-zero', () => { + expect(scale(vec(7, -3), 0)).toEqual(vec(0, 0)); + expect(Object.is(scale(vec(7, -3), 0).y, 0)).toBe(true); + }); +}); + +describe('dot', () => { + it('computes the standard inner product', () => { + expect(dot(vec(1, 2), vec(3, 4))).toBe(11); + }); + + it('returns 0 for orthogonal vectors', () => { + expect(dot(vec(1, 0), vec(0, 1))).toBe(0); + }); +}); + +describe('len and dist', () => { + it('len of (3,4) is 5', () => { + expect(len(vec(3, 4))).toBe(5); + }); + + it('dist is symmetric', () => { + const a = vec(1, 2); + const b = vec(4, 6); + expect(dist(a, b)).toBeCloseTo(dist(b, a)); + }); + + it('dist of a point with itself is 0', () => { + expect(dist(vec(7, -3), vec(7, -3))).toBe(0); + }); +}); + +describe('normalize', () => { + it('returns a unit vector for non-zero input', () => { + const n = normalize(vec(3, 4)); + expect(len(n)).toBeCloseTo(1, 10); + }); + + it('returns the zero vector for the zero vector', () => { + expect(normalize(vec(0, 0))).toEqual(vec(0, 0)); + }); + + it('preserves direction', () => { + expect(normalize(vec(2, 0))).toEqual(vec(1, 0)); + }); +}); + +describe('approxEqualLen', () => { + it('treats values within epsilon as equal', () => { + expect(approxEqualLen(10, 10 + EPSILON_LEN / 2)).toBe(true); + }); + + it('rejects values outside epsilon', () => { + expect(approxEqualLen(10, 10 + EPSILON_LEN * 2)).toBe(false); + }); + + it('uses 0.5 as the default tolerance', () => { + expect(EPSILON_LEN).toBe(0.5); + }); +}); diff --git a/src/lib/i18n/index.js b/src/lib/i18n/index.js new file mode 100644 index 0000000..b24533b --- /dev/null +++ b/src/lib/i18n/index.js @@ -0,0 +1,9 @@ +import * as siteCopy from './site.vi.js'; + +const locales = { vi: siteCopy }; +const defaultLocale = 'vi'; + +/** Site-wide chrome copy for the default locale. */ +export function t() { + return locales[defaultLocale]; +} diff --git a/src/lib/i18n/site.vi.js b/src/lib/i18n/site.vi.js new file mode 100644 index 0000000..30fb9b4 --- /dev/null +++ b/src/lib/i18n/site.vi.js @@ -0,0 +1,52 @@ +export const site = { + title: 'MathMax', + tagline: 'Toán tương tác cho học sinh THCS', + description: + 'MathMax giúp học sinh THCS lớp 6-9 học toán qua tương tác — Số học, Đại số, Hình học bằng cách kéo, thử nghiệm, và minh hoạ trực quan.', +}; + +export const hub = { + scopeLabel: 'Phạm vi', + topicsTitle: 'Chủ đề', +}; + +export const status = { + comingSoon: 'Sắp ra mắt', + live: 'Khám phá', +}; + +export const grades = { + 'lop-6': 'Lớp 6', + 'lop-7': 'Lớp 7', + 'lop-8': 'Lớp 8', + 'lop-9': 'Lớp 9', +}; + +export const topics = { + 'so-hoc': { + title: 'Số học', + blurb: 'Phép tính, ước-bội, phân số, số nguyên. Trực quan hoá thuật toán và quy luật.', + status: 'comingSoon', + href: null, + }, + 'dai-so': { + title: 'Đại số', + blurb: 'Biểu thức, phương trình, hàm số. Thao tác kéo-thả các đối tượng đại số.', + status: 'comingSoon', + href: null, + }, + 'hinh-hoc': { + title: 'Hình học', + blurb: 'Tam giác, tứ giác, đường tròn. Kéo điểm, định lý sống động.', + status: 'live', + href: '/hinh-hoc/', + }, +}; + +export const lessonChrome = { + backToTopic: '← Về danh sách bài', + backToHub: '← Về trang chủ', + theoremTitle: 'Định lý', + exampleTitle: 'Ví dụ', + instructionAria: 'Dùng phím mũi tên hoặc kéo điểm bằng chuột/cảm ứng', +}; diff --git a/src/lib/lessons/goc-noi-tiep/copy.vi.js b/src/lib/lessons/goc-noi-tiep/copy.vi.js new file mode 100644 index 0000000..a740f9b --- /dev/null +++ b/src/lib/lessons/goc-noi-tiep/copy.vi.js @@ -0,0 +1,19 @@ +export const vi = { + slug: 'goc-noi-tiep', + topic: 'hinh-hoc', + grade: 'lop-9', + title: 'Góc nội tiếp', + gradeLabel: 'Lớp 9', + intro: + 'Định lý góc nội tiếp nói rằng: khi M chạy trên cùng một cung của đường tròn, góc nội tiếp ∠AMB không đổi và bằng nửa góc ở tâm cùng chắn cung. Hãy kéo điểm M trên đường tròn để tự kiểm chứng.', + instruction: 'Kéo điểm M (đỏ) quanh đường tròn — hoặc dùng phím mũi tên', + inscribedLabel: 'Góc nội tiếp ∠AMB', + centralLabel: 'Góc ở tâm ∠AOB', + theoremTitle: 'Định lý', + theoremStatement: + 'Trong một đường tròn, số đo của góc nội tiếp bằng nửa số đo của góc ở tâm cùng chắn một cung.', + exampleTitle: 'Ví dụ', + exampleBody: + 'Cho đường tròn (O) với hai điểm A, B cố định sao cho góc ở tâm ∠AOB = 120°. Theo định lý, mọi điểm M nằm trên cung lớn AB đều cho ∠AMB = 60° (= 120° / 2). Khi M chuyển sang cung nhỏ, ∠AMB = 120° vì khi đó góc nội tiếp chắn cung lớn (240°), một nửa của nó là 120°. Hãy kéo điểm M để kiểm tra.', + nextTeaser: 'Sắp ra mắt: Tứ giác nội tiếp', +}; diff --git a/src/lib/lessons/registry.js b/src/lib/lessons/registry.js new file mode 100644 index 0000000..968fe03 --- /dev/null +++ b/src/lib/lessons/registry.js @@ -0,0 +1,21 @@ +import { vi as sssCopy } from './tam-giac-bang-nhau/copy.vi.js'; +import { vi as similarityCopy } from './tam-giac-dong-dang/copy.vi.js'; +import { vi as inscribedCopy } from './goc-noi-tiep/copy.vi.js'; + +/** + * @typedef {{slug: string, topic: string, grade: string, title: string, + * gradeLabel: string, intro: string, [k: string]: any}} LessonCopy + */ + +/** @type {LessonCopy[]} */ +export const lessons = [sssCopy, similarityCopy, inscribedCopy]; + +/** @param {string} topic */ +export function lessonsByTopic(topic) { + return lessons.filter((l) => l.topic === topic); +} + +/** @param {string} slug */ +export function lessonBySlug(slug) { + return lessons.find((l) => l.slug === slug); +} diff --git a/src/lib/lessons/tam-giac-bang-nhau/copy.vi.js b/src/lib/lessons/tam-giac-bang-nhau/copy.vi.js new file mode 100644 index 0000000..9b3f675 --- /dev/null +++ b/src/lib/lessons/tam-giac-bang-nhau/copy.vi.js @@ -0,0 +1,19 @@ +export const vi = { + slug: 'tam-giac-bang-nhau', + topic: 'hinh-hoc', + grade: 'lop-7', + title: 'Tam giác bằng nhau (SSS)', + gradeLabel: 'Lớp 7', + intro: + 'Hai tam giác bằng nhau khi cả ba cặp cạnh tương ứng có độ dài bằng nhau (trường hợp Cạnh – Cạnh – Cạnh). Hãy kéo từng đỉnh để xem khi nào hai tam giác trùng khớp.', + instruction: 'Kéo bất kỳ đỉnh nào của hai tam giác — hoặc dùng phím mũi tên', + congruentBadge: 'Hai tam giác bằng nhau (c.c.c)', + lengthsTitle: 'Độ dài cạnh', + theoremTitle: 'Định lý (cạnh – cạnh – cạnh)', + theoremStatement: + 'Nếu ba cạnh của tam giác này lần lượt bằng ba cạnh của tam giác kia thì hai tam giác đó bằng nhau.', + exampleTitle: 'Ví dụ', + exampleBody: + 'Cho hai tam giác △ABC và △A′B′C′ với AB = A′B′ (cùng có một dấu gạch đỏ), BC = B′C′ (cùng có hai dấu gạch xanh), CA = C′A′ (cùng có ba dấu gạch cam). Theo trường hợp c.c.c, △ABC = △A′B′C′ — và do đó các góc tương ứng cũng bằng nhau. Hãy kéo các đỉnh để các cặp cạnh có cùng độ dài, huy hiệu sẽ bật lên.', + nextTeaser: 'Sắp ra mắt: SAS / ASA / cạnh huyền – góc nhọn / cạnh huyền – cạnh góc vuông', +}; diff --git a/src/lib/lessons/tam-giac-dong-dang/copy.vi.js b/src/lib/lessons/tam-giac-dong-dang/copy.vi.js new file mode 100644 index 0000000..7f852ab --- /dev/null +++ b/src/lib/lessons/tam-giac-dong-dang/copy.vi.js @@ -0,0 +1,22 @@ +export const vi = { + slug: 'tam-giac-dong-dang', + topic: 'hinh-hoc', + grade: 'lop-8', + title: 'Tam giác đồng dạng', + gradeLabel: 'Lớp 8', + intro: + 'Hai tam giác đồng dạng có các góc tương ứng bằng nhau và các cạnh tương ứng tỉ lệ. Hãy kéo thanh trượt phóng/thu △A′B′C′ — tỉ số AB/A′B′ luôn bằng BC/B′C′ và CA/C′A′, dù tam giác lớn hay nhỏ. Các góc thì không đổi.', + instruction: 'Kéo thanh trượt để phóng to hoặc thu nhỏ △A′B′C′', + kLabel: 'Hệ số phóng', + sidesTitle: 'Cạnh tương ứng', + ratioTitle: 'Tỉ số AB/A′B′ = BC/B′C′ = CA/C′A′', + anglesNote: + 'Khi △A′B′C′ phóng/thu theo hệ số k, các cạnh nhân với k nhưng các góc tại A, B, C không thay đổi — đó chính là định nghĩa của hai tam giác đồng dạng.', + theoremTitle: 'Định nghĩa', + theoremStatement: + 'Hai tam giác gọi là đồng dạng khi các góc tương ứng bằng nhau và các cạnh tương ứng tỉ lệ. Tỉ số đó được gọi là tỉ số đồng dạng k.', + exampleTitle: 'Ví dụ', + exampleBody: + 'Ở hệ số k = 2, tam giác △A′B′C′ to gấp đôi △ABC: mỗi cạnh A′B′ = 2 · AB. Khi đó tỉ số AB/A′B′ = 1/2 = 0,50, đúng bằng BC/B′C′ và CA/C′A′. Khi k = 0,5, tam giác A′B′C′ nhỏ bằng nửa: tỉ số AB/A′B′ = 2,00. Hãy kéo thanh trượt để xác nhận.', + nextTeaser: 'Sắp ra mắt: kéo từng đỉnh tự do (AA / SAS / SSS đồng dạng)', +}; diff --git a/src/lib/utils/svg.js b/src/lib/utils/svg.js new file mode 100644 index 0000000..0b930fe --- /dev/null +++ b/src/lib/utils/svg.js @@ -0,0 +1,28 @@ +import { vec } from '$lib/geom-engine/vec.js'; + +/** + * Convert client (mouse/touch) coordinates to the SVG's viewBox space. + * @param {SVGSVGElement} svg + * @param {number} clientX + * @param {number} clientY + * @param {number} viewW + * @param {number} viewH + * @returns {import('$lib/geom-engine/vec.js').Vec2} + */ +export function clientToSvg(svg, clientX, clientY, viewW, viewH) { + const r = svg.getBoundingClientRect(); + return vec(((clientX - r.left) / r.width) * viewW, ((clientY - r.top) / r.height) * viewH); +} + +/** + * Clamp a point inside the viewBox with optional padding from edges. + * @param {import('$lib/geom-engine/vec.js').Vec2} v + * @param {number} viewW @param {number} viewH @param {number} [pad] + * @returns {import('$lib/geom-engine/vec.js').Vec2} + */ +export function clampToViewBox(v, viewW, viewH, pad = 16) { + return vec( + Math.max(pad, Math.min(viewW - pad, v.x)), + Math.max(pad, Math.min(viewH - pad, v.y)), + ); +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 9ce4d52..a85a503 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,125 +1,88 @@ +<script> + import { base } from '$app/paths'; + import { t } from '$lib/i18n/index.js'; + + const copy = t(); + /** @type {Array<keyof typeof copy.topics>} */ + const topicOrder = ['so-hoc', 'dai-so', 'hinh-hoc']; + const topics = topicOrder.map((key) => ({ key, ...copy.topics[key] })); +</script> + <svelte:head> <title>MathMax — Toán cho học sinh THCS - + -
MathMax -
-
-

MathMax

+

{copy.site.title}

- Toán tương tác cho học sinh THCS — lớp 6 đến lớp 9. Khám phá Số học, Đại số, Hình học qua các hoạt động kéo-thả, thử nghiệm, và minh hoạ trực quan. + {copy.site.description}

-
- Phạm vi + {copy.hub.scopeLabel}
    -
  • - Lớp 6 -
  • -
  • - Lớp 7 -
  • -
  • - Lớp 8 -
  • -
  • - Lớp 9 -
  • + {#each Object.values(copy.grades) as label} +
  • {label}
  • + {/each}
-
-

Chủ đề

+

{copy.hub.topicsTitle}

    - - -
  • -
    -

    Số học

    - - Sắp ra mắt - -
    -

    - Phép tính, ước-bội, phân số, số nguyên. Trực quan hoá thuật toán và quy luật. -

    -
  • - - -
  • -
    -

    Đại số

    - - Sắp ra mắt - -
    -

    - Biểu thức, phương trình, hàm số. Thao tác kéo-thả các đối tượng đại số. -

    -
  • - - -
  • -
    -

    Hình học

    - - Sắp ra mắt - -
    -

    - Tam giác, tứ giác, đường tròn. Kéo điểm, định lý sống động. -

    -
  • - + {#each topics as topic (topic.key)} + {@const isLive = topic.status === 'live' && topic.href} +
  • + {#if isLive} + +
    +

    {topic.title}

    + + {copy.status[/** @type {keyof typeof copy.status} */ (topic.status)]} + +
    +

    {topic.blurb}

    +
    + {:else} +
    +
    +

    {topic.title}

    + + {copy.status[/** @type {keyof typeof copy.status} */ (topic.status)]} + +
    +

    {topic.blurb}

    +
    + {/if} +
  • + {/each}
- diff --git a/src/routes/hinh-hoc/+page.svelte b/src/routes/hinh-hoc/+page.svelte new file mode 100644 index 0000000..202af5a --- /dev/null +++ b/src/routes/hinh-hoc/+page.svelte @@ -0,0 +1,59 @@ + + + + {topic.title} — {copy.site.title} + + + +
+
+ MathMax + +
+
+ +
+
+ + +
+

{topic.title}

+

{topic.blurb}

+
+ + +
+
+ +
+
+ © {new Date().getFullYear()} · + Mã nguồn +
+
diff --git a/src/routes/hinh-hoc/goc-noi-tiep/+page.svelte b/src/routes/hinh-hoc/goc-noi-tiep/+page.svelte new file mode 100644 index 0000000..128c025 --- /dev/null +++ b/src/routes/hinh-hoc/goc-noi-tiep/+page.svelte @@ -0,0 +1,119 @@ + + + + {m.title} — {copy.site.title} + + + +
+
+ MathMax +
+
+ +
+
+ + +
+
{m.gradeLabel}
+

{m.title}

+

{m.intro}

+
+ +
+ {m.inscribedLabel}: {inscribed.toFixed(1)}° + {m.centralLabel}: {central.toFixed(1)}° +
+ +
+ + + + + + + + + + + O + + + A + + + B + + + M + +

{m.instruction}

+
+ +
+

{m.theoremTitle}

+

{m.theoremStatement}

+
+ +
+

{m.exampleTitle}

+

{m.exampleBody}

+
+ +
+ {m.nextTeaser} +
+
+
diff --git a/src/routes/hinh-hoc/tam-giac-bang-nhau/+page.svelte b/src/routes/hinh-hoc/tam-giac-bang-nhau/+page.svelte new file mode 100644 index 0000000..8fc845e --- /dev/null +++ b/src/routes/hinh-hoc/tam-giac-bang-nhau/+page.svelte @@ -0,0 +1,159 @@ + + + + {m.title} — {copy.site.title} + + + +
+
+ MathMax +
+
+ +
+
+ + +
+
{m.gradeLabel}
+

{m.title}

+

{m.intro}

+
+ +
+
+ {#if isCongruent} + + ✓ {m.congruentBadge} + + {/if} +
+ + + + + + + + {#each ticksAB as t (t.x1 + ',' + t.y1)} + + {/each} + {#each ticksBC as t (t.x1 + ',' + t.y1)} + + {/each} + {#each ticksCA as t (t.x1 + ',' + t.y1)} + + {/each} + + + + + + + {#each ticksApBp as t (t.x1 + ',' + t.y1)} + + {/each} + {#each ticksBpCp as t (t.x1 + ',' + t.y1)} + + {/each} + {#each ticksCpAp as t (t.x1 + ',' + t.y1)} + + {/each} + + + + A + + B + + C + + + A′ + + B′ + + C′ + +

{m.instruction}

+
+ +
+

{m.lengthsTitle}

+
+
AB: {s1.ab.toFixed(1)}
+
A′B′: {s2.ab.toFixed(1)}
+
BC: {s1.bc.toFixed(1)}
+
B′C′: {s2.bc.toFixed(1)}
+
CA: {s1.ca.toFixed(1)}
+
C′A′: {s2.ca.toFixed(1)}
+
+
+ +
+

{m.theoremTitle}

+

{m.theoremStatement}

+
+ +
+

{m.exampleTitle}

+

{m.exampleBody}

+
+ +
+ {m.nextTeaser} +
+
+
diff --git a/src/routes/hinh-hoc/tam-giac-dong-dang/+page.svelte b/src/routes/hinh-hoc/tam-giac-dong-dang/+page.svelte new file mode 100644 index 0000000..87d5c04 --- /dev/null +++ b/src/routes/hinh-hoc/tam-giac-dong-dang/+page.svelte @@ -0,0 +1,145 @@ + + + + {m.title} — {copy.site.title} + + + +
+
+ MathMax +
+
+ +
+
+ + +
+
{m.gradeLabel}
+

{m.title}

+

{m.intro}

+
+ +
+ + + + + + {#each ticks1AB as t}{/each} + {#each ticks1BC as t}{/each} + {#each ticks1CA as t}{/each} + + A + B + C + + + + + + {#each ticks2AB as t (t.x1 + ',' + t.y1)}{/each} + {#each ticks2BC as t (t.x1 + ',' + t.y1)}{/each} + {#each ticks2CA as t (t.x1 + ',' + t.y1)}{/each} + + A′ + B′ + C′ + +
+ +
+ +

{m.instruction}

+
+ +
+

{m.sidesTitle}

+
+
AB: {s1.ab.toFixed(1)}
+
A′B′: {s2.ab.toFixed(1)}
+
BC: {s1.bc.toFixed(1)}
+
B′C′: {s2.bc.toFixed(1)}
+
CA: {s1.ca.toFixed(1)}
+
C′A′: {s2.ca.toFixed(1)}
+
+

+ {m.ratioTitle} = {ratio.toFixed(2)} +

+
+ +
+

{m.theoremTitle}

+

{m.theoremStatement}

+

{m.anglesNote}

+
+ +
+

{m.exampleTitle}

+

{m.exampleBody}

+
+ + +
+
diff --git a/tailwind.config.js b/tailwind.config.js index 4068a90..07ea122 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,7 +4,15 @@ export default { theme: { extend: { fontFamily: { - sans: ['system-ui', 'sans-serif'], + sans: ['"Be Vietnam Pro"', 'system-ui', 'sans-serif'], + }, + colors: { + // Pedagogical tick palette — one color per side-correspondence pair. + pair: { + 1: '#D7263D', + 2: '#1B998B', + 3: '#F46036', + }, }, }, }, diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..4175afd --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.js'], + environment: 'node', + }, +});