From 4bc84b539999db2085f71aa14303c5a6ac1e11c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Fri, 9 Jan 2026 14:42:33 +0100 Subject: [PATCH 1/6] feat: cv2 --- package.json | 34 +- pnpm-lock.yaml | 808 ++++++++---------- src/lib/chord-editor/action-linter.ts | 196 +++++ src/lib/chord-editor/action-plugin.ts | 8 +- src/lib/chord-editor/action-serializer.ts | 281 +++++- src/lib/chord-editor/autocomplete.ts | 105 +-- src/lib/chord-editor/chord-delim-plugin.ts | 6 +- src/lib/chord-editor/chords-grammar-plugin.ts | 5 +- src/lib/chord-editor/chords.grammar | 7 +- src/lib/chord-editor/test.txt | 32 +- src/lib/components/Action.svelte | 52 +- src/lib/components/layout/ActionList.svelte | 17 +- src/lib/serial/chord.ts | 81 +- src/lib/serial/connection.ts | 5 +- src/lib/style/_kbd.scss | 20 +- src/routes/(app)/config/cv2/+page.svelte | 144 +++- src/routes/(app)/config/x2/+page.svelte | 10 + 17 files changed, 1178 insertions(+), 633 deletions(-) create mode 100644 src/lib/chord-editor/action-linter.ts create mode 100644 src/routes/(app)/config/x2/+page.svelte diff --git a/package.json b/package.json index 314ba348..6cda0bce 100644 --- a/package.json +++ b/package.json @@ -35,25 +35,27 @@ }, "devDependencies": { "@codemirror/autocomplete": "^6.20.0", + "@codemirror/collab": "^6.1.1", "@codemirror/commands": "^6.10.1", "@codemirror/lang-javascript": "^6.2.4", - "@codemirror/language": "^6.11.3", + "@codemirror/language": "^6.12.1", + "@codemirror/lint": "^6.9.2", "@codemirror/merge": "^6.11.2", - "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.39.4", + "@codemirror/state": "^6.5.3", + "@codemirror/view": "^6.39.9", "@fontsource-variable/material-symbols-rounded": "^5.2.30", "@fontsource-variable/noto-sans-mono": "^5.2.10", - "@lezer/common": "^1.4.0", + "@lezer/common": "^1.5.0", "@lezer/generator": "^1.8.0", "@lezer/highlight": "^1.2.3", - "@lezer/lr": "^1.4.5", + "@lezer/lr": "^1.4.7", "@material/material-color-utilities": "^0.3.0", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", "@modyfi/vite-plugin-yaml": "^1.1.1", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.49.2", - "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@sveltejs/kit": "^2.49.3", + "@sveltejs/vite-plugin-svelte": "^6.2.3", "@tauri-apps/api": "^1.6.0", "@tauri-apps/cli": "^1.6.0", "@types/dom-view-transitions": "^1.0.6", @@ -76,29 +78,29 @@ "matrix-js-sdk": "^37.12.0", "npm-run-all": "^4.1.5", "prettier": "^3.7.4", - "prettier-plugin-css-order": "^2.1.2", + "prettier-plugin-css-order": "^2.2.0", "prettier-plugin-svelte": "^3.4.1", "rxjs": "^7.8.2", - "sass": "^1.97.0", + "sass": "^1.97.2", "semver": "^7.7.3", - "socket.io-client": "^4.8.1", + "socket.io-client": "^4.8.3", "stylelint": "^16.26.1", "stylelint-config-html": "^1.1.0", "stylelint-config-prettier-scss": "^1.0.0", "stylelint-config-recommended-scss": "^16.0.2", "stylelint-config-standard-scss": "^16.0.0", - "svelte": "5.37.1", - "svelte-check": "^4.3.4", + "svelte": "5.46.1", + "svelte-check": "^4.3.5", "svelte-preprocess": "^6.0.3", "tippy.js": "^6.3.7", "typesafe-i18n": "^5.26.2", - "typescript": "^5.8.3", - "vite": "^7.0.6", + "typescript": "^5.9.3", + "vite": "^7.3.1", "vite-plugin-mkcert": "^1.17.9", - "vite-plugin-pwa": "^1.0.2", + "vite-plugin-pwa": "^1.2.0", "vitest": "^4.0.16", "web-serial-polyfill": "^1.0.15", - "workbox-window": "^7.3.0" + "workbox-window": "^7.4.0" }, "type": "module" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b2cdb3c..f58e936b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@codemirror/autocomplete': specifier: ^6.20.0 version: 6.20.0 + '@codemirror/collab': + specifier: ^6.1.1 + version: 6.1.1 '@codemirror/commands': specifier: ^6.10.1 version: 6.10.1 @@ -18,17 +21,20 @@ importers: specifier: ^6.2.4 version: 6.2.4 '@codemirror/language': - specifier: ^6.11.3 - version: 6.11.3 + specifier: ^6.12.1 + version: 6.12.1 + '@codemirror/lint': + specifier: ^6.9.2 + version: 6.9.2 '@codemirror/merge': specifier: ^6.11.2 version: 6.11.2 '@codemirror/state': - specifier: ^6.5.2 - version: 6.5.2 + specifier: ^6.5.3 + version: 6.5.3 '@codemirror/view': - specifier: ^6.39.4 - version: 6.39.4 + specifier: ^6.39.9 + version: 6.39.9 '@fontsource-variable/material-symbols-rounded': specifier: ^5.2.30 version: 5.2.30 @@ -36,8 +42,8 @@ importers: specifier: ^5.2.10 version: 5.2.10 '@lezer/common': - specifier: ^1.4.0 - version: 1.4.0 + specifier: ^1.5.0 + version: 1.5.0 '@lezer/generator': specifier: ^1.8.0 version: 1.8.0 @@ -45,29 +51,29 @@ importers: specifier: ^1.2.3 version: 1.2.3 '@lezer/lr': - specifier: ^1.4.5 - version: 1.4.5 + specifier: ^1.4.7 + version: 1.4.7 '@material/material-color-utilities': specifier: ^0.3.0 version: 0.3.0 '@melt-ui/pp': specifier: ^0.3.2 - version: 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.37.1))(svelte@5.37.1) + version: 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.46.1))(svelte@5.46.1) '@melt-ui/svelte': specifier: ^0.86.6 - version: 0.86.6(svelte@5.37.1) + version: 0.86.6(svelte@5.46.1) '@modyfi/vite-plugin-yaml': specifier: ^1.1.1 - version: 1.1.1(rollup@2.79.2)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)) + version: 1.1.1(rollup@2.79.2)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)) '@sveltejs/adapter-static': specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1))) + version: 3.0.10(@sveltejs/kit@2.49.3(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(sass@1.97.2)(terser@5.44.1))) '@sveltejs/kit': - specifier: ^2.49.2 - version: 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)) + specifier: ^2.49.3 + version: 2.49.3(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)) '@sveltejs/vite-plugin-svelte': - specifier: ^6.2.1 - version: 6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)) + specifier: ^6.2.3 + version: 6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)) '@tauri-apps/api': specifier: ^1.6.0 version: 1.6.0 @@ -94,7 +100,7 @@ importers: version: 2023.10.7 '@vite-pwa/sveltekit': specifier: ^1.1.0 - version: 1.1.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(vite@7.0.6(sass@1.97.0)(terser@5.44.1))(workbox-build@7.1.1)(workbox-window@7.3.0) + version: 1.1.0(@sveltejs/kit@2.49.3(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(vite@7.3.1(sass@1.97.2)(terser@5.44.1))(workbox-build@7.1.1)(workbox-window@7.4.0) autoprefixer: specifier: ^10.4.23 version: 10.4.23(postcss@8.5.6) @@ -135,81 +141,77 @@ importers: specifier: ^3.7.4 version: 3.7.4 prettier-plugin-css-order: - specifier: ^2.1.2 - version: 2.1.2(postcss@8.5.6)(prettier@3.7.4) + specifier: ^2.2.0 + version: 2.2.0(postcss@8.5.6)(prettier@3.7.4) prettier-plugin-svelte: specifier: ^3.4.1 - version: 3.4.1(prettier@3.7.4)(svelte@5.37.1) + version: 3.4.1(prettier@3.7.4)(svelte@5.46.1) rxjs: specifier: ^7.8.2 version: 7.8.2 sass: - specifier: ^1.97.0 - version: 1.97.0 + specifier: ^1.97.2 + version: 1.97.2 semver: specifier: ^7.7.3 version: 7.7.3 socket.io-client: - specifier: ^4.8.1 - version: 4.8.1 + specifier: ^4.8.3 + version: 4.8.3 stylelint: specifier: ^16.26.1 - version: 16.26.1(typescript@5.8.3) + version: 16.26.1(typescript@5.9.3) stylelint-config-html: specifier: ^1.1.0 - version: 1.1.0(postcss-html@1.7.0)(stylelint@16.26.1(typescript@5.8.3)) + version: 1.1.0(postcss-html@1.7.0)(stylelint@16.26.1(typescript@5.9.3)) stylelint-config-prettier-scss: specifier: ^1.0.0 - version: 1.0.0(stylelint@16.26.1(typescript@5.8.3)) + version: 1.0.0(stylelint@16.26.1(typescript@5.9.3)) stylelint-config-recommended-scss: specifier: ^16.0.2 - version: 16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.8.3)) + version: 16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)) stylelint-config-standard-scss: specifier: ^16.0.0 - version: 16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.8.3)) + version: 16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)) svelte: - specifier: 5.37.1 - version: 5.37.1 + specifier: 5.46.1 + version: 5.46.1 svelte-check: - specifier: ^4.3.4 - version: 4.3.4(picomatch@4.0.3)(svelte@5.37.1)(typescript@5.8.3) + specifier: ^4.3.5 + version: 4.3.5(picomatch@4.0.3)(svelte@5.46.1)(typescript@5.9.3) svelte-preprocess: specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.28.5)(postcss@8.5.6)(sass@1.97.0)(svelte@5.37.1)(typescript@5.8.3) + version: 6.0.3(@babel/core@7.28.5)(postcss@8.5.6)(sass@1.97.2)(svelte@5.46.1)(typescript@5.9.3) tippy.js: specifier: ^6.3.7 version: 6.3.7 typesafe-i18n: specifier: ^5.26.2 - version: 5.26.2(typescript@5.8.3) + version: 5.26.2(typescript@5.9.3) typescript: - specifier: ^5.8.3 - version: 5.8.3 + specifier: ^5.9.3 + version: 5.9.3 vite: - specifier: ^7.0.6 - version: 7.0.6(sass@1.97.0)(terser@5.44.1) + specifier: ^7.3.1 + version: 7.3.1(sass@1.97.2)(terser@5.44.1) vite-plugin-mkcert: specifier: ^1.17.9 - version: 1.17.9(vite@7.0.6(sass@1.97.0)(terser@5.44.1)) + version: 1.17.9(vite@7.3.1(sass@1.97.2)(terser@5.44.1)) vite-plugin-pwa: - specifier: ^1.0.2 - version: 1.0.2(vite@7.0.6(sass@1.97.0)(terser@5.44.1))(workbox-build@7.1.1)(workbox-window@7.3.0) + specifier: ^1.2.0 + version: 1.2.0(vite@7.3.1(sass@1.97.2)(terser@5.44.1))(workbox-build@7.1.1)(workbox-window@7.4.0) vitest: specifier: ^4.0.16 - version: 4.0.16(jsdom@26.1.0)(sass@1.97.0)(terser@5.44.1) + version: 4.0.16(jsdom@26.1.0)(sass@1.97.2)(terser@5.44.1) web-serial-polyfill: specifier: ^1.0.15 version: 1.0.15 workbox-window: - specifier: ^7.3.0 - version: 7.3.0 + specifier: ^7.4.0 + version: 7.4.0 packages: - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@apideck/better-ajv-errors@0.3.6': resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} engines: {node: '>=10'} @@ -739,17 +741,20 @@ packages: '@codemirror/autocomplete@6.20.0': resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} + '@codemirror/collab@6.1.1': + resolution: {integrity: sha512-tkIn9Jguh98ie12dbBuba3lE8LHUkaMrIFuCVeVGhncSczFdKmX25vC12+58+yqQW5AXi3py6jWY0W+jelyglA==} + '@codemirror/commands@6.10.1': resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==} '@codemirror/lang-javascript@6.2.4': resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} - '@codemirror/language@6.11.3': - resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} + '@codemirror/language@6.12.1': + resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==} - '@codemirror/lint@6.8.1': - resolution: {integrity: sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==} + '@codemirror/lint@6.9.2': + resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==} '@codemirror/merge@6.11.2': resolution: {integrity: sha512-NO5EJd2rLRbwVWLgMdhIntDIhfDtMOKYEZgqV5WnkNUS2oXOCVWLPjG/kgl/Jth2fGiOuG947bteqxP9nBXmMg==} @@ -757,11 +762,11 @@ packages: '@codemirror/search@6.5.6': resolution: {integrity: sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==} - '@codemirror/state@6.5.2': - resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} + '@codemirror/state@6.5.3': + resolution: {integrity: sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==} - '@codemirror/view@6.39.4': - resolution: {integrity: sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==} + '@codemirror/view@6.39.9': + resolution: {integrity: sha512-miGSIfBOKC1s2oHoa80dp+BjtsL8sXsrgGlQnQuOcfvaedcQUtqddTmKbJSDkLl4mkgPvZyXuKic2HDNYcJLYA==} '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} @@ -828,152 +833,158 @@ packages: '@dual-bundle/import-meta-resolve@4.2.1': resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} - '@esbuild/aix-ppc64@0.25.2': - resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.2': - resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.2': - resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.2': - resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.2': - resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.2': - resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.2': - resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.2': - resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.2': - resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.2': - resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.2': - resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.2': - resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.2': - resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.2': - resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.2': - resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.2': - resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.2': - resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.2': - resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.2': - resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.2': - resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.2': - resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.25.2': - resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.2': - resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.2': - resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.2': - resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1011,10 +1022,6 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} - '@jridgewell/remapping@2.3.5': resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} @@ -1022,25 +1029,15 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.11': resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -1053,8 +1050,8 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@lezer/common@1.4.0': - resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==} + '@lezer/common@1.5.0': + resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==} '@lezer/generator@1.8.0': resolution: {integrity: sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg==} @@ -1066,8 +1063,8 @@ packages: '@lezer/javascript@1.4.17': resolution: {integrity: sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==} - '@lezer/lr@1.4.5': - resolution: {integrity: sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==} + '@lezer/lr@1.4.7': + resolution: {integrity: sha512-wNIFWdSUfX9Jc6ePMzxSPVgTVB4EOfDIwLQLWASyiUdHKaMsiilj9bYiGkGQCKVodd0x6bgQCV207PILGFCF9Q==} '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} @@ -1372,18 +1369,21 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.49.2': - resolution: {integrity: sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==} + '@sveltejs/kit@2.49.3': + resolution: {integrity: sha512-luTmE2Isk9GRJnitqanLoByKBiyLdfLpV2qV9a25JMxjbQt919TVqG8pibJDkxTvX9+w2k/9IL7o+/RtG++3QA==} engines: {node: '>=18.13'} hasBin: true 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 svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 peerDependenciesMeta: '@opentelemetry/api': optional: true + typescript: + optional: true '@sveltejs/vite-plugin-svelte-inspector@5.0.0': resolution: {integrity: sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==} @@ -1393,8 +1393,8 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 - '@sveltejs/vite-plugin-svelte@6.2.1': - resolution: {integrity: sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==} + '@sveltejs/vite-plugin-svelte@6.2.3': + resolution: {integrity: sha512-a+uxqQ9j6Lxmq4plbGaNdM9hgDCZyxAv/yvuyF5iWoA2H5icZkqD3rdK155ZQgFLX2lc3NvahHG4OgKpYqYPiQ==} engines: {node: ^20.19 || ^22.12 || >=24} peerDependencies: svelte: ^5.0.0 @@ -1493,9 +1493,6 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1577,11 +1574,6 @@ packages: '@vitest/utils@4.0.16': resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -1961,8 +1953,8 @@ packages: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} - css-declaration-sorter@7.2.0: - resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} + css-declaration-sorter@7.3.1: + resolution: {integrity: sha512-gz6x+KkgNCjxq3Var03pRYLhyNfwhkKF1g/yoLgDNtFvVu0/fOLV9C8fFEZRjACp/XQLumjAYo7JVjzH3wLbxA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.0.9 @@ -2345,8 +2337,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.25.2: - resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} hasBin: true @@ -2364,8 +2356,8 @@ packages: esptool-js@0.5.7: resolution: {integrity: sha512-k3pkXU9OTySCd58OUDjuJWNnFjM+QpPWAghxyWPm3zNfaLiP4ex2jNd7Rj0jWPu3/fgvwau236tetsTZrh4x5g==} - esrap@2.1.0: - resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==} + esrap@2.2.1: + resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} estree-walker@1.0.1: resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} @@ -2434,14 +2426,6 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.4.2: - resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fdir@6.4.6: resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: @@ -3144,9 +3128,6 @@ packages: magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3390,10 +3371,6 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -3464,8 +3441,8 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - prettier-plugin-css-order@2.1.2: - resolution: {integrity: sha512-vomxPjHI6pOMYcBuouSJHxxQClJXaUpU9rsV9IAO2wrSTZILRRlrxAAR8t9UF6wtczLkLfNRFUwM+ZbGXOONUA==} + prettier-plugin-css-order@2.2.0: + resolution: {integrity: sha512-GCkwEgQ2roT7le+zpUFQThPDO4x5EXcZmY9Rj6rvO++I/nATTGBWdZdsooha/BlvIBbZclJzXsgJdlKWrys9+w==} engines: {node: '>=16'} peerDependencies: prettier: 3.x @@ -3658,8 +3635,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sass@1.97.0: - resolution: {integrity: sha512-KR0igP1z4avUJetEuIeOdDlwaUDvkH8wSx7FdSjyYBS3dpyX3TzHfAMO0G1Q4/3cdjcmi3r7idh+KCmKqS+KeQ==} + sass@1.97.2: + resolution: {integrity: sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==} engines: {node: '>=14.0.0'} hasBin: true @@ -3770,8 +3747,8 @@ packages: smob@1.5.0: resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} - socket.io-client@4.8.1: - resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} engines: {node: '>=10.0.0'} socket.io-parser@4.2.4: @@ -3964,8 +3941,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-check@4.3.4: - resolution: {integrity: sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==} + svelte-check@4.3.5: + resolution: {integrity: sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: @@ -4009,8 +3986,8 @@ packages: typescript: optional: true - svelte@5.37.1: - resolution: {integrity: sha512-h8arWpQZ+3z8eahyBT5KkiBOUsG6xvI5Ykg0ozRr9xEdImgSMUPUlOFWRNkUsT7Ti0DSUCTEbPoped0aoxFyWA==} + svelte@5.46.1: + resolution: {integrity: sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==} engines: {node: '>=18'} svg-tags@1.0.0: @@ -4055,10 +4032,6 @@ packages: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} - tinyglobby@0.2.10: - resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -4175,8 +4148,8 @@ packages: peerDependencies: typescript: '>=3.5.1' - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -4264,18 +4237,6 @@ packages: peerDependencies: vite: '>=3' - vite-plugin-pwa@1.0.2: - resolution: {integrity: sha512-O3UwjsCnoDclgJANoOgzzqW7SFgwXE/th2OmUP/ILxHKwzWxxKDBu+B/Xa9Cv4IgSVSnj2HgRVIJ7F15+vQFkA==} - engines: {node: '>=16.0.0'} - peerDependencies: - '@vite-pwa/assets-generator': ^1.0.0 - vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - workbox-build: ^7.3.0 - workbox-window: ^7.3.0 - peerDependenciesMeta: - '@vite-pwa/assets-generator': - optional: true - vite-plugin-pwa@1.2.0: resolution: {integrity: sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==} engines: {node: '>=16.0.0'} @@ -4288,8 +4249,8 @@ packages: '@vite-pwa/assets-generator': optional: true - vite@7.0.6: - resolution: {integrity: sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==} + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4455,8 +4416,8 @@ packages: workbox-core@7.1.0: resolution: {integrity: sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==} - workbox-core@7.3.0: - resolution: {integrity: sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==} + workbox-core@7.4.0: + resolution: {integrity: sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==} workbox-expiration@7.1.0: resolution: {integrity: sha512-m5DcMY+A63rJlPTbbBNtpJ20i3enkyOtSgYfv/l8h+D6YbbNiA0zKEkCUaMsdDlxggla1oOfRkyqTvl5Ni5KQQ==} @@ -4491,8 +4452,8 @@ packages: workbox-window@7.1.0: resolution: {integrity: sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==} - workbox-window@7.3.0: - resolution: {integrity: sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==} + workbox-window@7.4.0: + resolution: {integrity: sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==} wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} @@ -4559,11 +4520,6 @@ packages: snapshots: - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - '@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)': dependencies: ajv: 8.17.1 @@ -5265,64 +5221,68 @@ snapshots: '@codemirror/autocomplete@6.20.0': dependencies: - '@codemirror/language': 6.11.3 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.39.4 - '@lezer/common': 1.4.0 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.9 + '@lezer/common': 1.5.0 + + '@codemirror/collab@6.1.1': + dependencies: + '@codemirror/state': 6.5.3 '@codemirror/commands@6.10.1': dependencies: - '@codemirror/language': 6.11.3 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.39.4 - '@lezer/common': 1.4.0 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.9 + '@lezer/common': 1.5.0 '@codemirror/lang-javascript@6.2.4': dependencies: '@codemirror/autocomplete': 6.20.0 - '@codemirror/language': 6.11.3 - '@codemirror/lint': 6.8.1 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.39.4 - '@lezer/common': 1.4.0 + '@codemirror/language': 6.12.1 + '@codemirror/lint': 6.9.2 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.9 + '@lezer/common': 1.5.0 '@lezer/javascript': 1.4.17 - '@codemirror/language@6.11.3': + '@codemirror/language@6.12.1': dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.39.4 - '@lezer/common': 1.4.0 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.9 + '@lezer/common': 1.5.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.5 + '@lezer/lr': 1.4.7 style-mod: 4.1.2 - '@codemirror/lint@6.8.1': + '@codemirror/lint@6.9.2': dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.39.4 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.9 crelt: 1.0.6 '@codemirror/merge@6.11.2': dependencies: - '@codemirror/language': 6.11.3 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.39.4 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.9 '@lezer/highlight': 1.2.3 style-mod: 4.1.2 '@codemirror/search@6.5.6': dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.39.4 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.9 crelt: 1.0.6 - '@codemirror/state@6.5.2': + '@codemirror/state@6.5.3': dependencies: '@marijn/find-cluster-break': 1.0.2 - '@codemirror/view@6.39.4': + '@codemirror/view@6.39.9': dependencies: - '@codemirror/state': 6.5.2 + '@codemirror/state': 6.5.3 crelt: 1.0.6 style-mod: 4.1.2 w3c-keyname: 2.2.8 @@ -5394,79 +5354,82 @@ snapshots: '@dual-bundle/import-meta-resolve@4.2.1': {} - '@esbuild/aix-ppc64@0.25.2': + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': optional: true - '@esbuild/android-arm64@0.25.2': + '@esbuild/android-arm@0.27.2': optional: true - '@esbuild/android-arm@0.25.2': + '@esbuild/android-x64@0.27.2': optional: true - '@esbuild/android-x64@0.25.2': + '@esbuild/darwin-arm64@0.27.2': optional: true - '@esbuild/darwin-arm64@0.25.2': + '@esbuild/darwin-x64@0.27.2': optional: true - '@esbuild/darwin-x64@0.25.2': + '@esbuild/freebsd-arm64@0.27.2': optional: true - '@esbuild/freebsd-arm64@0.25.2': + '@esbuild/freebsd-x64@0.27.2': optional: true - '@esbuild/freebsd-x64@0.25.2': + '@esbuild/linux-arm64@0.27.2': optional: true - '@esbuild/linux-arm64@0.25.2': + '@esbuild/linux-arm@0.27.2': optional: true - '@esbuild/linux-arm@0.25.2': + '@esbuild/linux-ia32@0.27.2': optional: true - '@esbuild/linux-ia32@0.25.2': + '@esbuild/linux-loong64@0.27.2': optional: true - '@esbuild/linux-loong64@0.25.2': + '@esbuild/linux-mips64el@0.27.2': optional: true - '@esbuild/linux-mips64el@0.25.2': + '@esbuild/linux-ppc64@0.27.2': optional: true - '@esbuild/linux-ppc64@0.25.2': + '@esbuild/linux-riscv64@0.27.2': optional: true - '@esbuild/linux-riscv64@0.25.2': + '@esbuild/linux-s390x@0.27.2': optional: true - '@esbuild/linux-s390x@0.25.2': + '@esbuild/linux-x64@0.27.2': optional: true - '@esbuild/linux-x64@0.25.2': + '@esbuild/netbsd-arm64@0.27.2': optional: true - '@esbuild/netbsd-arm64@0.25.2': + '@esbuild/netbsd-x64@0.27.2': optional: true - '@esbuild/netbsd-x64@0.25.2': + '@esbuild/openbsd-arm64@0.27.2': optional: true - '@esbuild/openbsd-arm64@0.25.2': + '@esbuild/openbsd-x64@0.27.2': optional: true - '@esbuild/openbsd-x64@0.25.2': + '@esbuild/openharmony-arm64@0.27.2': optional: true - '@esbuild/sunos-x64@0.25.2': + '@esbuild/sunos-x64@0.27.2': optional: true - '@esbuild/win32-arm64@0.25.2': + '@esbuild/win32-arm64@0.27.2': optional: true - '@esbuild/win32-ia32@0.25.2': + '@esbuild/win32-ia32@0.27.2': optional: true - '@esbuild/win32-x64@0.25.2': + '@esbuild/win32-x64@0.27.2': optional: true '@floating-ui/core@1.6.7': @@ -5508,12 +5471,6 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/gen-mapping@0.3.5': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/remapping@2.3.5': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -5521,8 +5478,6 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - '@jridgewell/source-map@0.3.11': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -5530,15 +5485,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.4.15': {} - '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -5552,26 +5500,26 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@lezer/common@1.4.0': {} + '@lezer/common@1.5.0': {} '@lezer/generator@1.8.0': dependencies: - '@lezer/common': 1.4.0 - '@lezer/lr': 1.4.5 + '@lezer/common': 1.5.0 + '@lezer/lr': 1.4.7 '@lezer/highlight@1.2.3': dependencies: - '@lezer/common': 1.4.0 + '@lezer/common': 1.5.0 '@lezer/javascript@1.4.17': dependencies: - '@lezer/common': 1.4.0 + '@lezer/common': 1.5.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.5 + '@lezer/lr': 1.4.7 - '@lezer/lr@1.4.5': + '@lezer/lr@1.4.7': dependencies: - '@lezer/common': 1.4.0 + '@lezer/common': 1.5.0 '@marijn/find-cluster-break@1.0.2': {} @@ -5579,14 +5527,14 @@ snapshots: '@matrix-org/matrix-sdk-crypto-wasm@15.1.0': {} - '@melt-ui/pp@0.3.2(@melt-ui/svelte@0.86.6(svelte@5.37.1))(svelte@5.37.1)': + '@melt-ui/pp@0.3.2(@melt-ui/svelte@0.86.6(svelte@5.46.1))(svelte@5.46.1)': dependencies: - '@melt-ui/svelte': 0.86.6(svelte@5.37.1) + '@melt-ui/svelte': 0.86.6(svelte@5.46.1) estree-walker: 3.0.3 magic-string: 0.30.10 - svelte: 5.37.1 + svelte: 5.46.1 - '@melt-ui/svelte@0.86.6(svelte@5.37.1)': + '@melt-ui/svelte@0.86.6(svelte@5.46.1)': dependencies: '@floating-ui/core': 1.6.7 '@floating-ui/dom': 1.6.10 @@ -5594,14 +5542,14 @@ snapshots: dequal: 2.0.3 focus-trap: 7.5.4 nanoid: 5.0.7 - svelte: 5.37.1 + svelte: 5.46.1 - '@modyfi/vite-plugin-yaml@1.1.1(rollup@2.79.2)(vite@7.0.6(sass@1.97.0)(terser@5.44.1))': + '@modyfi/vite-plugin-yaml@1.1.1(rollup@2.79.2)(vite@7.3.1(sass@1.97.2)(terser@5.44.1))': dependencies: '@rollup/pluginutils': 5.1.0(rollup@2.79.2) js-yaml: 4.1.0 tosource: 2.0.0-alpha.3 - vite: 7.0.6(sass@1.97.0)(terser@5.44.1) + vite: 7.3.1(sass@1.97.2)(terser@5.44.1) transitivePeerDependencies: - rollup @@ -5809,55 +5757,53 @@ snapshots: magic-string: 0.25.9 string.prototype.matchall: 4.0.12 - '@sveltejs/acorn-typescript@1.0.5(acorn@8.12.1)': - dependencies: - acorn: 8.12.1 - '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.3(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))': dependencies: - '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)) + '@sveltejs/kit': 2.49.3(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)) - '@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1))': + '@sveltejs/kit@2.49.3(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(sass@1.97.2)(terser@5.44.1))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)) + '@sveltejs/vite-plugin-svelte': 6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 devalue: 5.6.1 esm-env: 1.2.2 kleur: 4.1.5 - magic-string: 0.30.17 + magic-string: 0.30.21 mrmime: 2.0.0 sade: 1.8.1 set-cookie-parser: 2.6.0 sirv: 3.0.0 - svelte: 5.37.1 - vite: 7.0.6(sass@1.97.0)(terser@5.44.1) + svelte: 5.46.1 + vite: 7.3.1(sass@1.97.2)(terser@5.44.1) + optionalDependencies: + typescript: 5.9.3 - '@sveltejs/vite-plugin-svelte-inspector@5.0.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.0(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)) - debug: 4.4.1(supports-color@8.1.1) - svelte: 5.37.1 - vite: 7.0.6(sass@1.97.0)(terser@5.44.1) + '@sveltejs/vite-plugin-svelte': 6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)) + debug: 4.4.3 + svelte: 5.46.1 + vite: 7.3.1(sass@1.97.2)(terser@5.44.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1))': + '@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)) - debug: 4.4.1(supports-color@8.1.1) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.0(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)) deepmerge: 4.3.1 - magic-string: 0.30.17 - svelte: 5.37.1 - vite: 7.0.6(sass@1.97.0)(terser@5.44.1) - vitefu: 1.1.1(vite@7.0.6(sass@1.97.0)(terser@5.44.1)) + magic-string: 0.30.21 + obug: 2.1.1 + svelte: 5.46.1 + vite: 7.3.1(sass@1.97.2)(terser@5.44.1) + vitefu: 1.1.1(vite@7.3.1(sass@1.97.2)(terser@5.44.1)) transitivePeerDependencies: - supports-color @@ -5928,8 +5874,6 @@ snapshots: '@types/estree@1.0.6': {} - '@types/estree@1.0.7': {} - '@types/estree@1.0.8': {} '@types/events@3.0.3': {} @@ -5964,12 +5908,12 @@ snapshots: '@types/node': 20.14.10 optional: true - '@vite-pwa/sveltekit@1.1.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(vite@7.0.6(sass@1.97.0)(terser@5.44.1))(workbox-build@7.1.1)(workbox-window@7.3.0)': + '@vite-pwa/sveltekit@1.1.0(@sveltejs/kit@2.49.3(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(vite@7.3.1(sass@1.97.2)(terser@5.44.1))(workbox-build@7.1.1)(workbox-window@7.4.0)': dependencies: - '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)))(svelte@5.37.1)(vite@7.0.6(sass@1.97.0)(terser@5.44.1)) + '@sveltejs/kit': 2.49.3(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(sass@1.97.2)(terser@5.44.1)) kolorist: 1.8.0 tinyglobby: 0.2.14 - vite-plugin-pwa: 1.2.0(vite@7.0.6(sass@1.97.0)(terser@5.44.1))(workbox-build@7.1.1)(workbox-window@7.3.0) + vite-plugin-pwa: 1.2.0(vite@7.3.1(sass@1.97.2)(terser@5.44.1))(workbox-build@7.1.1)(workbox-window@7.4.0) transitivePeerDependencies: - supports-color - vite @@ -5985,13 +5929,13 @@ snapshots: chai: 6.2.1 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.16(vite@7.0.6(sass@1.97.0)(terser@5.44.1))': + '@vitest/mocker@4.0.16(vite@7.3.1(sass@1.97.2)(terser@5.44.1))': dependencies: '@vitest/spy': 4.0.16 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.0.6(sass@1.97.0)(terser@5.44.1) + vite: 7.3.1(sass@1.97.2)(terser@5.44.1) '@vitest/pretty-format@4.0.16': dependencies: @@ -6015,8 +5959,6 @@ snapshots: '@vitest/pretty-format': 4.0.16 tinyrainbow: 3.0.3 - acorn@8.12.1: {} - acorn@8.15.0: {} agent-base@7.1.3: {} @@ -6315,11 +6257,11 @@ snapshots: dependencies: '@codemirror/autocomplete': 6.20.0 '@codemirror/commands': 6.10.1 - '@codemirror/language': 6.11.3 - '@codemirror/lint': 6.8.1 + '@codemirror/language': 6.12.1 + '@codemirror/lint': 6.9.2 '@codemirror/search': 6.5.6 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.39.4 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.9 color-convert@1.9.3: dependencies: @@ -6366,14 +6308,14 @@ snapshots: core-util-is@1.0.2: {} - cosmiconfig@9.0.0(typescript@5.8.3): + cosmiconfig@9.0.0(typescript@5.9.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.0 js-yaml: 4.1.1 parse-json: 5.2.0 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 crelt@1.0.6: {} @@ -6399,7 +6341,7 @@ snapshots: crypto-random-string@2.0.0: {} - css-declaration-sorter@7.2.0(postcss@8.5.6): + css-declaration-sorter@7.3.1(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -6945,33 +6887,34 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.25.2: + esbuild@0.27.2: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.2 - '@esbuild/android-arm': 0.25.2 - '@esbuild/android-arm64': 0.25.2 - '@esbuild/android-x64': 0.25.2 - '@esbuild/darwin-arm64': 0.25.2 - '@esbuild/darwin-x64': 0.25.2 - '@esbuild/freebsd-arm64': 0.25.2 - '@esbuild/freebsd-x64': 0.25.2 - '@esbuild/linux-arm': 0.25.2 - '@esbuild/linux-arm64': 0.25.2 - '@esbuild/linux-ia32': 0.25.2 - '@esbuild/linux-loong64': 0.25.2 - '@esbuild/linux-mips64el': 0.25.2 - '@esbuild/linux-ppc64': 0.25.2 - '@esbuild/linux-riscv64': 0.25.2 - '@esbuild/linux-s390x': 0.25.2 - '@esbuild/linux-x64': 0.25.2 - '@esbuild/netbsd-arm64': 0.25.2 - '@esbuild/netbsd-x64': 0.25.2 - '@esbuild/openbsd-arm64': 0.25.2 - '@esbuild/openbsd-x64': 0.25.2 - '@esbuild/sunos-x64': 0.25.2 - '@esbuild/win32-arm64': 0.25.2 - '@esbuild/win32-ia32': 0.25.2 - '@esbuild/win32-x64': 0.25.2 + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 escalade@3.2.0: {} @@ -6985,9 +6928,9 @@ snapshots: pako: 2.1.0 tslib: 2.6.3 - esrap@2.1.0: + esrap@2.2.1: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 estree-walker@1.0.1: {} @@ -7059,10 +7002,6 @@ snapshots: dependencies: pend: 1.2.0 - fdir@6.4.2(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - fdir@6.4.6(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -7536,7 +7475,7 @@ snapshots: is-reference@3.0.3: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 is-regex@1.1.4: dependencies: @@ -7780,10 +7719,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -8011,8 +7946,6 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.2: {} - picomatch@4.0.3: {} pidtree@0.3.1: {} @@ -8065,19 +7998,19 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier-plugin-css-order@2.1.2(postcss@8.5.6)(prettier@3.7.4): + prettier-plugin-css-order@2.2.0(postcss@8.5.6)(prettier@3.7.4): dependencies: - css-declaration-sorter: 7.2.0(postcss@8.5.6) + css-declaration-sorter: 7.3.1(postcss@8.5.6) postcss-less: 6.0.0(postcss@8.5.6) postcss-scss: 4.0.9(postcss@8.5.6) prettier: 3.7.4 transitivePeerDependencies: - postcss - prettier-plugin-svelte@3.4.1(prettier@3.7.4)(svelte@5.37.1): + prettier-plugin-svelte@3.4.1(prettier@3.7.4)(svelte@5.46.1): dependencies: prettier: 3.7.4 - svelte: 5.37.1 + svelte: 5.46.1 prettier@3.7.4: {} @@ -8289,7 +8222,7 @@ snapshots: safer-buffer@2.1.2: {} - sass@1.97.0: + sass@1.97.2: dependencies: chokidar: 4.0.1 immutable: 5.1.1 @@ -8414,10 +8347,10 @@ snapshots: smob@1.5.0: {} - socket.io-client@4.8.1: + socket.io-client@4.8.3: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.6 + debug: 4.4.3 engine.io-client: 6.6.2 socket.io-parser: 4.2.4 transitivePeerDependencies: @@ -8575,42 +8508,42 @@ snapshots: style-mod@4.1.2: {} - stylelint-config-html@1.1.0(postcss-html@1.7.0)(stylelint@16.26.1(typescript@5.8.3)): + stylelint-config-html@1.1.0(postcss-html@1.7.0)(stylelint@16.26.1(typescript@5.9.3)): dependencies: postcss-html: 1.7.0 - stylelint: 16.26.1(typescript@5.8.3) + stylelint: 16.26.1(typescript@5.9.3) - stylelint-config-prettier-scss@1.0.0(stylelint@16.26.1(typescript@5.8.3)): + stylelint-config-prettier-scss@1.0.0(stylelint@16.26.1(typescript@5.9.3)): dependencies: - stylelint: 16.26.1(typescript@5.8.3) + stylelint: 16.26.1(typescript@5.9.3) - stylelint-config-recommended-scss@16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.8.3)): + stylelint-config-recommended-scss@16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)): dependencies: postcss-scss: 4.0.9(postcss@8.5.6) - stylelint: 16.26.1(typescript@5.8.3) - stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.8.3)) - stylelint-scss: 6.12.1(stylelint@16.26.1(typescript@5.8.3)) + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3)) + stylelint-scss: 6.12.1(stylelint@16.26.1(typescript@5.9.3)) optionalDependencies: postcss: 8.5.6 - stylelint-config-recommended@17.0.0(stylelint@16.26.1(typescript@5.8.3)): + stylelint-config-recommended@17.0.0(stylelint@16.26.1(typescript@5.9.3)): dependencies: - stylelint: 16.26.1(typescript@5.8.3) + stylelint: 16.26.1(typescript@5.9.3) - stylelint-config-standard-scss@16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.8.3)): + stylelint-config-standard-scss@16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)): dependencies: - stylelint: 16.26.1(typescript@5.8.3) - stylelint-config-recommended-scss: 16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.8.3)) - stylelint-config-standard: 39.0.1(stylelint@16.26.1(typescript@5.8.3)) + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended-scss: 16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)) + stylelint-config-standard: 39.0.1(stylelint@16.26.1(typescript@5.9.3)) optionalDependencies: postcss: 8.5.6 - stylelint-config-standard@39.0.1(stylelint@16.26.1(typescript@5.8.3)): + stylelint-config-standard@39.0.1(stylelint@16.26.1(typescript@5.9.3)): dependencies: - stylelint: 16.26.1(typescript@5.8.3) - stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.8.3)) + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3)) - stylelint-scss@6.12.1(stylelint@16.26.1(typescript@5.8.3)): + stylelint-scss@6.12.1(stylelint@16.26.1(typescript@5.9.3)): dependencies: css-tree: 3.1.0 is-plain-object: 5.0.0 @@ -8620,9 +8553,9 @@ snapshots: postcss-resolve-nested-selector: 0.1.6 postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - stylelint: 16.26.1(typescript@5.8.3) + stylelint: 16.26.1(typescript@5.9.3) - stylelint@16.26.1(typescript@5.8.3): + stylelint@16.26.1(typescript@5.9.3): dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-syntax-patches-for-csstree': 1.0.21 @@ -8632,7 +8565,7 @@ snapshots: '@dual-bundle/import-meta-resolve': 4.2.1 balanced-match: 2.0.0 colord: 2.9.3 - cosmiconfig: 9.0.0(typescript@5.8.3) + cosmiconfig: 9.0.0(typescript@5.9.3) css-functions-list: 3.2.3 css-tree: 3.1.0 debug: 4.4.3 @@ -8686,42 +8619,43 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.37.1)(typescript@5.8.3): + svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.46.1)(typescript@5.9.3): dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.1 - fdir: 6.4.6(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.37.1 - typescript: 5.8.3 + svelte: 5.46.1 + typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-preprocess@6.0.3(@babel/core@7.28.5)(postcss@8.5.6)(sass@1.97.0)(svelte@5.37.1)(typescript@5.8.3): + svelte-preprocess@6.0.3(@babel/core@7.28.5)(postcss@8.5.6)(sass@1.97.2)(svelte@5.46.1)(typescript@5.9.3): dependencies: - svelte: 5.37.1 + svelte: 5.46.1 optionalDependencies: '@babel/core': 7.28.5 postcss: 8.5.6 - sass: 1.97.0 - typescript: 5.8.3 + sass: 1.97.2 + typescript: 5.9.3 - svelte@5.37.1: + svelte@5.46.1: dependencies: - '@ampproject/remapping': 2.3.0 - '@jridgewell/sourcemap-codec': 1.5.0 - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.12.1) - '@types/estree': 1.0.7 - acorn: 8.12.1 + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 + devalue: 5.6.1 esm-env: 1.2.2 - esrap: 2.1.0 + esrap: 2.2.1 is-reference: 3.0.3 locate-character: 3.0.0 - magic-string: 0.30.17 + magic-string: 0.30.21 zimmerframe: 1.1.2 svg-tags@1.0.0: {} @@ -8764,11 +8698,6 @@ snapshots: tinyexec@1.0.2: {} - tinyglobby@0.2.10: - dependencies: - fdir: 6.4.2(picomatch@4.0.2) - picomatch: 4.0.2 - tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.3) @@ -8898,11 +8827,11 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typesafe-i18n@5.26.2(typescript@5.8.3): + typesafe-i18n@5.26.2(typescript@5.9.3): dependencies: - typescript: 5.8.3 + typescript: 5.9.3 - typescript@5.8.3: {} + typescript@5.9.3: {} unbox-primitive@1.0.2: dependencies: @@ -8981,58 +8910,47 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - vite-plugin-mkcert@1.17.9(vite@7.0.6(sass@1.97.0)(terser@5.44.1)): + vite-plugin-mkcert@1.17.9(vite@7.3.1(sass@1.97.2)(terser@5.44.1)): dependencies: axios: 1.13.2(debug@4.4.3) debug: 4.4.3 picocolors: 1.1.1 - vite: 7.0.6(sass@1.97.0)(terser@5.44.1) - transitivePeerDependencies: - - supports-color - - vite-plugin-pwa@1.0.2(vite@7.0.6(sass@1.97.0)(terser@5.44.1))(workbox-build@7.1.1)(workbox-window@7.3.0): - dependencies: - debug: 4.4.0(supports-color@8.1.1) - pretty-bytes: 6.1.1 - tinyglobby: 0.2.10 - vite: 7.0.6(sass@1.97.0)(terser@5.44.1) - workbox-build: 7.1.1 - workbox-window: 7.3.0 + vite: 7.3.1(sass@1.97.2)(terser@5.44.1) transitivePeerDependencies: - supports-color - vite-plugin-pwa@1.2.0(vite@7.0.6(sass@1.97.0)(terser@5.44.1))(workbox-build@7.1.1)(workbox-window@7.3.0): + vite-plugin-pwa@1.2.0(vite@7.3.1(sass@1.97.2)(terser@5.44.1))(workbox-build@7.1.1)(workbox-window@7.4.0): dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 pretty-bytes: 6.1.1 - tinyglobby: 0.2.14 - vite: 7.0.6(sass@1.97.0)(terser@5.44.1) + tinyglobby: 0.2.15 + vite: 7.3.1(sass@1.97.2)(terser@5.44.1) workbox-build: 7.1.1 - workbox-window: 7.3.0 + workbox-window: 7.4.0 transitivePeerDependencies: - supports-color - vite@7.0.6(sass@1.97.0)(terser@5.44.1): + vite@7.3.1(sass@1.97.2)(terser@5.44.1): dependencies: - esbuild: 0.25.2 - fdir: 6.4.6(picomatch@4.0.3) + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.46.1 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 optionalDependencies: fsevents: 2.3.3 - sass: 1.97.0 + sass: 1.97.2 terser: 5.44.1 - vitefu@1.1.1(vite@7.0.6(sass@1.97.0)(terser@5.44.1)): + vitefu@1.1.1(vite@7.3.1(sass@1.97.2)(terser@5.44.1)): optionalDependencies: - vite: 7.0.6(sass@1.97.0)(terser@5.44.1) + vite: 7.3.1(sass@1.97.2)(terser@5.44.1) - vitest@4.0.16(jsdom@26.1.0)(sass@1.97.0)(terser@5.44.1): + vitest@4.0.16(jsdom@26.1.0)(sass@1.97.2)(terser@5.44.1): dependencies: '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(vite@7.0.6(sass@1.97.0)(terser@5.44.1)) + '@vitest/mocker': 4.0.16(vite@7.3.1(sass@1.97.2)(terser@5.44.1)) '@vitest/pretty-format': 4.0.16 '@vitest/runner': 4.0.16 '@vitest/snapshot': 4.0.16 @@ -9049,7 +8967,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.0.6(sass@1.97.0)(terser@5.44.1) + vite: 7.3.1(sass@1.97.2)(terser@5.44.1) why-is-node-running: 2.3.0 optionalDependencies: jsdom: 26.1.0 @@ -9223,7 +9141,7 @@ snapshots: workbox-core@7.1.0: {} - workbox-core@7.3.0: {} + workbox-core@7.4.0: {} workbox-expiration@7.1.0: dependencies: @@ -9280,10 +9198,10 @@ snapshots: '@types/trusted-types': 2.0.7 workbox-core: 7.1.0 - workbox-window@7.3.0: + workbox-window@7.4.0: dependencies: '@types/trusted-types': 2.0.7 - workbox-core: 7.3.0 + workbox-core: 7.4.0 wrap-ansi@6.2.0: dependencies: diff --git a/src/lib/chord-editor/action-linter.ts b/src/lib/chord-editor/action-linter.ts new file mode 100644 index 00000000..03105146 --- /dev/null +++ b/src/lib/chord-editor/action-linter.ts @@ -0,0 +1,196 @@ +import { + KEYMAP_CODES, + KEYMAP_IDS, + type KeyInfo, +} from "$lib/serial/keymap-codes"; +import { syntaxTree } from "@codemirror/language"; +import { linter, type Diagnostic } from "@codemirror/lint"; +import { derived, get } from "svelte/store"; +import { parseCharaChords } from "./action-serializer"; +import { deviceChords } from "$lib/serial/connection"; + +export const actionLinterDependencies = derived( + [KEYMAP_IDS, KEYMAP_CODES, deviceChords], + (it) => it, +); + +export const actionLinter = linter( + (view) => { + const diagnostics: Diagnostic[] = []; + const [ids, codes, deviceChords] = get(actionLinterDependencies); + + const { meta, compoundInputs } = parseCharaChords(view.state, ids); + + syntaxTree(view.state) + .cursor() + .iterate((node) => { + let action: KeyInfo | undefined = undefined; + switch (node.name) { + case "SingleLetter": { + action = ids.get(view.state.doc.sliceString(node.from, node.to)); + break; + } + case "ActionId": { + action = ids.get(view.state.doc.sliceString(node.from, node.to)); + break; + } + case "HexNumber": { + const hexString = view.state.doc.sliceString(node.from, node.to); + const code = Number.parseInt(hexString, 16); + if (hexString.length === 10) { + if (compoundInputs.has(code)) { + diagnostics.push({ + from: node.from, + to: node.to, + severity: "info", + message: "Compound hash literal can be expanded", + actions: [ + { + name: "Expand", + apply(view, from, to) { + view.dispatch({ + changes: { + from: from - 1, + to: to + 1, + insert: compoundInputs.get(code)! + "|", + }, + }); + }, + }, + ], + }); + } + return; + } + + if (!(code >= 0 && code <= 1023)) { + diagnostics.push({ + from: node.from, + to: node.to, + severity: "error", + message: "Hex code invalid (out of range)", + actions: [ + { + name: "Remove", + apply(view, from, to) { + view.dispatch({ changes: { from, to } }); + }, + }, + ], + }); + return; + } + + action = codes.get(code); + break; + } + default: + return; + } + if (!action) { + const action = view.state.doc.sliceString(node.from, node.to); + diagnostics.push({ + from: node.from, + to: node.to, + severity: node.name === "HexNumber" ? "warning" : "error", + message: `Unknown action: ${action}`, + actions: [ + ...(node.name === "SingleLetter" + ? ([ + { + name: "Generate Windows Hex Numpad Code", + apply(view, from, to) { + view.dispatch({ + changes: { + from, + to, + insert: + "" + + action + .codePointAt(0)! + .toString(16) + .split("") + .map((c) => + /^\d$/.test(c) + ? `` + : c.toLowerCase(), + ) + .join("") + + "", + }, + }); + }, + }, + ] satisfies Diagnostic["actions"]) + : []), + ], + }); + } + }); + + for (const m of meta) { + if (m.invalidActions) { + diagnostics.push({ + from: m.from, + to: m.to, + severity: "error", + markClass: "chord-invalid", + message: `Chord contains invalid actions`, + }); + } + if (m.invalidInput) { + diagnostics.push({ + from: m.from, + to: m.to, + severity: "error", + markClass: "chord-invalid", + message: `Chord input is invalid`, + }); + } + if (m.emptyPhrase) { + diagnostics.push({ + from: m.from, + to: m.to, + severity: "warning", + message: `Chord phrase is empty`, + }); + } + if (m.overriddenBy) { + diagnostics.push({ + from: m.from, + to: m.to, + severity: "warning", + message: `Chord overridden by previous chord`, + }); + } + if (m.orphan) { + diagnostics.push({ + from: m.from, + to: m.to, + severity: "warning", + message: `Orphan compound chord`, + }); + } + if (m.disabled) { + diagnostics.push({ + from: m.from, + to: m.to, + severity: "info", + markClass: "chord-ignored", + message: `Chord disabled`, + }); + } + if ((m.overrides?.length ?? 0) > 0) { + diagnostics.push({ + from: m.from, + to: m.to, + severity: "info", + message: `Chord overrides other chords`, + }); + } + } + + return diagnostics; + }, + { delay: 100 }, +); diff --git a/src/lib/chord-editor/action-plugin.ts b/src/lib/chord-editor/action-plugin.ts index cbf8a10a..dbb3811d 100644 --- a/src/lib/chord-editor/action-plugin.ts +++ b/src/lib/chord-editor/action-plugin.ts @@ -56,18 +56,16 @@ function actionWidgets(view: EditorView) { enter: (node) => { if (node.name !== "ExplicitAction") return; const value = - node.node.getChild("ActionId") ?? - node.node.getChild("HexNumber") ?? - node.node.getChild("DecimalNumber"); + node.node.getChild("ActionId") ?? node.node.getChild("HexNumber"); if (!value) return; if (!node.node.getChild("ExplicitDelimEnd")) { return; } - const id = view.state.doc.sliceString(value.from, value.to); + if (value.name === "HexNumber" && id.length === 10) return; let deco = Decoration.replace({ widget: new ActionWidget( - value.name === "ActionId" ? id : parseInt(id), + value.name === "ActionId" ? id : Number.parseInt(id, 16), ), }); widgets.push(deco.range(node.from, node.to)); diff --git a/src/lib/chord-editor/action-serializer.ts b/src/lib/chord-editor/action-serializer.ts index d2781d63..3be836de 100644 --- a/src/lib/chord-editor/action-serializer.ts +++ b/src/lib/chord-editor/action-serializer.ts @@ -1,5 +1,20 @@ -import { KEYMAP_CODES, type KeyInfo } from "$lib/serial/keymap-codes"; +import { + KEYMAP_CODES, + KEYMAP_IDS, + type KeyInfo, +} from "$lib/serial/keymap-codes"; +import type { CharaChordFile } from "$lib/share/chara-file"; +import { syntaxTree } from "@codemirror/language"; +import { StateEffect, ChangeDesc, type EditorState } from "@codemirror/state"; +import type { Update } from "@codemirror/collab"; import { get } from "svelte/store"; +import { + composeChordInput, + hashChord, + splitCompound, + willBeValidChordInput, +} from "$lib/serial/chord"; +import type { SyntaxNodeRef } from "@lezer/common"; export function canUseIdAsString(info: KeyInfo): boolean { return !!info.id && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(info.id); @@ -9,8 +24,270 @@ export function actionToValue(action: number | KeyInfo) { const info = typeof action === "number" ? get(KEYMAP_CODES).get(action) : action; if (info && info.id?.length === 1) - return /^[<>\\\s]$/.test(info.id) ? `\\${info.id}` : info.id; + return /^[<>|\\\s]$/.test(info.id) ? `\\${info.id}` : info.id; if (!info || !canUseIdAsString(info)) return `<0x${(info?.code ?? action).toString(16).padStart(2, "0")}>`; return `<${info.id}>`; } + +export interface ParseMeta { + from: number; + to: number; + invalidActions?: true; + invalidInput?: true; + emptyPhrase?: true; + orphan?: true; + disabled?: true; + overrides?: number[]; + overriddenBy?: number; +} + +export interface ParseResult { + result: CharaChordFile["chords"]; + meta: ParseMeta[]; + compoundInputs: Map; +} + +export function parseCharaChords( + data: EditorState, + ids: Map, +): ParseResult { + const chords: CharaChordFile["chords"] = []; + const metas: ParseMeta[] = []; + const keys = new Map(); + const compoundInputs = new Map(); + + let currentChord: CharaChordFile["chords"][number] | undefined = undefined; + let compound: number | undefined = undefined; + let currentActions: number[] = []; + let invalidActions = false; + let invalidInput = false; + let chordFrom = 0; + + function makeChordInput(node: SyntaxNodeRef): number[] { + invalidInput ||= !willBeValidChordInput(currentActions.length, !!compound); + const input = composeChordInput(currentActions, compound); + compound = hashChord(input); + if (!compoundInputs.has(compound)) { + compoundInputs.set(compound, data.doc.sliceString(chordFrom, node.from)); + } + return input; + } + + syntaxTree(data) + .cursor() + .iterate( + (node) => { + if (node.name === "Chord") { + currentChord = undefined; + compound = undefined; + invalidActions = false; + invalidInput = false; + chordFrom = node.from; + } else if (node.name === "ActionString") { + currentActions = []; + } else if (node.name === "HexNumber") { + const hexString = data.doc.sliceString(node.from, node.to); + const code = Number.parseInt(hexString, 16); + if (hexString.length === 10) { + if (compound !== undefined) { + invalidInput = true; + } + compound = code; + } else { + if (Number.isNaN(code) || code < 0 || code > 1023) { + invalidActions = true; + } + currentActions.push(code); + } + } else if ( + node.name === "ActionId" || + node.name === "SingleLetter" || + node.name === "EscapedChar" + ) { + const id = data.doc.sliceString(node.from, node.to); + const code = ids.get(id)?.code; + if (code === undefined) { + invalidActions = true; + const encoder = new TextEncoder(); + const bytes = encoder.encode(id); + for (let byte of bytes) { + currentActions.push(-byte); + } + } else { + currentActions.push(code); + } + } + }, + (node) => { + if (node.name === "Chord" && currentChord !== undefined) { + if (currentChord !== undefined) { + currentChord[1] = currentActions; + const index = chords.length; + chords.push(currentChord); + const meta: ParseMeta = { from: node.from, to: node.to }; + if (invalidActions) { + meta.invalidActions = true; + } + if (invalidInput) { + meta.invalidInput = true; + } + metas.push(meta); + if (currentChord[1].length === 0) { + meta.emptyPhrase = true; + } + const key = JSON.stringify(currentChord[0]); + if (!meta.invalidInput) { + if (keys.has(key)) { + const targetIndex = keys.get(key)!; + const targetMeta = metas[targetIndex]!; + if (!targetMeta.overrides) targetMeta.overrides = []; + targetMeta.overrides.push(index); + meta.overriddenBy = targetIndex; + } else { + keys.set(key, index); + } + } + if ( + meta.emptyPhrase || + meta.invalidInput || + meta.invalidActions || + meta.overriddenBy !== undefined + ) { + meta.disabled = true; + } + } + } else if (node.name === "CompoundDelim") { + makeChordInput(node); + } else if (node.name === "PhraseDelim") { + const input = makeChordInput(node); + currentChord = [composeChordInput(input, compound), []]; + } + }, + ); + + for (let i = 0; i < metas.length; i++) { + const [, compound] = splitCompound(chords[i]![0]); + if (compound !== undefined && !compoundInputs.has(compound)) { + metas[i]!.orphan = true; + } + } + + return { result: chords, meta: metas, compoundInputs }; +} + +class ChordRecord { + private chords = new Map>(); + + constructor(chords: CharaChordFile["chords"]) { + for (let chord of chords) { + const key = JSON.stringify(chord[0]); + if (!this.chords.has(key)) { + this.chords.set(key, new Set()); + } + this.chords.get(key)!.add(JSON.stringify(chord)); + } + } + + static createDiff( + previous: CharaChordFile["chords"], + updated: CharaChordFile["chords"], + ) { + const deleted = new ChordRecord(previous); + const added = new ChordRecord(updated); + const dupA = deleted.duplicates(added); + const dupB = added.duplicates(deleted); + for (let chord of dupA) { + deleted.remove(chord); + added.remove(chord); + } + for (let chord of dupB) { + deleted.remove(chord); + added.remove(chord); + } + return { deleted, added }; + } + + duplicates( + other: ChordRecord, + ): IteratorObject { + const duplicates = new Set(); + for (let [key, chordSet] of this.chords) { + for (let chord of chordSet) { + if (other.hasInternal(key, chord)) { + duplicates.add(chord); + } + } + } + return duplicates + .values() + .map((it) => JSON.parse(it) as CharaChordFile["chords"][number]); + } + + private hasInternal(key: string, chord: string): boolean { + return this.chords.get(key)?.has(chord) ?? false; + } + + has(chord: CharaChordFile["chords"][number]): boolean { + return this.hasInternal(JSON.stringify(chord[0]), JSON.stringify(chord)); + } + + remove(chord: CharaChordFile["chords"][number]) { + const key = JSON.stringify(chord[0]); + const set = this.chords.get(key); + if (set) { + set.delete(JSON.stringify(chord)); + if (set.size === 0) { + this.chords.delete(key); + } + } + } +} + +export function syncChords( + previous: CharaChordFile["chords"], + updated: CharaChordFile["chords"], + state: EditorState, +) { + const deviceDiff = ChordRecord.createDiff(previous, updated); + const current = parseCharaChords(state, get(KEYMAP_IDS)); + // save initial device chords + // compare new device chords with initial device chords + // take changed/new/removed chords + // compare current editor chords with initial device chords + // compare two change sets + // apply removals if the chord didn't change on either end + // apply +} + +export function rebaseUpdates( + updates: readonly Update[], + over: readonly { changes: ChangeDesc; clientID: string }[], +) { + if (!over.length || !updates.length) return updates; + let changes: ChangeDesc | null = null, + skip = 0; + for (let update of over) { + let other = skip < updates.length ? updates[skip] : null; + if (other && other.clientID == update.clientID) { + if (changes) changes = changes.mapDesc(other.changes, true); + skip++; + } else { + changes = changes ? changes.composeDesc(update.changes) : update.changes; + } + } + + if (skip) updates = updates.slice(skip); + return !changes + ? updates + : updates.map((update) => { + let updateChanges = update.changes.map(changes!); + changes = changes!.mapDesc(update.changes, true); + return { + changes: updateChanges, + effects: + update.effects && StateEffect.mapEffects(update.effects, changes!), + clientID: update.clientID, + }; + }); +} diff --git a/src/lib/chord-editor/autocomplete.ts b/src/lib/chord-editor/autocomplete.ts index abb796e5..42697d6d 100644 --- a/src/lib/chord-editor/autocomplete.ts +++ b/src/lib/chord-editor/autocomplete.ts @@ -1,72 +1,39 @@ -import { KEYMAP_CATEGORIES, KEYMAP_CODES } from "$lib/serial/keymap-codes"; -import type { - Completion, - CompletionSection, - CompletionSource, -} from "@codemirror/autocomplete"; -import { derived, get } from "svelte/store"; -import { actionToValue, canUseIdAsString } from "./action-serializer"; +import { + EditorView, + ViewPlugin, + ViewUpdate, + type PluginValue, +} from "@codemirror/view"; +import { syntaxTree } from "@codemirror/language"; +import type { EditorState } from "@codemirror/state"; -const completionSections = derived( - KEYMAP_CATEGORIES, - (categories) => - new Map( - categories.map( - (category) => - [ - category, - { - name: category.name, - } satisfies CompletionSection, - ] as const, - ), - ), -); +export function actionAutocompletePlugin( + query: (query: string | undefined) => void, +) { + return ViewPlugin.fromClass( + class implements PluginValue { + constructor(readonly view: EditorView) {} -export const actionAutocompleteItems = derived( - [KEYMAP_CODES, completionSections], - ([codes, sections]) => - codes - .values() - .map((info) => { - const canUseId = canUseIdAsString(info); - const completionValue = - (canUseId && info.id) || - `0x${info.code.toString(16).padStart(2, "0")}`; - return { - label: - [ - canUseId || !info.id ? undefined : `"${info.id}"`, - info.title, - info.variant?.replace(/^[a-z]/g, (c) => c.toUpperCase()), - ] - .filter(Boolean) - .join(" ") || completionValue, - detail: actionToValue(info), - section: info.category ? sections.get(info.category) : undefined, - info: info.description, - type: "keyword", - apply: completionValue + ">", - } satisfies Completion; - }) - .filter( - (item) => typeof item.label === "string" && item.apply !== undefined, - ) - .toArray(), -); + update(update: ViewUpdate) { + query(this.resolveAutocomplete(update.state)); + } -export const actionAutocomplete = ((context) => { - let word = context.tokenBefore([ - "ExplicitDelimStart", - "ActionId", - "HexNumber", - "DecimalNumber", - ]); - if (!word) return null; - console.log(get(actionAutocompleteItems)); - return { - from: word.type.name === "ExplicitDelimStart" ? word.to : word.from, - validFor: /^"} - CompoundDelim {"+>"} + CompoundDelim {"|"} PhraseDelim {"=>"} Escape { "\\" } HexNumber { "0x" $[a-fA-F0-9]+ } - DecimalNumber { $[0-9]+ } ActionId { $[a-zA-Z_]$[a-zA-Z0-9_]* } SingleLetter { ![\\] } EscapedLetter { ![] } diff --git a/src/lib/chord-editor/test.txt b/src/lib/chord-editor/test.txt index dd377111..3bafe340 100644 --- a/src/lib/chord-editor/test.txt +++ b/src/lib/chord-editor/test.txt @@ -1,16 +1,16 @@ -.= => => -;ims => <0x219> --; => <0x23e>_<0x23e> -.;g => <0x23e>...<0x23e> -'dg => <0x23e>'<0x23e> -'gl => <0x23e>'ll<0x23e> -'ar => <0x23e>'re<0x23e> -'gs => <0x23e>'s<0x23e> -'ev => <0x23e>'ve<0x23e> --; => <0x23e><0x223>-<0x223> -; => <0x23e><0x223><0x23d><0x223> -;g => <0x23e><0x223><0x223> -deg => <0x23e>ed<0x23e> -;gr => <0x23e>er<0x23e> -;es => <0x23e>es<0x23e> -;est => <0x23e>est<0x23e> +a|.==>t=t +;ims=<0x219> +-;=><0x23e>_<0x23e> +.;g=><0x23e>...<0x23e> +'dg=><0x23e>'<0x23e> +'gl=><0x23e>'ll<0x23e> +'ar=><0x23e>'re<0x23e> +'gs=><0x23e>'s<0x23e> +'ev=><0x23e>'ve<0x23e> +-;=><0x23e><0x223>-<0x223> +;=><0x23e><0x223><0x23d><0x223> +;g=><0x23e><0x223><0x223> +deg=><0x23e>ed<0x23e> +;gr=><0x23e>er<0x23e> +;es=><0x23e>es<0x23e> +;est=><0x23e>est<0x23e> diff --git a/src/lib/components/Action.svelte b/src/lib/components/Action.svelte index 3fa99fe3..82abc83d 100644 --- a/src/lib/components/Action.svelte +++ b/src/lib/components/Action.svelte @@ -8,10 +8,12 @@ let { action, display, + ignoreIcon = false, inText = false, }: { action: string | number | KeyInfo; display: "inline-keys" | "keys" | "verbose"; + ignoreIcon?: boolean; inText?: boolean; } = $props(); @@ -30,6 +32,7 @@ ? ({ code: 1024, id: action } satisfies KeyInfo) : action), ); + let icon = $derived(ignoreIcon ? undefined : info.icon); let dynamicMapping = $derived(info.keyCode && $osLayout.get(info.keyCode)); let hasPopover = $derived( !retrievedInfo || !info.id || info.title || info.description, @@ -63,7 +66,7 @@ {#snippet kbdText()} {dynamicMapping ?? - info.icon ?? + icon ?? info.display ?? info.id ?? `0x${info.code.toString(16)}`} @@ -71,7 +74,7 @@ {#snippet kbdSnippet(withPopover = true)} 1023} @@ -91,7 +94,7 @@ class:left={info.variant === "left"} class:right={info.variant === "right"}>{dynamicMapping} - {:else if !info.icon && info.id?.length === 1} + {:else if !icon && info.id?.length === 1} 1023} {@attach hasPopover ? actionTooltip(popover) : null} @@ -155,21 +158,50 @@ text-decoration: line-through; } - $variant-offset: 12px; - $variant-padding: calc(2px + $variant-offset); $variant-color: color-mix( in srgb, var(--md-sys-color-on-surface) 50%, transparent ); + .left, + .right { + background-color: transparent; + + &::before { + position: absolute; + inset: 0; + outline: 2px dashed + color-mix(in srgb, var(--bg-color), var(--md-sys-color-outline) 40%); + outline-offset: -2px; + border-radius: var(--border-radius); + content: ""; + } + } + + $cutoff: 60%; + .left { - padding-inline-end: $variant-padding; - text-shadow: $variant-offset 0 2px $variant-color; + background-image: linear-gradient( + to right, + var(--bg-color) $cutoff, + transparent $cutoff + ); + + &::before { + clip-path: inset(0 0 0 $cutoff); + } } .right { - padding-inline-start: $variant-padding; - text-shadow: -$variant-offset 0 2px $variant-color; + background-image: linear-gradient( + to left, + var(--bg-color) $cutoff, + transparent $cutoff + ); + + &::before { + clip-path: inset(0 $cutoff 0 0); + } } .inline-kbd { diff --git a/src/lib/components/layout/ActionList.svelte b/src/lib/components/layout/ActionList.svelte index 15d116af..ad0149d7 100644 --- a/src/lib/components/layout/ActionList.svelte +++ b/src/lib/components/layout/ActionList.svelte @@ -19,13 +19,17 @@ let { currentAction = undefined, nextAction = undefined, + queryFilter = undefined, + ignoreIcon, autofocus = false, onselect, onclose, }: { currentAction?: number; + queryFilter?: string; nextAction?: number; autofocus?: boolean; + ignoreIcon?: boolean; onselect?: (id: number) => void; onclose?: () => void; } = $props(); @@ -43,6 +47,14 @@ createIndex($KEYMAP_CODES); }); + let didClear = true; + $effect(() => { + if (queryFilter !== undefined || !didClear) { + searchBox.value = queryFilter ?? ""; + search(); + } + }); + async function createIndex(codes: Map) { for (const [, action] of codes) { await index?.addAsync( @@ -60,6 +72,7 @@ (category) => [category, []] as [KeymapCategory, KeyInfo[]], ), ); + didClear = searchBox.value === ""; const result = searchBox.value === "" ? Array.from($KEYMAP_CODES.keys()) @@ -167,7 +180,7 @@
  • Action code is out of range
  • {/if} {/if} - {#each results as [category, actions] (category)} + {#each results as [category, actions] (actions)} {#if actions.length > 0}

    {category.name}

    @@ -191,7 +204,7 @@ } : undefined} > - + {/each} diff --git a/src/lib/serial/chord.ts b/src/lib/serial/chord.ts index 532e2ca3..4f80421f 100644 --- a/src/lib/serial/chord.ts +++ b/src/lib/serial/chord.ts @@ -56,6 +56,84 @@ export function deserializeActions(native: bigint): number[] { return actions; } +const compoundHashItems = 3; +const maxChordInputItems = 12; +const actionBits = 10; +const actionMask = (1 << actionBits) - 1; + +/** + * Applies the compound value to a **valid** chord input + */ +export function applyCompound(actions: number[], compound: number): number[] { + const result = [...actions]; + for (let i = 0; i < compoundHashItems; i++) { + result[i] = (compound >>> (i * actionBits)) & actionMask; + } + result[compoundHashItems] = 0; + return result; +} + +/** + * Extracts the compound value from a chord input, if present + */ +export function splitCompound( + actions: number[], +): [inputs: number[], compound: number | undefined] { + if (actions[compoundHashItems] != 0) { + return [ + actions.slice( + Math.max( + 0, + actions.findIndex((it) => it !== 0), + ), + ), + undefined, + ]; + } + + let compound = 0; + for (let i = 0; i < compoundHashItems; i++) { + compound |= (actions[i] ?? 0) << (i * actionBits); + } + + return [ + actions.slice( + actions.findIndex((it, i) => i > compoundHashItems && it !== 0), + ), + compound === 0 ? undefined : compound, + ]; +} + +export function willBeValidChordInput( + inputCount: number, + hasCompound: boolean, +): boolean { + return ( + inputCount > 0 && + inputCount <= maxChordInputItems - (hasCompound ? compoundHashItems + 1 : 0) + ); +} + +/** + * Composes a chord input from a list of actions and an optional compound value + * to a valid chord input + */ +export function composeChordInput( + actions: number[], + compound?: number, +): number[] { + const result = [ + ...Array.from( + { + length: Math.max(0, maxChordInputItems - actions.length), + }, + () => 0, + ), + ...actions.slice(0, maxChordInputItems).sort((a, b) => a - b), + ]; + return compound !== undefined ? applyCompound(result, compound) : result; +} + /** * Hashes a chord input the same way as CCOS */ @@ -72,5 +150,6 @@ export function hashChord(actions: number[]) { if ((hash & 0xff) === 0xff) { hash ^= 0xff; } - return hash & 0x3fff_ffff; + hash &= 0x3fff_ffff; + return hash === 0 ? 1 : hash; } diff --git a/src/lib/serial/connection.ts b/src/lib/serial/connection.ts index f0196aa1..55ecf933 100644 --- a/src/lib/serial/connection.ts +++ b/src/lib/serial/connection.ts @@ -55,7 +55,10 @@ export const syncStatus: Writable< "done" | "error" | "downloading" | "uploading" > = writable("done"); -export const deviceMeta = writable(undefined); +export const deviceMeta = persistentWritable( + "current-meta", + undefined, +); export interface ProgressInfo { max: number; diff --git a/src/lib/style/_kbd.scss b/src/lib/style/_kbd.scss index 0ab2e5da..a4ae2ba4 100644 --- a/src/lib/style/_kbd.scss +++ b/src/lib/style/_kbd.scss @@ -1,22 +1,22 @@ kbd { - display: inline-flex; - justify-content: center; - align-items: center; - margin-block: 6px; - border-radius: 4px; - - //border: 1px solid currentcolor; - background: color-mix( + --bg-color: color-mix( in srgb, var(--md-sys-color-surface-variant) 50%, transparent ); - padding: 4px; + --border-radius: 4px; + display: inline-flex; + position: relative; + justify-content: center; + align-items: center; + margin-block: 6px; + border-radius: var(--border-radius); + background: var(--bg-color); + padding: 4px; height: 20px; color: currentcolor; font-weight: normal; - font-size: 14px; &.icon { diff --git a/src/routes/(app)/config/cv2/+page.svelte b/src/routes/(app)/config/cv2/+page.svelte index 9713d609..a16b3476 100644 --- a/src/routes/(app)/config/cv2/+page.svelte +++ b/src/routes/(app)/config/cv2/+page.svelte @@ -4,13 +4,7 @@ import { actionToValue } from "$lib/chord-editor/action-serializer"; import { actionPlugin } from "$lib/chord-editor/action-plugin"; import { delimPlugin } from "$lib/chord-editor/chord-delim-plugin"; - import { - drawSelection, - dropCursor, - highlightActiveLine, - highlightSpecialChars, - keymap, - } from "@codemirror/view"; + import { highlightActiveLine, keymap } from "@codemirror/view"; import { history, standardKeymap } from "@codemirror/commands"; import "$lib/chord-editor/chords.grammar"; import { @@ -18,20 +12,32 @@ chordLanguageSupport, } from "$lib/chord-editor/chords-grammar-plugin"; import { syntaxHighlighting } from "@codemirror/language"; - import { autocompletion } from "@codemirror/autocomplete"; import { persistentWritable } from "$lib/storage"; import ActionList from "$lib/components/layout/ActionList.svelte"; + import { actionAutocompletePlugin } from "$lib/chord-editor/autocomplete"; + import { + actionLinter, + actionLinterDependencies, + } from "$lib/chord-editor/action-linter"; + import { forceLinting } from "@codemirror/lint"; + import { untrack } from "svelte"; + import { splitCompound } from "$lib/serial/chord"; + + let queryFilter: string | undefined = $state(undefined); const rawCode = persistentWritable("chord-editor-raw-code", false); const showEdits = persistentWritable("chord-editor-show-edits", true); + const denseSpacing = persistentWritable("chord-editor-spacing", false); + let originalDoc = $derived( $chords .map((chord) => { + const [actions, compound] = splitCompound(chord.actions); return ( - chord.actions - .filter((it) => it !== 0) - .map((it) => actionToValue(it)) - .join("") + + (compound + ? "<0x" + compound.toString(16).padStart(8, "0") + ">" + : "") + + actions.map((it) => actionToValue(it)).join("") + "=>" + chord.phrase.map((it) => actionToValue(it)).join("") ); @@ -49,31 +55,60 @@ extensions: [ ...($rawCode ? [] : [delimPlugin, actionPlugin]), chordLanguageSupport(), - autocompletion({ icons: false, selectOnOpen: true }), + actionLinter, + // lineNumbers(), + actionAutocompletePlugin((query) => { + queryFilter = query; + }), history(), - dropCursor(), syntaxHighlighting(chordHighlightStyle), highlightActiveLine(), - drawSelection(), - highlightSpecialChars(), + // drawSelection(), + EditorView.theme({ + ".cm-line": { + borderBottom: "1px solid transparent", + caretColor: "var(--md-sys-color-on-surface)", + }, + ".cm-scroller": { + overflow: "auto", + width: "100%", + fontFamily: "inherit !important", + gap: "8px", + }, + ".cm-content": { + width: "100%", + }, + ".cm-cursor": { + borderColor: "var(--md-sys-color-on-surface)", + }, + }), keymap.of(standardKeymap), ], }); return () => view.destroy(); }); + + $effect(() => { + $actionLinterDependencies; + untrack(() => view && forceLinting(view)); + }); - - + + +
    -
    +
    diff --git a/src/lib/chord-editor/action-linter.ts b/src/lib/chord-editor/action-linter.ts index 13e6f730..3930eb4b 100644 --- a/src/lib/chord-editor/action-linter.ts +++ b/src/lib/chord-editor/action-linter.ts @@ -128,6 +128,27 @@ export function actionLinter(config?: Parameters[1]) { message: `Phrase changed`, }); } + + if (chord.aliases) { + diagnostics.push({ + from: chord.phrase.range[0], + to: chord.phrase.range[1], + severity: "warning", + markClass: "chord-alias", + message: `Alias of ${chord.aliases.length} chord(s)`, + actions: chord.aliases.map((alias) => ({ + name: `Go to ${view.state.doc.sliceString(alias.range[0], alias.input?.range[1] ?? alias.range[1])}`, + apply(view) { + view.dispatch({ + selection: { + anchor: alias.range[0], + }, + scrollIntoView: true, + }); + }, + })), + }); + } } } return diagnostics; diff --git a/src/lib/chord-editor/action-serializer.ts b/src/lib/chord-editor/action-serializer.ts index de22321b..7e4b746a 100644 --- a/src/lib/chord-editor/action-serializer.ts +++ b/src/lib/chord-editor/action-serializer.ts @@ -1,8 +1,5 @@ -import { KEYMAP_CODES, type KeyInfo } from "$lib/serial/keymap-codes"; +import type { KeyInfo } from "$lib/serial/keymap-codes"; import type { CharaChordFile } from "$lib/share/chara-file"; -import { syntaxTree } from "@codemirror/language"; -import type { EditorState } from "@codemirror/state"; -import { get } from "svelte/store"; import { composeChordInput, hasConcatenator, @@ -15,25 +12,13 @@ import type { MetaRange, ParseResult, } from "./parse-meta"; +import type { Tree } from "@lezer/common"; -export function canUseIdAsString(info: KeyInfo): boolean { - return !!info.id && /^[^>\n]+$/.test(info.id); -} - -export function actionToValue(action: number | KeyInfo) { - const info = - typeof action === "number" ? get(KEYMAP_CODES).get(action) : action; - if (info && info.id?.length === 1) - return /^[<>|\\\s]$/.test(info.id) ? `\\${info.id}` : info.id; - if (!info || !canUseIdAsString(info)) - return `<0x${(info?.code ?? action).toString(16).padStart(2, "0")}>`; - return `<${info.id}>`; -} - -export function parseChordMeta( - data: EditorState, +function parseChordMeta( + tree: Tree, ids: Map, codes: Map, + sliceString: (from: number, to: number) => string, ): ChordMeta[] { console.time("parseChordTree"); const result: ChordMeta[] = []; @@ -42,124 +27,118 @@ export function parseChordMeta( let actions: ActionMeta[] = []; let actionRange: MetaRange | undefined = undefined; - syntaxTree(data) - .cursor() - .iterate( - (node) => { - if (node.name === "Action") { - actionRange = [node.from, node.to]; - } else if (node.name === "ChordPhrase") { - current.phrase = { - range: [node.from, node.to], - value: [], - valid: true, - actions: [], - hasConcatenator: false, - }; - } else if (node.name === "Chord") { - current = { range: [node.from, node.to], valid: false }; - } else if (node.name === "ActionString") { - actions = []; - } else if (node.name === "HexNumber") { - const hexString = data.doc.sliceString(node.from, node.to); - const code = Number.parseInt(hexString, 16); - const parentNode = node.node.parent; - if (parentNode?.type.name === "CompoundLiteral") { - current.compounds ??= []; - current.compounds.push({ - range: [parentNode.from, parentNode.to], - value: code, - actions: [], - valid: true, // TODO: validate compound literal - }); - } else { - const valid = !(Number.isNaN(code) || code < 0 || code > 1023); - actions.push({ - code, - info: codes.get(code), - explicit: true, - valid, - range: actionRange!, - }); - } - } else if ( - node.name === "ActionId" || - node.name === "SingleLetter" || - node.name === "EscapedLetter" - ) { - const id = data.doc.sliceString(node.from, node.to); - const info = ids.get(id); - const value: ActionMeta = { - code: info?.code ?? Number.NaN, - info, - valid: info !== undefined, - range: actionRange!, - }; - if (node.name === "ActionId") { - value.explicit = true; - } - actions.push(value); - } - }, - (node) => { - if (node.name === "Chord") { - result.push(current); - if (current.phrase) { - current.phrase.actions = actions; - current.phrase.value = actions.map(({ code }) => code); - current.phrase.valid = actions.every(({ valid }) => valid); - current.phrase.hasConcatenator = hasConcatenator( - current.phrase.value, - codes, - ); - } - current.valid = - (current.phrase?.valid ?? false) && (current.input?.valid ?? false); - if (!current.valid) { - current.disabled = true; - } - } else if (node.name === "CompoundInput") { - const lastCompound = current.compounds?.at(-1); + tree.cursor().iterate( + (node) => { + if (node.name === "Action") { + actionRange = [node.from, node.to]; + } else if (node.name === "ChordPhrase") { + current.phrase = { + range: [node.from, node.to], + value: [], + valid: true, + actions: [], + hasConcatenator: false, + }; + } else if (node.name === "Chord") { + current = { range: [node.from, node.to], valid: false }; + } else if (node.name === "ActionString") { + actions = []; + } else if (node.name === "HexNumber") { + const hexString = sliceString(node.from, node.to); + const code = Number.parseInt(hexString, 16); + const parentNode = node.node.parent; + if (parentNode?.type.name === "CompoundLiteral") { current.compounds ??= []; current.compounds.push({ - range: [node.from, node.to], - value: hashChord( - composeChordInput( - actions.map(({ code }) => code), - lastCompound?.value, - ), - ), - actions, - valid: - willBeValidChordInput( - actions.length, - lastCompound !== undefined, - ) && actions.every(({ valid }) => valid), + range: [parentNode.from, parentNode.to], + value: code, + actions: [], + valid: true, // TODO: validate compound literal + }); + } else { + const valid = !(Number.isNaN(code) || code < 0 || code > 1023); + actions.push({ + code, + info: codes.get(code), + explicit: true, + valid, + range: actionRange!, }); - } else if (node.name === "ChordInput") { - const lastCompound = current.compounds?.at(-1); - current.input = { - range: [node.from, node.to], - value: composeChordInput( + } + } else if ( + node.name === "ActionId" || + node.name === "SingleLetter" || + node.name === "EscapedLetter" + ) { + const id = sliceString(node.from, node.to); + const info = ids.get(id); + const value: ActionMeta = { + code: info?.code ?? Number.NaN, + info, + valid: info !== undefined, + range: actionRange!, + }; + if (node.name === "ActionId") { + value.explicit = true; + } + actions.push(value); + } + }, + (node) => { + if (node.name === "Chord") { + result.push(current); + if (current.phrase) { + current.phrase.actions = actions; + current.phrase.value = actions.map(({ code }) => code); + current.phrase.valid = actions.every(({ valid }) => valid); + current.phrase.hasConcatenator = hasConcatenator( + current.phrase.value, + codes, + ); + } + current.valid = + (current.phrase?.valid ?? false) && (current.input?.valid ?? false); + if (!current.valid) { + current.disabled = true; + } + } else if (node.name === "CompoundInput") { + const lastCompound = current.compounds?.at(-1); + current.compounds ??= []; + current.compounds.push({ + range: [node.from, node.to], + value: hashChord( + composeChordInput( actions.map(({ code }) => code), lastCompound?.value, ), - valid: - willBeValidChordInput( - actions.length, - lastCompound !== undefined, - ) && actions.every(({ valid }) => valid), - actions, - }; - } - }, - ); + ), + actions, + valid: + willBeValidChordInput(actions.length, lastCompound !== undefined) && + actions.every(({ valid }) => valid), + }); + } else if (node.name === "ChordInput") { + const lastCompound = current.compounds?.at(-1); + current.input = { + range: [node.from, node.to], + value: composeChordInput( + actions.map(({ code }) => code), + lastCompound?.value, + ), + valid: + willBeValidChordInput(actions.length, lastCompound !== undefined) && + actions.every(({ valid }) => valid), + actions, + }; + } + }, + ); console.timeEnd("parseChordTree"); return result; } -function resolveChordOverrides(chords: ChordMeta[]) { +function resolveChordOverrides(chords: ChordMeta[]): Map { console.time("resolveOverrides"); const seen = new Map(); for (const info of chords) { @@ -176,9 +155,10 @@ function resolveChordOverrides(chords: ChordMeta[]) { } } console.timeEnd("resolveOverrides"); + return seen; } -function resolveChordAliases(chords: ChordMeta[]) { +function resolveChordAliases(chords: ChordMeta[]): Map { console.time("resolveAliases"); const aliases = new Map(); for (const info of chords) { @@ -188,17 +168,20 @@ function resolveChordAliases(chords: ChordMeta[]) { list.push(info); aliases.set(key, list); } - for (const aliasList of aliases.values()) { - if (aliasList.length > 1) { - for (const info of aliasList) { - info.aliases = aliasList.filter((i) => i !== info); + for (const [key, value] of aliases) { + if (value.length <= 1) { + aliases.delete(key); + } else { + for (const info of value) { + info.aliases = value.filter((i) => i !== info); } } } console.timeEnd("resolveAliases"); + return aliases; } -function resolveCompoundParents(chords: ChordMeta[]) { +function resolveCompoundParents(chords: ChordMeta[]): Map { console.time("resolveCompoundParents"); const compounds = new Map(); for (const chord of chords) { @@ -222,31 +205,32 @@ function resolveCompoundParents(chords: ChordMeta[]) { } } console.timeEnd("resolveCompoundParents"); + return compounds; } export function resolveChanges( chords: ChordMeta[], + inputs: Map, deviceChords: CharaChordFile["chords"], -): CharaChordFile["chords"] { +): [CharaChordFile["chords"], Map] { console.time("resolveChanges"); const removed: CharaChordFile["chords"] = []; - const info = new Map(); + const exact = new Map(); for (const chord of chords) { if (chord.input && chord.phrase && !chord.disabled) { - info.set( + exact.set( JSON.stringify([chord.input.value, chord.phrase?.value ?? []]), chord, ); - info.set(JSON.stringify(chord.input.value), chord); } } for (const deviceChord of deviceChords) { - const exact = info.get(JSON.stringify(deviceChord)); - if (exact) { - exact.phrase!.originalValue = exact.phrase!.value; + const exactMatch = exact.get(JSON.stringify(deviceChord)); + if (exactMatch) { + exactMatch.phrase!.originalValue = exactMatch.phrase!.value; continue; } - const byInput = info.get(JSON.stringify(deviceChord[0])); + const byInput = inputs.get(JSON.stringify(deviceChord[0])); if (byInput) { byInput.phrase!.originalValue = deviceChord[1]; continue; @@ -255,54 +239,25 @@ export function resolveChanges( } console.timeEnd("resolveChanges"); - return removed; + return [removed, exact]; } export function parseCharaChords( - data: EditorState, + tree: Tree, ids: Map, codes: Map, deviceChords: CharaChordFile["chords"], + sliceString: (from: number, to: number) => string, ): ParseResult { console.time("parseTotal"); - const chords = parseChordMeta(data, ids, codes); - resolveChordOverrides(chords); - resolveChordAliases(chords); - resolveCompoundParents(chords); - const removed = resolveChanges(chords, deviceChords); - - /*for (let i = 0; i < metas.length; i++) { - const [, compound] = splitCompound(chords[i]![0]); - if ( - compound !== undefined && - (!compoundInputs.has(compound) || orphanCompounds.has(compound)) - ) { - metas[i]!.orphan = true; - } - } - - const removed: CharaChordFile["chords"] = []; - for (let deviceChord of deviceChords) { - const key = JSON.stringify(deviceChord[0]); - if (!keys.has(key)) { - removed.push(deviceChord); - } else { - const index = keys.get(key)!; - const meta = metas[index]!; - if ( - JSON.stringify(deviceChord[1]) !== - JSON.stringify(chords[keys.get(key)!]![1]) - ) { - meta.originalPhrase = deviceChord[1]; - } else { - meta.unchanged = true; - } - } - }*/ + const chords = parseChordMeta(tree, ids, codes, sliceString); + const inputs = resolveChordOverrides(chords); + const aliases = resolveChordAliases(chords); + const compounds = resolveCompoundParents(chords); + const [removed, exact] = resolveChanges(chords, inputs, deviceChords); console.timeEnd("parseTotal"); - console.log(chords); - return { chords, removed }; + return { chords, removed, aliases, compounds, inputs, exact }; } diff --git a/src/lib/chord-editor/changes-decorations-plugin.ts b/src/lib/chord-editor/changes-decorations-plugin.ts deleted file mode 100644 index e5e7ac74..00000000 --- a/src/lib/chord-editor/changes-decorations-plugin.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { EditorView, Decoration, type DecorationSet } from "@codemirror/view"; -import { StateField } from "@codemirror/state"; -import { parsedChordsEffect } from "./parsed-chords-plugin"; - -const changedMark = Decoration.mark({ class: "cm-changed" }); - -const chordMetaMark = StateField.define({ - create() { - return Decoration.none; - }, - update(decorations, tr) { - const newChords = tr.effects.findLast((e) => e.is(parsedChordsEffect)); - if (!newChords) { - return decorations.map(tr.changes); - } - return newChords.value.meta.map(meta => { - if (meta.originalPhrase) { - return underlineMark.range(meta.from, meta.to); -} -}); - }, - provide: (f) => EditorView.decorations.from(f), -}); diff --git a/src/lib/chord-editor/changes-panel.svelte.ts b/src/lib/chord-editor/changes-panel.svelte.ts new file mode 100644 index 00000000..9c34ed25 --- /dev/null +++ b/src/lib/chord-editor/changes-panel.svelte.ts @@ -0,0 +1,44 @@ +import { EditorView, showPanel, type Panel } from "@codemirror/view"; +import { parsedChordsField } from "./parsed-chords-plugin"; +import { mount, unmount } from "svelte"; +import ChangesPanel from "./ChangesPanel.svelte"; + +function changesPanelFunc(view: EditorView): Panel { + let dom = document.createElement("div"); + dom.style.display = "contents"; + let viewState = $state.raw(view); + let parsed = $state.raw(view.state.field(parsedChordsField)); + let component: {}; + return { + dom, + mount() { + component = mount(ChangesPanel, { + target: dom, + props: { + get parsed() { + return parsed; + }, + get view() { + return viewState; + }, + }, + }); + }, + update: (update) => { + if ( + update.startState.field(parsedChordsField) !== + update.state.field(parsedChordsField) + ) { + console.log("update changes panel"); + parsed = update.state.field(parsedChordsField); + } + }, + destroy() { + unmount(component); + }, + }; +} + +export function changesPanel() { + return showPanel.of(changesPanelFunc); +} diff --git a/src/lib/chord-editor/changes-panel.ts b/src/lib/chord-editor/changes-panel.ts deleted file mode 100644 index cf97f14c..00000000 --- a/src/lib/chord-editor/changes-panel.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { EditorState } from "@codemirror/state"; -import { EditorView, showPanel, type Panel } from "@codemirror/view"; -import { parsedChordsField } from "./parsed-chords-plugin"; - -function getChanges(state: EditorState): string { - const parsed = state.field(parsedChordsField); - const added = parsed.chords.reduce( - (acc, chord) => - acc + (chord.phrase && chord.phrase.originalValue === undefined ? 1 : 0), - 0, - ); - const changed = parsed.chords.reduce( - (acc, chord) => - acc + - (chord.phrase && - chord.phrase.originalValue && - chord.phrase.originalValue !== chord.phrase.value - ? 1 - : 0), - 0, - ); - const removed = parsed.removed.length; - return `+${added} ~${changed} -${removed} (${parsed.chords.length} total)`; -} - -function wordCountPanel(view: EditorView): Panel { - let dom = document.createElement("div"); - dom.textContent = getChanges(view.state); - return { - dom, - update(update) { - dom.textContent = getChanges(update.state); - }, - }; -} - -export function changesPanel() { - return showPanel.of(wordCountPanel); -} diff --git a/src/lib/chord-editor/chord-sync-plugin.ts b/src/lib/chord-editor/chord-sync-plugin.ts index d0a110dc..7406d79e 100644 --- a/src/lib/chord-editor/chord-sync-plugin.ts +++ b/src/lib/chord-editor/chord-sync-plugin.ts @@ -1,28 +1,40 @@ import type { CharaChordFile } from "$lib/share/chara-file"; import { StateEffect, StateField } from "@codemirror/state"; +import { actionMetaPlugin } from "./action-meta-plugin"; +import { syncCharaChords } from "./chord-sync"; +import type { EditorView } from "@codemirror/view"; -export const chordSyncEffect = StateEffect.define(); +const chordSyncEffect = StateEffect.define(); + +export function editorSyncChords( + view: EditorView, + newDeviceChords: CharaChordFile["chords"], +) { + const { ids, codes } = view.state.field(actionMetaPlugin.field); + const oldDeviceChords = view.state.field(deviceChordField); + const changes = syncCharaChords( + oldDeviceChords, + newDeviceChords, + ids, + codes, + view.state.doc.toString(), + ); + view.dispatch({ + effects: chordSyncEffect.of(newDeviceChords), + changes, + }); +} export const deviceChordField = StateField.define({ create() { return []; }, update(value, transaction) { - // save initial device chords - // compare new device chords with initial device chords - // take changed/new/removed chords - // compare current editor chords with initial device chords - // compare two change sets - // apply removals if the chord didn't change on either end - // apply return ( transaction.effects.findLast((it) => it.is(chordSyncEffect))?.value ?? value ); }, - compare(a, b) { - return JSON.stringify(a) === JSON.stringify(b); - }, toJSON(value) { return value; }, diff --git a/src/lib/chord-editor/chord-sync.spec.ts b/src/lib/chord-editor/chord-sync.spec.ts new file mode 100644 index 00000000..f739655d --- /dev/null +++ b/src/lib/chord-editor/chord-sync.spec.ts @@ -0,0 +1,135 @@ +import type { KeyInfo } from "$lib/serial/keymap-codes"; +import type { CharaChordFile } from "$lib/share/chara-file"; +import { describe, it, expect } from "vitest"; +import { parseCharaChords } from "./action-serializer"; +import { parser } from "./chords.grammar"; +import { syncCharaChords } from "./chord-sync"; +import { Text } from "@codemirror/state"; + +const asciiInfo: KeyInfo[] = Array.from( + { length: 0x7f - 0x20 }, + (_, i) => + ({ + code: i + 0x20, + id: String.fromCharCode(i + 0x20), + }) satisfies KeyInfo, +); +const asciiCodes = new Map( + asciiInfo.map((info) => [info.code, info]), +); +const asciiIds = new Map( + asciiInfo.map((info) => [info.id!, info]), +); + +function chords(...strings: string[]): string { + return strings.join("\n"); +} + +function backup(doc: string): CharaChordFile["chords"] { + const tree = parser.parse(doc); + const result = parseCharaChords(tree, asciiIds, asciiCodes, [], (from, to) => + doc.slice(from, to), + ); + return result.chords + .filter((chord) => !chord.disabled) + .map((chord) => [chord.input?.value ?? [], chord.phrase?.value ?? []]); +} + +function expectSync(options: { + org: string[]; + mod: string[]; + cur: string[]; + exp: string[]; +}) { + expect( + syncCharaChords( + backup(chords(...options.org)), + backup(chords(...options.mod)), + asciiIds, + asciiCodes, + chords(...options.cur), + ) + .apply(Text.of(options.cur)) + .toString() + .replace(/\n$/, ""), + ).toEqual(chords(...options.exp)); +} + +describe("chord sync", function () { + it("should not do anything when no changes happened", function () { + expectSync({ + org: ["abc=>def", "def=>ghi", "jkl=>mno"], + mod: ["abc=>def", "def=>ghi", "jkl=>mno"], + cur: ["abc=>def", "def=>ghi", "jkl=>mno"], + exp: ["abc=>def", "def=>ghi", "jkl=>mno"], + }); + }); + + it("should not touch the doc if device chords are unchanged", function () { + expectSync({ + org: ["abc=>def", "def=>ghi", "jkl=>mno"], + mod: ["abc=>def", "def=>ghi", "jkl=>mno"], + cur: ["ab=>def", "def=>gh"], + exp: ["ab=>def", "def=>gh"], + }); + }); + + it("should apply removals to unchanged chords only", function () { + expectSync({ + org: ["abc=>def", "def=>ghi", "jkl=>mno", "mno=>pqr"], + mod: ["abc=>def"], + cur: ["abc=>def", "def=>ghij", "jkl=>mno", "mno=>pqr"], + exp: ["abc=>def", "def=>ghij"], + }); + }); + + it("should keep user modifications over device modifications", function () { + expectSync({ + org: ["abc=>def", "def=>ghi", "jkl=>mno", "mno=>pqr"], + mod: ["abc=>def", "def=>ghijk", "jkl=>mnop", "mno=>pqr"], + cur: ["abc=>def", "def=>ghij", "jkl=>mno", "mno=>pqr"], + exp: ["abc=>def", "def=>ghij", "jkl=>mnop", "mno=>pqr"], + }); + }); + + it("should handle complex changes", function () { + expectSync({ + org: [ + "unchanged=>unchanged", + "usermod=>usermod", + "devmod=>devmod", + "userremoval=>userremoval", + "devremoval=>devremoval", + "devremusermod=>devremusermod", + ], + mod: [ + "unchanged=>unchanged", + "devadd=>devadd", + "usermod=>usermod", + "userremoval=>userremoval", + "devmod=>devmod1", + "sameadd=>sameadd", + ], + cur: [ + "useradd1=>useradd1", + "unchanged=>unchanged", + "usermod=>use", + "devremusermod=>xyz", + "devmod=>devmod", + "sameadd=>sameadd", + "devremoval=>devremoval", + "useradd=>useradd", + ], + exp: [ + "devadd=>devadd", + "useradd1=>useradd1", + "unchanged=>unchanged", + "usermod=>use", + "devremusermod=>xyz", + "devmod=>devmod1", + "sameadd=>sameadd", + "useradd=>useradd", + ], + }); + }); +}); diff --git a/src/lib/chord-editor/chord-sync.ts b/src/lib/chord-editor/chord-sync.ts new file mode 100644 index 00000000..8446dfa8 --- /dev/null +++ b/src/lib/chord-editor/chord-sync.ts @@ -0,0 +1,130 @@ +import type { KeyInfo } from "$lib/serial/keymap-codes"; +import { ChangeSet, type ChangeSpec } from "@codemirror/state"; +import { parseCharaChords } from "./action-serializer"; +import { parser } from "./chords.grammar"; +import type { CharaChordFile } from "$lib/share/chara-file"; +import { splitCompound } from "$lib/serial/chord"; + +function canUseIdAsString(info: KeyInfo): boolean { + return !!info.id && /^[^>\n]+$/.test(info.id); +} + +export function actionToValue(code: number, info?: KeyInfo) { + if (info && info.id?.length === 1) + return /^[<>|\\\s]$/.test(info.id) ? `\\${info.id}` : info.id; + if (!info || !canUseIdAsString(info)) + return `<0x${code.toString(16).padStart(2, "0")}>`; + return `<${info.id}>`; +} + +function canonicalInputSorting(input: number[], phrase: number[]): number[] { + const tail = [...input]; + const prefix = phrase.filter((code) => { + const index = tail.indexOf(code); + if (index !== -1) { + tail.splice(index, 1); + return true; + } + return false; + }); + return [...prefix, ...tail]; +} + +export interface ChangeType { + from: number; + to: number; + insert: string; +} + +export function syncCharaChords( + originalDeviceChords: CharaChordFile["chords"], + newDeviceChords: CharaChordFile["chords"], + ids: Map, + codes: Map, + doc: string, +): ChangeSet { + const tree = parser.parse(doc); + const result = parseCharaChords( + tree, + ids, + codes, + originalDeviceChords, + (from, to) => doc.slice(from, to), + ); + + const exactChords = new Map(); + for (const chord of originalDeviceChords) { + const key = JSON.stringify(chord); + const count = exactChords.get(key) ?? 0; + exactChords.set(key, count + 1); + } + + const changes: ChangeType[] = []; + + const inputModified = new Set(); + for (const chord of newDeviceChords) { + const key = JSON.stringify(chord); + const count = exactChords.get(key) ?? 0; + if (count > 0) { + exactChords.set(key, count - 1); + continue; + } + + const inputKey = JSON.stringify(chord[0]); + inputModified.add(inputKey); + const byInput = result.inputs.get(inputKey); + if (byInput) { + if ( + byInput.phrase?.originalValue && + byInput.phrase.originalValue === byInput.phrase.value + ) { + changes.push({ + from: byInput.phrase.range[0], + to: byInput.phrase.range[1], + insert: chord[1] + .map((code) => actionToValue(code, codes.get(code))) + .join(""), + }); + } + } else { + const [inputs, compound] = splitCompound(chord[0]); + const sortedInput = canonicalInputSorting(inputs, chord[1]); + changes.push({ + from: 0, + to: 0, + insert: + (compound ? `|0x${compound.toString(16)}|` : "") + + sortedInput + .map((code) => actionToValue(code, codes.get(code))) + .join("") + + "=>" + + chord[1] + .map((code) => actionToValue(code, codes.get(code))) + .join("") + + "\n", + }); + } + } + + changes.push( + ...exactChords + .entries() + .filter(([, count]) => count > 0) + .map(([key]) => result.exact.get(key)) + .filter((chord) => chord !== undefined) + .filter( + (chord) => + chord.input && !inputModified.has(JSON.stringify(chord.input.value)), + ) + .map( + (chord) => + ({ + from: chord.range[0], + to: chord.range[1], + insert: "", + }) satisfies ChangeSpec, + ), + ); + + return ChangeSet.of(changes, doc.length); +} diff --git a/src/lib/chord-editor/parse-meta.ts b/src/lib/chord-editor/parse-meta.ts index ac21336d..3f503113 100644 --- a/src/lib/chord-editor/parse-meta.ts +++ b/src/lib/chord-editor/parse-meta.ts @@ -132,6 +132,10 @@ export function mapChordMeta(chord: ChordMeta, change: ChangeDesc): ChordMeta { export interface ParseResult { chords: ChordMeta[]; removed: CharaChordFile["chords"]; + aliases: Map; + compounds: Map; + inputs: Map; + exact: Map; } export function mapParseResult( diff --git a/src/lib/chord-editor/parsed-chords-plugin.ts b/src/lib/chord-editor/parsed-chords-plugin.ts index 2a3160f6..9e900443 100644 --- a/src/lib/chord-editor/parsed-chords-plugin.ts +++ b/src/lib/chord-editor/parsed-chords-plugin.ts @@ -10,6 +10,10 @@ export const parsedChordsField = StateField.define({ return { chords: [], removed: [], + aliases: new Map(), + compounds: new Map(), + inputs: new Map(), + exact: new Map(), }; }, update(value, transaction) { @@ -23,7 +27,13 @@ export const parsedChordsField = StateField.define({ codes !== transaction.startState.field(actionMetaPlugin.field).codes || deviceChords !== transaction.startState.field(deviceChordField) ) { - return parseCharaChords(transaction.state, ids, codes, deviceChords); + return parseCharaChords( + syntaxTree(transaction.state), + ids, + codes, + deviceChords, + (from, to) => transaction.state.doc.sliceString(from, to), + ); } return mapParseResult(value, transaction.changes); }, diff --git a/src/lib/chord-editor/persistent-state-plugin.ts b/src/lib/chord-editor/persistent-state-plugin.ts index 15db9849..1af0660a 100644 --- a/src/lib/chord-editor/persistent-state-plugin.ts +++ b/src/lib/chord-editor/persistent-state-plugin.ts @@ -27,11 +27,8 @@ import { syntaxHighlighting } from "@codemirror/language"; import { deviceChordField } from "./chord-sync-plugin"; import { actionMetaPlugin } from "./action-meta-plugin"; import { parsedChordsField } from "./parsed-chords-plugin"; -import { changesPanel } from "./changes-panel"; -import { - parseCompressed, - stringifyCompressed, -} from "$lib/serial/serialization"; +import { changesPanel } from "./changes-panel.svelte"; +import { searchKeymap } from "@codemirror/search"; const serializedFields = { history: historyField, @@ -44,11 +41,8 @@ export interface EditorConfig { autocomplete(query: string | undefined): void; } -export async function loadPersistentState( - params: EditorConfig, -): Promise { - const stored = localStorage.getItem(params.storeName); - const config = { +export function createConfig(params: EditorConfig) { + return { extensions: [ actionMetaPlugin.plugin, deviceChordField, @@ -86,14 +80,20 @@ export async function loadPersistentState( borderColor: "var(--md-sys-color-on-surface)", }, }), - keymap.of([...standardKeymap, ...historyKeymap]), + keymap.of([...standardKeymap, ...historyKeymap, ...searchKeymap]), ], } satisfies EditorStateConfig; +} + +export async function loadPersistentState( + params: EditorConfig, +): Promise { + const stored = await getState(params.storeName); + const config = createConfig(params); if (stored) { try { - const parsed = await parseCompressed(new Blob([stored])); - return EditorState.fromJSON(parsed, config, serializedFields); + return EditorState.fromJSON(stored, config, serializedFields); } catch (e) { console.error("Failed to parse persistent state:", e); } @@ -109,13 +109,10 @@ export function persistentStatePlugin(storeName: string) { .pipe( debounceTime(500), mergeMap(() => - stringifyCompressed(this.view.state.toJSON(serializedFields)), + storeState(storeName, this.view.state.toJSON(serializedFields)), ), - mergeMap((blob) => blob.text()), ) - .subscribe((value) => { - localStorage.setItem(storeName, value); - }); + .subscribe(() => {}); constructor(readonly view: EditorView) {} @@ -131,3 +128,58 @@ export function persistentStatePlugin(storeName: string) { }, ); } + +const dbName = "chord-state"; +const dbVersion = 1; +const storeName = "state"; + +async function openDb(): Promise { + const dbRequest = indexedDB.open(dbName, dbVersion); + return new Promise((resolve, reject) => { + dbRequest.onsuccess = () => resolve(dbRequest.result); + dbRequest.onerror = () => reject(dbRequest.error); + dbRequest.onupgradeneeded = () => { + const db = dbRequest.result; + if (!db.objectStoreNames.contains(storeName)) { + db.createObjectStore(storeName); + } + }; + }); +} + +async function getState(name: string): Promise { + const db = await openDb(); + try { + const readTransaction = db.transaction([storeName], "readonly"); + const store = readTransaction.objectStore(storeName); + const itemRequest = store.get(name); + const result = await new Promise((resolve) => { + itemRequest.onsuccess = () => resolve(itemRequest.result); + itemRequest.onerror = () => resolve(undefined); + }); + return result; + } catch (e) { + console.error(e); + return undefined; + } finally { + db.close(); + } +} + +async function storeState(name: string, state: T): Promise { + const db = await openDb(); + try { + const putTransaction = db.transaction([storeName], "readwrite"); + const putStore = putTransaction.objectStore(storeName); + const putRequest = putStore.put(state, name); + await new Promise((resolve, reject) => { + putRequest.onsuccess = () => resolve(); + putRequest.onerror = () => reject(putRequest.error); + }); + putTransaction.commit(); + } catch (e) { + console.error(e); + } finally { + db.close(); + } +} diff --git a/src/lib/chord-editor/save-chords.ts b/src/lib/chord-editor/save-chords.ts new file mode 100644 index 00000000..efc82a56 --- /dev/null +++ b/src/lib/chord-editor/save-chords.ts @@ -0,0 +1,58 @@ +import type { EditorView } from "@codemirror/view"; +import { parser } from "./chords.grammar"; +import { parseCharaChords } from "./action-serializer"; +import { actionMetaPlugin } from "./action-meta-plugin"; +import { deviceChordField } from "./chord-sync-plugin"; +import type { CharaChordFile } from "$lib/share/chara-file"; + +export interface SaveChordsTask { + remove: number[][]; + set: [number[], number[]][]; +} + +export function createSaveTask(view: EditorView): SaveChordsTask { + const tree = parser.parse(view.state.doc.toString()); + const { ids, codes } = view.state.field(actionMetaPlugin.field); + const deviceChords = view.state.field(deviceChordField); + const result = parseCharaChords(tree, ids, codes, deviceChords, (from, to) => + view.state.doc.sliceString(from, to), + ); + + return { + remove: result.removed.map((chord) => chord[0]), + set: result.chords + .filter( + (chord) => + !chord.disabled && + (!chord.phrase || + chord.phrase?.originalValue !== chord.phrase?.value), + ) + .map((chord) => [chord.input?.value ?? [], chord.phrase?.value ?? []]), + }; +} + +export function applySaveTask( + backup: CharaChordFile["chords"], + task: SaveChordsTask, +): CharaChordFile["chords"] { + const newBackup = [...backup]; + for (const input of task.remove) { + const index = newBackup.findIndex((chord) => { + return JSON.stringify(chord[0]) === JSON.stringify(input); + }); + if (index !== -1) { + newBackup.splice(index, 1); + } + } + for (const [input, phrase] of task.set) { + const index = newBackup.findIndex((chord) => { + return JSON.stringify(chord[0]) === JSON.stringify(input); + }); + if (index !== -1) { + newBackup[index] = [input, phrase]; + } else { + newBackup.push([input, phrase]); + } + } + return newBackup; +} diff --git a/src/lib/components/layout/ActionList.svelte b/src/lib/components/layout/ActionList.svelte index ad0149d7..03905e5c 100644 --- a/src/lib/components/layout/ActionList.svelte +++ b/src/lib/components/layout/ActionList.svelte @@ -14,7 +14,7 @@ import type { KeymapCategory } from "$lib/meta/types/actions"; import Action from "../Action.svelte"; import { isVerbose } from "../verbose-action"; - import { actionToValue } from "$lib/chord-editor/action-serializer"; + import { actionToValue } from "$lib/chord-editor/chord-sync"; let { currentAction = undefined, diff --git a/src/lib/learn/chords.ts b/src/lib/learn/chords.ts deleted file mode 100644 index d191eb92..00000000 --- a/src/lib/learn/chords.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { osLayout } from "$lib/os-layout"; -import { KEYMAP_CODES } from "$lib/serial/keymap-codes"; -import { persistentWritable } from "$lib/storage"; -import { type ChordInfo, chords } from "$lib/undo-redo"; -import { derived } from "svelte/store"; - -export const words = derived( - [chords, osLayout, KEYMAP_CODES], - ([chords, layout, KEYMAP_CODES]) => - new Map( - chords - .map((chord) => ({ - chord, - output: chord.phrase.map((action) => - layout.get(KEYMAP_CODES.get(action)?.keyCode ?? ""), - ), - })) - .filter(({ output }) => output.every((it) => !!it)) - .map(({ chord, output }) => [output.join("").trim(), chord] as const), - ), -); - -interface Score { - lastTyped: number; - score: number; - total: number; -} - -export const scores = persistentWritable>("scores", {}); - -export const learnConfigDefault = { - maxScore: 3, - minScore: -3, - scoreBlend: 0.5, - weakRate: 0.8, - weakBoost: 0.5, - maxWeak: 3, - newRate: 0.3, - initialNewRate: 0.9, - initialCount: 10, -}; -export const learnConfigStored = persistentWritable< - Partial ->("learn-config", {}); -export const learnConfig = derived(learnConfigStored, (config) => ({ - ...learnConfigDefault, - ...config, -})); - -let lastWord: string | undefined; - -function shuffle(array: T[]): T[] { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j]!, array[i]!]; - } - return array; -} - -function randomLog2(array: T[], max = array.length): T | undefined { - return array[ - Math.floor(Math.pow(2, Math.log2(Math.random() * Math.log2(max)))) - ]; -} - -export const nextWord = derived( - [words, scores, learnConfig], - ([words, scores, config]) => { - const values = Object.entries(scores).filter(([it]) => it !== lastWord); - - values.sort(([, a], [, b]) => a.score - b.score); - const weakCount = - (values.findIndex(([, { score }]) => score > 0) + 1 || - values.length + 1) - 1; - const weak = randomLog2(values, weakCount); - if (weak && Math.random() / weakCount < config.weakRate) { - lastWord = weak[0]; - return weak[0]; - } - - values.sort(([, { lastTyped: a }], [, { lastTyped: b }]) => a - b); - const recent = randomLog2(values); - const newRate = - values.length < config.initialCount - ? config.initialNewRate - : config.newRate; - if ( - recent && - (Math.random() < Math.min(1, Math.max(0, weakCount / config.maxWeak)) || - Math.random() > newRate) - ) { - lastWord = recent[0]; - return recent[0]; - } - - const newWord = shuffle(Array.from(words.keys())).find((it) => !scores[it]); - const word = newWord || recent?.[0] || weak?.[0]; - lastWord = word; - return word; - }, -); diff --git a/src/lib/learn/stats.ts b/src/lib/learn/stats.ts deleted file mode 100644 index 05df68a2..00000000 --- a/src/lib/learn/stats.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { persistentWritable } from "$lib/storage"; - -interface ChordStats { - level: number; - lastUprank: number; -} - -export const chordStats = persistentWritable>( - "chord-stats", - {}, -); diff --git a/src/lib/undo-redo.ts b/src/lib/undo-redo.ts index cfbf743a..e8958246 100644 --- a/src/lib/undo-redo.ts +++ b/src/lib/undo-redo.ts @@ -1,16 +1,9 @@ import { persistentWritable } from "$lib/storage"; import { derived } from "svelte/store"; -import { hashChord, type Chord } from "$lib/serial/chord"; -import { - deviceChords, - deviceLayout, - deviceSettings, -} from "$lib/serial/connection"; -import { KEYMAP_CODES } from "$lib/serial/keymap-codes"; +import { deviceLayout, deviceSettings } from "$lib/serial/connection"; export enum ChangeType { Layout, - Chord, Setting, } @@ -22,14 +15,6 @@ export interface LayoutChange { profile?: number; } -export interface ChordChange { - type: ChangeType.Chord; - deleted?: true; - id: number[]; - actions: number[]; - phrase: number[]; -} - export interface SettingChange { type: ChangeType.Setting; id: number; @@ -42,20 +27,18 @@ export interface ChangeInfo { isCommitted?: boolean; } -export type Change = LayoutChange | ChordChange | SettingChange; +export type Change = LayoutChange | SettingChange; export const changes = persistentWritable("changes", []); export interface Overlay { layout: Array | undefined> | undefined>; - chords: Map; settings: Array | undefined>; } export const overlay = derived(changes, (changes) => { const overlay: Overlay = { layout: [], - chords: new Map(), settings: [], }; @@ -71,13 +54,6 @@ export const overlay = derived(changes, (changes) => { change.action, ); break; - case ChangeType.Chord: - overlay.chords.set(JSON.stringify(change.id), { - actions: change.actions, - phrase: change.phrase, - deleted: change.deleted ?? false, - }); - break; case ChangeType.Setting: change.profile ??= 0; overlay.settings[change.profile] ??= new Map(); @@ -113,90 +89,3 @@ export const layout = derived([overlay, deviceLayout], ([overlay, profiles]) => ), ), ); - -export type ChordInfo = Chord & - ChangeInfo & { - phraseChanged: boolean; - actionsChanged: boolean; - sortBy: string; - } & { - id: number[]; - deleted: boolean; - }; -export const chords = derived( - [overlay, deviceChords, KEYMAP_CODES], - ([overlay, chords, codes]) => { - const newChords = new Set(overlay.chords.keys()); - - const changedChords = chords.map((chord) => { - const id = JSON.stringify(chord.actions); - if (overlay.chords.has(id)) { - newChords.delete(id); - const changedChord = overlay.chords.get(id)!; - return { - id: chord.actions, - // use the old phrase for stable editing - sortBy: chord.phrase.map((it) => codes.get(it)?.id ?? it).join(), - actions: changedChord.actions, - phrase: changedChord.phrase, - actionsChanged: id !== JSON.stringify(changedChord.actions), - phraseChanged: - JSON.stringify(chord.phrase) !== - JSON.stringify(changedChord.phrase), - isApplied: false, - deleted: changedChord.deleted, - }; - } else { - return { - id: chord.actions, - sortBy: chord.phrase.map((it) => codes.get(it)?.id ?? it).join(), - actions: chord.actions, - phrase: chord.phrase, - phraseChanged: false, - actionsChanged: false, - isApplied: true, - deleted: false, - }; - } - }); - for (const id of newChords) { - const chord = overlay.chords.get(id)!; - changedChords.push({ - sortBy: "", - isApplied: false, - actionsChanged: true, - phraseChanged: false, - deleted: chord.deleted, - id: JSON.parse(id), - phrase: chord.phrase, - actions: chord.actions, - }); - } - - return changedChords.sort(({ sortBy: a }, { sortBy: b }) => - a.localeCompare(b), - ); - }, -); - -export const duplicateChords = derived(chords, (chords) => { - const duplicates = new Set(); - const seen = new Set(); - - for (const chord of chords) { - const key = JSON.stringify(chord.actions); - if (seen.has(key)) { - duplicates.add(key); - } else { - seen.add(key); - } - } - - return duplicates; -}); - -export const chordHashes = derived( - chords, - (chords) => - new Map(chords.map((chord) => [hashChord(chord.actions), chord] as const)), -); diff --git a/src/routes/(app)/PageTransition.svelte b/src/routes/(app)/PageTransition.svelte index 840a94d6..477a341c 100644 --- a/src/routes/(app)/PageTransition.svelte +++ b/src/routes/(app)/PageTransition.svelte @@ -13,14 +13,7 @@ let isNavigating = $state(false); - const routeOrder = [ - "/config", - "/learn", - "/docs", - "/editor", - "/chat", - "/plugin", - ]; + const routeOrder = ["/config", "/docs", "/editor", "/chat", "/plugin"]; function routeIndex(route: string | undefined): number { return routeOrder.findIndex((it) => route?.startsWith(it)); diff --git a/src/routes/(app)/Sidebar.svelte b/src/routes/(app)/Sidebar.svelte index bf3cc62b..d62a4e35 100644 --- a/src/routes/(app)/Sidebar.svelte +++ b/src/routes/(app)/Sidebar.svelte @@ -39,7 +39,12 @@ [ { href: "/editor", icon: "edit_document", title: "Editor", wip: true }, { href: "/chat", icon: "chat", title: "Chat", wip: true }, - { href: "/learn", icon: "school", title: "Learn", wip: true }, + { + href: "https://adventure.charachorder.io/", + icon: "school", + title: "Learn", + external: true, + }, ], /*[ { href: "/plugin", icon: "code", title: "Plugin", wip: true }, diff --git a/src/routes/(app)/config/EditActions.svelte b/src/routes/(app)/config/EditActions.svelte index dc3efeba..58d31785 100644 --- a/src/routes/(app)/config/EditActions.svelte +++ b/src/routes/(app)/config/EditActions.svelte @@ -6,18 +6,15 @@ layout, overlay, settings, - duplicateChords, } from "$lib/undo-redo"; - import type { Change, ChordChange } from "$lib/undo-redo"; + import type { Change } from "$lib/undo-redo"; import { fly } from "svelte/transition"; import { actionTooltip } from "$lib/title"; import { - deviceChords, deviceLayout, deviceSettings, serialLog, serialPort, - sync, syncProgress, syncStatus, } from "$lib/serial/connection"; @@ -106,115 +103,7 @@ return true; } - async function safeDeleteChord(actions: number[]): Promise { - const port = $serialPort; - if (!port) return false; - try { - await port.deleteChord({ actions }); - return true; - } catch (e) { - console.error(e); - try { - if ((await port.getChordPhrase(actions)) === undefined) { - return true; - } - } catch (e) { - console.error(e); - } - } - return false; - } - - async function saveChords(progress: () => void): Promise { - const port = $serialPort; - if (!port) return false; - let ok = true; - - const empty = new Set(); - for (const [id, chord] of $overlay.chords) { - if (chord.actions.length === 0 || chord.phrase.length === 0) { - empty.add(id); - } - } - changes.update((changes) => { - changes.push([ - ...empty.keys().map( - (id) => - ({ - type: ChangeType.Chord, - id: JSON.parse(id), - deleted: true, - actions: [], - phrase: [], - }) satisfies ChordChange, - ), - ]); - return changes; - }); - await tick(); - - const deleted = new Set(); - const changed = new Map(); - for (const [id, chord] of $overlay.chords) { - if (!chord.deleted) continue; - if (await safeDeleteChord(JSON.parse(id))) { - deleted.add(id); - } else { - ok = false; - } - progress(); - } - deviceChords.update((chords) => - chords.filter((chord) => !deleted.has(JSON.stringify(chord.actions))), - ); - deleted.clear(); - await tick(); - - for (const [id, chord] of $overlay.chords) { - if (chord.deleted) continue; - if ($duplicateChords.has(JSON.stringify(chord.actions))) { - ok = false; - } else { - let skip = false; - if (id !== JSON.stringify(chord.actions)) { - if (await safeDeleteChord(JSON.parse(id))) { - deleted.add(id); - } else { - skip = true; - ok = false; - } - } - if (!skip) { - try { - await port.setChord({ - actions: chord.actions, - phrase: chord.phrase, - }); - deleted.add(JSON.stringify(chord.actions)); - changed.set(JSON.stringify(chord.actions), chord.phrase); - } catch (e) { - console.error(e); - ok = false; - } - } else { - ok = false; - } - } - progress(); - } - deviceChords.update((chords) => { - chords.filter((chord) => !deleted.has(JSON.stringify(chord.actions))); - for (const [id, phrase] of changed) { - chords.push({ actions: JSON.parse(id), phrase }); - } - return chords; - }); - await tick(); - return ok; - } - async function save() { - let needsSync = false; try { const port = $serialPort; if (!port) { @@ -235,10 +124,8 @@ (acc, profile) => acc + (profile?.size ?? 0), 0, ); - const chordChanges = $overlay.chords.size; - needsSync = chordChanges > 0; const needsCommit = settingChanges > 0 || layoutChanges > 0; - const progressMax = layoutChanges + settingChanges + chordChanges; + const progressMax = layoutChanges + settingChanges; let progressCurrent = 0; @@ -261,11 +148,9 @@ layoutSuccess = false; } } - let chordsSuccess = await saveChords(updateProgress); - if (layoutSuccess && settingsSuccess && chordsSuccess) { + if (layoutSuccess && settingsSuccess) { changes.set([]); - needsSync = true; } else { throw new Error("Some changes could not be saved."); } @@ -280,10 +165,6 @@ } finally { $syncStatus = "done"; } - - if (needsSync) { - await sync(); - } } let progressPopover: HTMLElement | undefined = $state(); diff --git a/src/routes/(app)/config/chords/+page.svelte b/src/routes/(app)/config/chords/+page.svelte index 5f65f8d1..fffaee8f 100644 --- a/src/routes/(app)/config/chords/+page.svelte +++ b/src/routes/(app)/config/chords/+page.svelte @@ -1,468 +1,278 @@ - - Chord Manager - CharaChorder Device Manager - - - -
    - $searchIndex && search($searchIndex, event)} - class:loading={progress !== $chords.length} - /> -
    - {#if $lastPage !== -1} - {page + 1} / {$lastPage + 1} - {:else} - - / - - {/if} +
    +
    + + + + + +
    - - -
    -
    - - {#await tick() then} -
    - - {#if $lastPage !== -1} - - {#if page === 0} - - {/if} - {#each $items.slice(page * $pageSize - (page === 0 ? 0 : 1), (page + 1) * $pageSize - 1) as [chord]} - {#if chord} - (page = 0)} /> - {/if} - {/each} - {:else} - - {/if} -
    insertChord(action)} - />
    {$LL.configure.chords.search.NO_RESULTS()}
    -
    - - {/await} -
    +
    +
    + +
    +
    diff --git a/src/routes/(app)/config/chords/ChordActionEdit.svelte b/src/routes/(app)/config/chords/ChordActionEdit.svelte deleted file mode 100644 index f1363de5..00000000 --- a/src/routes/(app)/config/chords/ChordActionEdit.svelte +++ /dev/null @@ -1,240 +0,0 @@ - - - - - diff --git a/src/routes/(app)/config/chords/ChordEdit.svelte b/src/routes/(app)/config/chords/ChordEdit.svelte deleted file mode 100644 index 032d2fa3..00000000 --- a/src/routes/(app)/config/chords/ChordEdit.svelte +++ /dev/null @@ -1,172 +0,0 @@ - - - - - {}} /> - - - - - -
    - {#if !chord.deleted} - - {:else} - - {/if} - - -
    - -
    - - - - diff --git a/src/routes/(app)/config/chords/ChordPhraseEdit.svelte b/src/routes/(app)/config/chords/ChordPhraseEdit.svelte deleted file mode 100644 index 8200962c..00000000 --- a/src/routes/(app)/config/chords/ChordPhraseEdit.svelte +++ /dev/null @@ -1,376 +0,0 @@ - - - - -
    { - box?.focus(); - }} -> - {#if supportsAutospace} - { - const autospace = hasAutospace; - if ((event.target as HTMLInputElement).checked) { - if (chord.phrase[0] === JOIN_ACTION) { - deleteAction(0, 1); - await tick(); - moveCursor(cursorPosition - 1, true); - } - } else { - if (chord.phrase[0] !== JOIN_ACTION) { - insertAction(0, JOIN_ACTION); - moveCursor(cursorPosition + 1, true); - } - } - await tick(); - resolveAutospace(autospace); - }} - /> - {/if} -
    (hasFocus = true)} - onfocusout={(event) => { - if (event.relatedTarget !== button) hasFocus = false; - }} - > - {#if hasFocus} -
    - -
    - {:else} -
    - - {/if} - {#each chord.phrase as action, i} - {#if isHidden(action, i, chord.phrase)} - - {:else} - - {/if} - {/each} -
    - {#if supportsAutospace} - - resolveAutospace((event.target as HTMLInputElement).checked)} - /> - {/if} - -
    - - diff --git a/src/routes/(app)/config/chords/action-selector.ts b/src/routes/(app)/config/chords/action-selector.ts deleted file mode 100644 index 51b8c41d..00000000 --- a/src/routes/(app)/config/chords/action-selector.ts +++ /dev/null @@ -1,56 +0,0 @@ -import ActionSelector from "$lib/components/layout/ActionSelector.svelte"; -import { mount, unmount, tick } from "svelte"; - -export function selectAction( - event: MouseEvent | KeyboardEvent, - select: (action: number) => void, - dismissed?: () => void, -) { - const component = mount(ActionSelector, { - target: document.body, - props: { - onclose: () => closed(), - onselect: (action: number) => { - select(action); - closed(); - }, - }, - }); - const dialog = document.querySelector("dialog > div") as HTMLDivElement; - const backdrop = document.querySelector("dialog") as HTMLDialogElement; - const dialogRect = dialog.getBoundingClientRect(); - const groupRect = (event.target as HTMLElement).getBoundingClientRect(); - - const scale = 0.5; - const dialogScale = `${ - 1 - scale * (1 - groupRect.width / dialogRect.width) - } ${1 - scale * (1 - groupRect.height / dialogRect.height)}`; - const dialogTranslate = `${scale * (groupRect.x - dialogRect.x)}px ${ - scale * (groupRect.y - dialogRect.y) - }px`; - - const duration = 150; - const options = { duration, easing: "ease" }; - const dialogAnimation = dialog.animate( - [ - { scale: dialogScale, translate: dialogTranslate }, - { translate: "0 0", scale: "1" }, - ], - options, - ); - const backdropAnimation = backdrop.animate( - [{ opacity: 0 }, { opacity: 1 }], - options, - ); - - async function closed() { - dialogAnimation.reverse(); - backdropAnimation.reverse(); - - await dialogAnimation.finished; - - unmount(component); - await tick(); - dismissed?.(); - } -} diff --git a/src/routes/(app)/config/chords/input-converter.ts b/src/routes/(app)/config/chords/input-converter.ts deleted file mode 100644 index 5619fae4..00000000 --- a/src/routes/(app)/config/chords/input-converter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { KEYMAP_IDS, KEYMAP_KEYCODES } from "$lib/serial/keymap-codes"; -import { get } from "svelte/store"; - -export function inputToAction( - event: KeyboardEvent, - useKeycodes?: boolean, -): number | undefined { - if (useKeycodes) { - return get(KEYMAP_KEYCODES).get(event.code); - } else { - return ( - get(KEYMAP_IDS).get(event.key)?.code ?? - get(KEYMAP_KEYCODES).get(event.code) - ); - } -} diff --git a/src/routes/(app)/config/chords/will-my-compound-break/+page.svelte b/src/routes/(app)/config/chords/will-my-compound-break/+page.svelte index a7e5b4a7..6ad0145f 100644 --- a/src/routes/(app)/config/chords/will-my-compound-break/+page.svelte +++ b/src/routes/(app)/config/chords/will-my-compound-break/+page.svelte @@ -1,7 +1,5 @@ @@ -35,7 +34,7 @@ >, your library might have been corrupted.

    {#each broken as chord} - {}} /> + {/each} {:else}

    No problematic chords found

    diff --git a/src/routes/(app)/config/cv2/+page.svelte b/src/routes/(app)/config/cv2/+page.svelte deleted file mode 100644 index aa167895..00000000 --- a/src/routes/(app)/config/cv2/+page.svelte +++ /dev/null @@ -1,356 +0,0 @@ - - -
    -
    - - - - - - - -
    - -
    -
    - -
    -
    - - diff --git a/src/routes/(app)/learn/+page.svelte b/src/routes/(app)/learn/+page.svelte deleted file mode 100644 index 0ed68878..00000000 --- a/src/routes/(app)/learn/+page.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - - - diff --git a/src/routes/(app)/learn/chords/+page.svelte b/src/routes/(app)/learn/chords/+page.svelte deleted file mode 100644 index bb009fe2..00000000 --- a/src/routes/(app)/learn/chords/+page.svelte +++ /dev/null @@ -1,232 +0,0 @@ - - -

    WIP

    - - - -{#key $nextWord} -

    - {$nextWord} - {#if $scores[$nextWord!] === undefined} - new - {:else if ($scores[$nextWord!]?.score ?? 0) < 0} - weak - {/if} -

    - -
    - - - -
    -{/key} - -{#key $nextWord} -
    - {}} /> -
    -{/key} - - -
    - - - - - - {#each Object.entries($scores) - .sort(([, a], [, b]) => a.score - b.score) - .splice(0, 10) as [word, score]} - - - - - {/each} - -
    Weak
    {word}{score.score.toFixed(2)}
    - - - - - - - {#each Object.entries($scores) - .sort(([, a], [, b]) => b.score - a.score) - .splice(0, 10) as [word, score]} - - - - - {/each} - -
    Strong
    {word}{score.score.toFixed(2)}
    - - - - - - - {#each Object.entries($scores) - .sort(([, a], [, b]) => b.lastTyped - a.lastTyped) - .splice(0, 10) as [word, _score]} - - - - {/each} - -
    Rehearse
    {word}
    -
    - -
    - Settings - - - - {#each Object.entries(learnConfigDefault) as [key, value]} - - - - - - {/each} - -
    {key} - ($learnConfigStored[key as keyof typeof $learnConfig] = ( - event.target as HTMLInputElement - ).value as any)} - /> - - -
    -
    - - diff --git a/src/routes/(app)/learn/layout/+page.svelte b/src/routes/(app)/learn/layout/+page.svelte deleted file mode 100644 index a495383d..00000000 --- a/src/routes/(app)/learn/layout/+page.svelte +++ /dev/null @@ -1,124 +0,0 @@ - - -
    -
    - -
    - - -
    - - diff --git a/src/routes/(app)/learn/sentence/+page.svelte b/src/routes/(app)/learn/sentence/+page.svelte deleted file mode 100644 index 629ed3ba..00000000 --- a/src/routes/(app)/learn/sentence/+page.svelte +++ /dev/null @@ -1,652 +0,0 @@ - - -
    -

    Sentence Trainer

    - - -
    - {#each masteryThresholds as [, , title], i} - - {/each} - - {#each masteryThresholds as _, i} -
    - {/each} -
    -
    -
    - {#each sentenceWords as _, i} - {#if i !== sentenceWords.length - 1} - {@const word = sentenceWords.slice(i, i + 2).join(" ")} - {@const mastery = wordMastery.get(word) ?? 0} -
    - {/if} - {/each} - {#each sentenceWords as word, i} - {@const mastery = wordMastery.get(word)} -
    - {word} -
    - {/each} - {#each sentenceWords as _, i} - {#if i < sentenceWords.length - 2} - {@const word = sentenceWords.slice(i, i + 3).join(" ")} - {@const mastery = wordMastery.get(word) ?? 0} -
    - {/if} - {/each} -
    - {#if level === masteryThresholds.length} - {@const maxDigits = 4} - {@const indices = Array.from({ length: maxDigits }, (_, i) => i)} - {@const wpmString = Math.floor(bestWPM).toString().padStart(maxDigits, " ")} -
    -
    - {#each indices as i} - {@const char = wpmString[i]} - {#key char} -
    - {char} -
    - {/key} - {/each} -
    - WPM -
    -
    -
    - {#key wpm} -
    - {Math.floor(wpm)} -
    - {/key} -
    WPM
    -
    -
    - {/if} - -
    -
    - {#key recorder} -
    - - - - -
    - {/key} -
    -
    - {#if devTools} -
    Dev Tools
    - - - - - - - - - - - - - - - - - -
    Total{Math.round(totalMs)}ms -
    Char{Math.round(msPerChar)}ms -
    Word{Math.round(msPerWord)}ms -
    - - - {#each masteryThresholds as _, i} - - - - - - - {/each} - -
    L{i + 1}
    - - - {#each wordStats.entries() as [word, stats]} - {@const mastery = wordMastery.get(word) ?? 0} - - - - {#each stats as stat} - - {/each} - - {/each} - -
    {word}{Math.round(mastery * 100)}% - {stat}
    - {/if} -
    - - diff --git a/src/routes/(app)/learn/sentence/configuration.ts b/src/routes/(app)/learn/sentence/configuration.ts deleted file mode 100644 index 2d932e90..00000000 --- a/src/routes/(app)/learn/sentence/configuration.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface PageParam { - key: string; - default: T; - parse?: (value: string) => T; -} - -export const SENTENCE_TRAINER_PAGE_PARAMS: { - sentence: PageParam; - wpm: PageParam; - showDevTools: PageParam; - textAreaDebounceInMillis: PageParam; -} = { - sentence: { - key: "sentence", - default: "This text has been typed at the speed of thought", - }, - wpm: { - key: "wpm", - default: 250, - parse: (value) => Number(value), - }, - showDevTools: { - key: "dev", - default: false, - parse: (value) => value === "true", - }, - textAreaDebounceInMillis: { - key: "debounceMillis", - default: 5000, - parse: (value) => Number(value), - }, -}; diff --git a/src/routes/(app)/learn/sentence/constants.ts b/src/routes/(app)/learn/sentence/constants.ts deleted file mode 100644 index faf41187..00000000 --- a/src/routes/(app)/learn/sentence/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Domain constants -export const AVG_WORD_LENGTH = 5; -export const SECONDS_IN_MINUTE = 60; -export const MILLIS_IN_SECOND = 1000; - -// Error messages. -export const TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE = - "The sentence is too short to make N-Grams, please enter longer sentence"; diff --git a/src/routes/(app)/learn/sentence/types.ts b/src/routes/(app)/learn/sentence/types.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/routes/(app)/learn/sentence/word-selector.spec.ts b/src/routes/(app)/learn/sentence/word-selector.spec.ts deleted file mode 100644 index 2e46a9f0..00000000 --- a/src/routes/(app)/learn/sentence/word-selector.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { describe, it, beforeEach, expect, vi } from "vitest"; -import { pickNextWord } from "./word-selector"; -import { untrack } from "svelte"; -import { SvelteMap } from "svelte/reactivity"; -import { TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE } from "./constants"; - -// Mock untrack so it simply executes the callback, allowing us to spy on its usage. -vi.mock("svelte", () => ({ - untrack: vi.fn((fn: any) => fn()), -})); - -describe("pickNextWord", () => { - let words: string[]; - let wordMastery: SvelteMap; - let currentWord: string; - - beforeEach(() => { - vi.clearAllMocks(); - - // Set up sample words and mastery values. - words = ["alpha", "beta", "gamma"]; - wordMastery = new SvelteMap(); - // For this test, assume none of the words are mastered. - words.forEach((word) => wordMastery.set(word, 0)); - currentWord = "alpha"; - }); - - it("should return a word different from current", () => { - // Force Math.random() to return a predictable value. - vi.spyOn(Math, "random").mockReturnValueOnce(0.3); - - const nextWord = pickNextWord(words, wordMastery, currentWord); - - // Since currentWord ("alpha") should be skipped, we expect next word. - expect(nextWord).toBe("beta"); - }); - - it("should randomly skip words", () => { - // Force Math.random() to return a predictable value. - vi.spyOn(Math, "random").mockReturnValueOnce(0.6).mockReturnValueOnce(0.3); - - const nextWord = pickNextWord(words, wordMastery, currentWord); - - // Since currentWord ("alpha") should be skipped as current - // and "beta" should be randomly skipped we expect "gamma". - expect(nextWord).toBe("gamma"); - }); - - it("should return current word if all other words were randomly skipped", () => { - // Force Math.random() to return a predictable value. - vi.spyOn(Math, "random").mockReturnValueOnce(0.6).mockReturnValueOnce(0.6); - - const nextWord = pickNextWord(words, wordMastery, currentWord); - - // Since all other words have been randomly skipped, we expect - // current word to be returned. - expect(nextWord).toBe("alpha"); - }); - - it("current word should be passed untracked", () => { - pickNextWord(words, wordMastery, currentWord); - expect(untrack).toHaveBeenCalledTimes(0); - }); - - it("should return TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE if the words array is empty", () => { - const result = pickNextWord([], wordMastery, currentWord); - expect(result).toBe(TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE); - }); -}); diff --git a/src/routes/(app)/learn/sentence/word-selector.ts b/src/routes/(app)/learn/sentence/word-selector.ts deleted file mode 100644 index 21603af7..00000000 --- a/src/routes/(app)/learn/sentence/word-selector.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE } from "./constants"; -import { SvelteMap } from "svelte/reactivity"; - -export function pickNextWord( - words: string[], - wordMastery: SvelteMap, - untrackedCurrentWord: string, -) { - const unmasteredWords = words - .map((it) => [it, wordMastery.get(it) ?? 0] as const) - .filter(([, it]) => it !== 1); - unmasteredWords.sort(([, a], [, b]) => a - b); - let nextWord = - unmasteredWords[0]?.[0] ?? - words[0] ?? - TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE; - // This is important to break infinite loop created by - // reading and writing `currentWord` inside $effect rune - for (const [word] of unmasteredWords) { - if (word === untrackedCurrentWord || Math.random() > 0.5) continue; - nextWord = word; - break; - } - return nextWord; -} diff --git a/vitest.config.ts b/vitest.config.ts index 6a77c909..35f27e69 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,10 @@ import { defineConfig } from "vitest/config"; -import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { sveltekit } from "@sveltejs/kit/vite"; +import { lezerGrammarPlugin } from "./vite-plugin-lezer"; +import { layoutPlugin } from "./vite-plugin-layout"; export default defineConfig({ - plugins: [svelte({ hot: !process.env.VITEST })], + plugins: [layoutPlugin(), sveltekit(), lezerGrammarPlugin()], test: { globals: true, environment: "jsdom",