From e6cc257aa2eb0aec33353d1883bd0848eeaaad39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Guillois?= Date: Tue, 26 May 2026 18:02:25 +0200 Subject: [PATCH 01/25] =?UTF-8?q?M9/#41:=20configure=20Vitest=20+=20store?= =?UTF-8?q?=20tests=20(auth,=20songs,=20sessions)=20=E2=80=94=2053=20tests?= =?UTF-8?q?,=2082.5%=20line=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lyremember-app/package.json | 9 +- lyremember-app/pnpm-lock.yaml | 2586 +++++++++++++++++ lyremember-app/src/components/LyricsGrid.vue | 74 + .../src/stores/__tests__/auth.spec.ts | 207 ++ .../src/stores/__tests__/sessions.spec.ts | 250 ++ .../src/stores/__tests__/songs.spec.ts | 272 ++ lyremember-app/src/views/SongDetailView.vue | 299 +- lyremember-app/vite.config.ts | 18 + 8 files changed, 3624 insertions(+), 91 deletions(-) create mode 100644 lyremember-app/pnpm-lock.yaml create mode 100644 lyremember-app/src/components/LyricsGrid.vue create mode 100644 lyremember-app/src/stores/__tests__/auth.spec.ts create mode 100644 lyremember-app/src/stores/__tests__/sessions.spec.ts create mode 100644 lyremember-app/src/stores/__tests__/songs.spec.ts diff --git a/lyremember-app/package.json b/lyremember-app/package.json index 57aa6c6..a6e9a4d 100644 --- a/lyremember-app/package.json +++ b/lyremember-app/package.json @@ -7,7 +7,10 @@ "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", - "tauri": "tauri" + "tauri": "tauri", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@tauri-apps/api": "^2", @@ -20,11 +23,15 @@ "devDependencies": { "@tauri-apps/cli": "^2", "@vitejs/plugin-vue": "^5.2.1", + "@vitest/coverage-v8": "^4.1.7", + "@vue/test-utils": "^2.4.10", "autoprefixer": "^10.4.24", + "happy-dom": "^20.9.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", "typescript": "~5.6.2", "vite": "^6.0.3", + "vitest": "^4.1.7", "vue-tsc": "^2.2.12" } } diff --git a/lyremember-app/pnpm-lock.yaml b/lyremember-app/pnpm-lock.yaml new file mode 100644 index 0000000..d555c34 --- /dev/null +++ b/lyremember-app/pnpm-lock.yaml @@ -0,0 +1,2586 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tauri-apps/api': + specifier: ^2 + version: 2.11.0 + '@tauri-apps/plugin-opener': + specifier: ^2 + version: 2.5.4 + lucide-vue-next: + specifier: ^0.574.0 + version: 0.574.0(vue@3.5.34(typescript@5.6.3)) + pinia: + specifier: ^3.0.4 + version: 3.0.4(typescript@5.6.3)(vue@3.5.34(typescript@5.6.3)) + vue: + specifier: ^3.5.13 + version: 3.5.34(typescript@5.6.3) + vue-router: + specifier: ^4.6.4 + version: 4.6.4(vue@3.5.34(typescript@5.6.3)) + devDependencies: + '@tauri-apps/cli': + specifier: ^2 + version: 2.11.2 + '@vitejs/plugin-vue': + specifier: ^5.2.1 + version: 5.2.4(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7))(vue@3.5.34(typescript@5.6.3)) + '@vitest/coverage-v8': + specifier: ^4.1.7 + version: 4.1.7(vitest@4.1.7) + '@vue/test-utils': + specifier: ^2.4.10 + version: 2.4.10(@vue/compiler-dom@3.5.34)(@vue/server-renderer@3.5.34(vue@3.5.34(typescript@5.6.3)))(vue@3.5.34(typescript@5.6.3)) + autoprefixer: + specifier: ^10.4.24 + version: 10.5.0(postcss@8.5.15) + happy-dom: + specifier: ^20.9.0 + version: 20.9.0 + postcss: + specifier: ^8.5.6 + version: 8.5.15 + tailwindcss: + specifier: ^3.4.19 + version: 3.4.19 + typescript: + specifier: ~5.6.2 + version: 5.6.3 + vite: + specifier: ^6.0.3 + version: 6.4.2(@types/node@25.9.1)(jiti@1.21.7) + vitest: + specifier: ^4.1.7 + version: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(happy-dom@20.9.0)(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7)) + vue-tsc: + specifier: ^2.2.12 + version: 2.2.12(typescript@5.6.3) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tauri-apps/api@2.11.0': + resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} + + '@tauri-apps/cli-darwin-arm64@2.11.2': + resolution: {integrity: sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.11.2': + resolution: {integrity: sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + resolution: {integrity: sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + resolution: {integrity: sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tauri-apps/cli-linux-arm64-musl@2.11.2': + resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@tauri-apps/cli-linux-x64-gnu@2.11.2': + resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tauri-apps/cli-linux-x64-musl@2.11.2': + resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + resolution: {integrity: sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.11.2': + resolution: {integrity: sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.11.2': + resolution: {integrity: sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-opener@2.5.4': + resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vitest/coverage-v8@4.1.7': + resolution: {integrity: sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==} + peerDependencies: + '@vitest/browser': 4.1.7 + vitest: 4.1.7 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + + '@volar/language-core@2.4.15': + resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} + + '@volar/source-map@2.4.15': + resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} + + '@volar/typescript@2.4.15': + resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} + + '@vue/compiler-core@3.5.34': + resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} + + '@vue/compiler-dom@3.5.34': + resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} + + '@vue/compiler-sfc@3.5.34': + resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==} + + '@vue/compiler-ssr@3.5.34': + resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/language-core@2.2.12': + resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.34': + resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} + + '@vue/runtime-core@3.5.34': + resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==} + + '@vue/runtime-dom@3.5.34': + resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==} + + '@vue/server-renderer@3.5.34': + resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==} + peerDependencies: + vue: 3.5.34 + + '@vue/shared@3.5.34': + resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} + + '@vue/test-utils@2.4.10': + resolution: {integrity: sha512-SmoZ5EA1kYiAFs9NkYdiFFQF+cSnUwnvlYEbY+DogWQZUiqOm/Y29eSbc5T6yi75SgSF9863SBeXniIEoPajCA==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + alien-signals@1.0.13: + resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.2: + resolution: {integrity: sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==} + + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} + engines: {node: '>=6.0.0'} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + + electron-to-chromium@1.5.361: + resolution: {integrity: sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + happy-dom@20.9.0: + resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} + engines: {node: '>=20.0.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.7: + resolution: {integrity: sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==} + engines: {node: '>=20'} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lucide-vue-next@0.574.0: + resolution: {integrity: sha512-WzsTQJ2YLINuGeN2iM2fmhHmgkyhGdAADbiXnbAvOuizry4ISFbE2oQGRz3ExjwK3wunW9oGe9EC8WNiXW2b9A==} + deprecated: Package deprecated. Please use @lucide/vue instead. + peerDependencies: + vue: '>=3.0.1' + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.2: + resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-type-helpers@3.3.2: + resolution: {integrity: sha512-l4Z2Y34m7nFMlx8vrslJaVtXxUpzgDMSESC7TakG/c5kwjYT/do+E0NcT2/vWDzaoIhsShg/2OKwX7Q4nbzC0g==} + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-tsc@2.2.12: + resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.34: + resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@one-ini/wasm@0.1.1': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@tauri-apps/api@2.11.0': {} + + '@tauri-apps/cli-darwin-arm64@2.11.2': + optional: true + + '@tauri-apps/cli-darwin-x64@2.11.2': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.11.2': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.11.2': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.11.2': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.11.2': + optional: true + + '@tauri-apps/cli@2.11.2': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.11.2 + '@tauri-apps/cli-darwin-x64': 2.11.2 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.2 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.2 + '@tauri-apps/cli-linux-arm64-musl': 2.11.2 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-musl': 2.11.2 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.2 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 + '@tauri-apps/cli-win32-x64-msvc': 2.11.2 + + '@tauri-apps/plugin-opener@2.5.4': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + + '@types/whatwg-mimetype@3.0.2': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.9.1 + + '@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7))(vue@3.5.34(typescript@5.6.3))': + dependencies: + vite: 6.4.2(@types/node@25.9.1)(jiti@1.21.7) + vue: 3.5.34(typescript@5.6.3) + + '@vitest/coverage-v8@4.1.7(vitest@4.1.7)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.7 + ast-v8-to-istanbul: 1.0.2 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(happy-dom@20.9.0)(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7)) + + '@vitest/expect@4.1.7': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.7(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7))': + dependencies: + '@vitest/spy': 4.1.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.2(@types/node@25.9.1)(jiti@1.21.7) + + '@vitest/pretty-format@4.1.7': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.7': + dependencies: + '@vitest/utils': 4.1.7 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.7': {} + + '@vitest/utils@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@volar/language-core@2.4.15': + dependencies: + '@volar/source-map': 2.4.15 + + '@volar/source-map@2.4.15': {} + + '@volar/typescript@2.4.15': + dependencies: + '@volar/language-core': 2.4.15 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.34': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.34 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.34': + dependencies: + '@vue/compiler-core': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/compiler-sfc@3.5.34': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.34 + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.34': + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@2.2.12(typescript@5.6.3)': + dependencies: + '@volar/language-core': 2.4.15 + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.34 + alien-signals: 1.0.13 + minimatch: 9.0.9 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.6.3 + + '@vue/reactivity@3.5.34': + dependencies: + '@vue/shared': 3.5.34 + + '@vue/runtime-core@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/runtime-dom@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/runtime-core': 3.5.34 + '@vue/shared': 3.5.34 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.34(vue@3.5.34(typescript@5.6.3))': + dependencies: + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + vue: 3.5.34(typescript@5.6.3) + + '@vue/shared@3.5.34': {} + + '@vue/test-utils@2.4.10(@vue/compiler-dom@3.5.34)(@vue/server-renderer@3.5.34(vue@3.5.34(typescript@5.6.3)))(vue@3.5.34(typescript@5.6.3))': + dependencies: + '@vue/compiler-dom': 3.5.34 + js-beautify: 1.15.4 + vue: 3.5.34(typescript@5.6.3) + vue-component-type-helpers: 3.3.2 + optionalDependencies: + '@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@5.6.3)) + + abbrev@2.0.0: {} + + alien-signals@1.0.13: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + arg@5.0.2: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.2: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + autoprefixer@10.5.0(postcss@8.5.15): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.15 + postcss-value-parser: 4.2.0 + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.10.32: {} + + binary-extensions@2.3.0: {} + + birpc@2.9.0: {} + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.32 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.361 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001793: {} + + chai@6.2.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@10.0.1: {} + + commander@4.1.1: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + convert-source-map@2.0.0: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + de-indent@1.0.2: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + eastasianwidth@0.2.0: {} + + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.8.1 + + electron-to-chromium@1.5.361: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@7.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.1.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + happy-dom@20.9.0: + dependencies: + '@types/node': 25.9.1 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + has-flag@4.0.0: {} + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + hookable@5.5.3: {} + + html-escaper@2.0.2: {} + + ini@1.3.8: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-what@5.5.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@1.21.7: {} + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.5.0 + js-cookie: 3.0.7 + nopt: 7.2.1 + + js-cookie@3.0.7: {} + + js-tokens@10.0.0: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lru-cache@10.4.3: {} + + lucide-vue-next@0.574.0(vue@3.5.34(typescript@5.6.3)): + dependencies: + vue: 3.5.34(typescript@5.6.3) + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + + minipass@7.1.3: {} + + mitt@3.0.1: {} + + muggle-string@0.4.1: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.12: {} + + node-releases@2.0.46: {} + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + obug@2.1.1: {} + + package-json-from-dist@1.0.1: {} + + path-browserify@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@2.3.0: {} + + pinia@3.0.4(typescript@5.6.3)(vue@3.5.34(typescript@5.6.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.34(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.12 + + postcss-js@4.1.0(postcss@8.5.15): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.15 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.15): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.15 + + postcss-nested@6.2.0(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proto-list@1.2.4: {} + + queue-microtask@1.2.3: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + semver@7.8.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.16 + ts-interface-checker: 0.1.13 + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.15 + postcss-import: 15.1.0(postcss@8.5.15) + postcss-js: 4.1.0(postcss@8.5.15) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.15) + postcss-nested: 6.2.0(postcss@8.5.15) + postcss-selector-parser: 6.1.2 + resolve: 1.22.12 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@1.2.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-interface-checker@0.1.13: {} + + typescript@5.6.3: {} + + undici-types@7.24.6: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.60.4 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.9.1 + fsevents: 2.3.3 + jiti: 1.21.7 + + vitest@4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(happy-dom@20.9.0)(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7)): + dependencies: + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 6.4.2(@types/node@25.9.1)(jiti@1.21.7) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.1 + '@vitest/coverage-v8': 4.1.7(vitest@4.1.7) + happy-dom: 20.9.0 + transitivePeerDependencies: + - msw + + vscode-uri@3.1.0: {} + + vue-component-type-helpers@3.3.2: {} + + vue-router@4.6.4(vue@3.5.34(typescript@5.6.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.34(typescript@5.6.3) + + vue-tsc@2.2.12(typescript@5.6.3): + dependencies: + '@volar/typescript': 2.4.15 + '@vue/language-core': 2.2.12(typescript@5.6.3) + typescript: 5.6.3 + + vue@3.5.34(typescript@5.6.3): + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-sfc': 3.5.34 + '@vue/runtime-dom': 3.5.34 + '@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@5.6.3)) + '@vue/shared': 3.5.34 + optionalDependencies: + typescript: 5.6.3 + + whatwg-mimetype@3.0.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + ws@8.21.0: {} diff --git a/lyremember-app/src/components/LyricsGrid.vue b/lyremember-app/src/components/LyricsGrid.vue new file mode 100644 index 0000000..0a8e9be --- /dev/null +++ b/lyremember-app/src/components/LyricsGrid.vue @@ -0,0 +1,74 @@ + + + diff --git a/lyremember-app/src/stores/__tests__/auth.spec.ts b/lyremember-app/src/stores/__tests__/auth.spec.ts new file mode 100644 index 0000000..7c40b4c --- /dev/null +++ b/lyremember-app/src/stores/__tests__/auth.spec.ts @@ -0,0 +1,207 @@ +import { setActivePinia, createPinia } from 'pinia'; +import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest'; +import { useAuthStore } from '../auth'; + +// Mock @tauri-apps/api/core so invoke never hits native bindings +vi.mock('@tauri-apps/api/core', () => ({ + invoke: vi.fn(), +})); + +// Provide a minimal localStorage mock for happy-dom (already available, but explicit) +const mockUser = { + id: 'user-1', + username: 'testuser', + email: 'test@example.com', + genius_token: null, + created_at: '2024-01-01T00:00:00Z', +}; + +// Import the mocked invoke so tests can control its behaviour +import { invoke } from '@tauri-apps/api/core'; +const mockInvoke = vi.mocked(invoke); + +describe('authStore', () => { + beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + localStorage.clear(); + }); + + afterEach(() => { + localStorage.clear(); + }); + + // ── Initial state ──────────────────────────────────────────────────────── + describe('initial state', () => { + it('starts unauthenticated', () => { + const store = useAuthStore(); + expect(store.user).toBeNull(); + expect(store.token).toBeNull(); + expect(store.loading).toBe(false); + expect(store.error).toBeNull(); + }); + + it('isAuthenticated is false when not logged in', () => { + const store = useAuthStore(); + expect(store.isAuthenticated).toBe(false); + }); + + it('username returns empty string when not logged in', () => { + const store = useAuthStore(); + expect(store.username).toBe(''); + }); + }); + + // ── register ───────────────────────────────────────────────────────────── + describe('register', () => { + it('sets user on successful registration', async () => { + mockInvoke.mockResolvedValueOnce(mockUser); + const store = useAuthStore(); + + const result = await store.register('testuser', 'test@example.com', 'password123'); + + expect(result).toEqual(mockUser); + expect(store.user).toEqual(mockUser); + expect(store.loading).toBe(false); + expect(store.error).toBeNull(); + }); + + it('sets error and throws on registration failure', async () => { + mockInvoke.mockRejectedValueOnce(new Error('Username already taken')); + const store = useAuthStore(); + + await expect(store.register('testuser', 'test@example.com', 'pw')).rejects.toThrow(); + expect(store.error).toBe('Username already taken'); + expect(store.user).toBeNull(); + expect(store.loading).toBe(false); + }); + }); + + // ── login ──────────────────────────────────────────────────────────────── + describe('login', () => { + it('sets user and token on successful login', async () => { + const token = 'valid-token-123'; + // First invoke: cmd_login → returns token + // Second invoke: cmd_verify_token → returns user + mockInvoke + .mockResolvedValueOnce(token) + .mockResolvedValueOnce(mockUser); + + const store = useAuthStore(); + const result = await store.login('testuser', 'password123'); + + expect(result).toEqual(mockUser); + expect(store.token).toBe(token); + expect(store.user).toEqual(mockUser); + expect(store.isAuthenticated).toBe(true); + expect(store.username).toBe('testuser'); + expect(store.loading).toBe(false); + expect(store.error).toBeNull(); + }); + + it('persists token to localStorage on login', async () => { + const token = 'valid-token-123'; + mockInvoke + .mockResolvedValueOnce(token) + .mockResolvedValueOnce(mockUser); + + const store = useAuthStore(); + await store.login('testuser', 'password123'); + + expect(localStorage.getItem('auth_token')).toBe(token); + }); + + it('clears state and sets error on login failure', async () => { + mockInvoke.mockRejectedValueOnce(new Error('Invalid credentials')); + const store = useAuthStore(); + + await expect(store.login('testuser', 'wrong')).rejects.toThrow(); + + expect(store.token).toBeNull(); + expect(store.user).toBeNull(); + expect(store.error).toBe('Invalid credentials'); + expect(store.isAuthenticated).toBe(false); + expect(store.loading).toBe(false); + }); + }); + + // ── logout ─────────────────────────────────────────────────────────────── + describe('logout', () => { + it('clears state and removes token from localStorage', async () => { + // Set up logged-in state + const token = 'valid-token-123'; + mockInvoke + .mockResolvedValueOnce(token) + .mockResolvedValueOnce(mockUser); + + const store = useAuthStore(); + await store.login('testuser', 'password123'); + expect(store.isAuthenticated).toBe(true); + + await store.logout(); + + expect(store.user).toBeNull(); + expect(store.token).toBeNull(); + expect(store.isAuthenticated).toBe(false); + expect(localStorage.getItem('auth_token')).toBeNull(); + }); + + it('is safe to call when not logged in', async () => { + const store = useAuthStore(); + await expect(store.logout()).resolves.toBeUndefined(); + expect(store.user).toBeNull(); + }); + }); + + // ── checkAuth ──────────────────────────────────────────────────────────── + describe('checkAuth', () => { + it('returns false when no saved token', async () => { + const store = useAuthStore(); + const result = await store.checkAuth(); + expect(result).toBe(false); + }); + + it('restores session from localStorage when token is valid', async () => { + localStorage.setItem('auth_token', 'saved-token'); + mockInvoke.mockResolvedValueOnce(mockUser); + + const store = useAuthStore(); + const result = await store.checkAuth(); + + expect(result).toBe(true); + expect(store.user).toEqual(mockUser); + expect(store.token).toBe('saved-token'); + }); + + it('clears invalid token and returns false', async () => { + localStorage.setItem('auth_token', 'expired-token'); + mockInvoke.mockRejectedValueOnce(new Error('Token expired')); + + const store = useAuthStore(); + const result = await store.checkAuth(); + + expect(result).toBe(false); + expect(store.user).toBeNull(); + expect(store.token).toBeNull(); + expect(localStorage.getItem('auth_token')).toBeNull(); + }); + }); + + // ── clearError ─────────────────────────────────────────────────────────── + describe('clearError', () => { + it('resets error to null', async () => { + mockInvoke.mockRejectedValueOnce(new Error('Some error')); + const store = useAuthStore(); + + try { + await store.login('user', 'bad'); + } catch { + // expected + } + + expect(store.error).not.toBeNull(); + store.clearError(); + expect(store.error).toBeNull(); + }); + }); +}); diff --git a/lyremember-app/src/stores/__tests__/sessions.spec.ts b/lyremember-app/src/stores/__tests__/sessions.spec.ts new file mode 100644 index 0000000..b0dd14c --- /dev/null +++ b/lyremember-app/src/stores/__tests__/sessions.spec.ts @@ -0,0 +1,250 @@ +import { setActivePinia, createPinia } from 'pinia'; +import { beforeEach, describe, it, expect, vi } from 'vitest'; +import { useSessionsStore } from '../sessions'; +import { useAuthStore } from '../auth'; + +vi.mock('@tauri-apps/api/core', () => ({ + invoke: vi.fn(), +})); + +import { invoke } from '@tauri-apps/api/core'; +const mockInvoke = vi.mocked(invoke); + +const mockUser = { + id: 'user-1', + username: 'testuser', + email: 'test@example.com', + genius_token: null, + created_at: '2024-01-01T00:00:00Z', +}; + +describe('sessionsStore', () => { + beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + }); + + // ── Initial state ──────────────────────────────────────────────────────── + describe('initial state', () => { + it('starts with no active session', () => { + const store = useSessionsStore(); + expect(store.currentSongId).toBeNull(); + expect(store.mode).toBeNull(); + expect(store.score).toBe(0); + expect(store.totalLines).toBe(0); + expect(store.correctLines).toBe(0); + expect(store.startedAt).toBeNull(); + }); + + it('sessionActive is false initially', () => { + const store = useSessionsStore(); + expect(store.sessionActive).toBe(false); + }); + + it('scorePercent is 0 when no lines recorded', () => { + const store = useSessionsStore(); + expect(store.scorePercent).toBe(0); + }); + }); + + // ── startSession ───────────────────────────────────────────────────────── + describe('startSession', () => { + it('initializes session state', () => { + const store = useSessionsStore(); + const before = Date.now(); + store.startSession('song-1', 'karaoke'); + const after = Date.now(); + + expect(store.currentSongId).toBe('song-1'); + expect(store.mode).toBe('karaoke'); + expect(store.score).toBe(0); + expect(store.totalLines).toBe(0); + expect(store.correctLines).toBe(0); + expect(store.startedAt).toBeGreaterThanOrEqual(before); + expect(store.startedAt).toBeLessThanOrEqual(after); + expect(store.sessionActive).toBe(true); + }); + + it('resets score when starting a new session', () => { + const store = useSessionsStore(); + store.startSession('song-1', 'karaoke'); + store.recordLine(true); + store.recordLine(true); + expect(store.totalLines).toBe(2); + + // Start fresh session + store.startSession('song-2', 'blanks'); + expect(store.totalLines).toBe(0); + expect(store.correctLines).toBe(0); + expect(store.score).toBe(0); + }); + + it('supports all practice modes', () => { + const store = useSessionsStore(); + const modes = ['karaoke', 'blanks', 'quiz', 'oral'] as const; + for (const mode of modes) { + store.startSession('song-1', mode); + expect(store.mode).toBe(mode); + } + }); + }); + + // ── recordLine ─────────────────────────────────────────────────────────── + describe('recordLine', () => { + it('increments totalLines on each call', () => { + const store = useSessionsStore(); + store.startSession('song-1', 'blanks'); + + store.recordLine(true); + store.recordLine(false); + store.recordLine(true); + + expect(store.totalLines).toBe(3); + }); + + it('increments correctLines only for correct answers', () => { + const store = useSessionsStore(); + store.startSession('song-1', 'blanks'); + + store.recordLine(true); + store.recordLine(false); + store.recordLine(true); + + expect(store.correctLines).toBe(2); + }); + + it('updates score after each line', () => { + const store = useSessionsStore(); + store.startSession('song-1', 'quiz'); + + store.recordLine(true); + expect(store.score).toBe(100); + + store.recordLine(false); + expect(store.score).toBe(50); + + store.recordLine(true); + expect(store.score).toBe(67); + }); + }); + + // ── scorePercent ───────────────────────────────────────────────────────── + describe('scorePercent', () => { + it('returns 0 when totalLines is 0', () => { + const store = useSessionsStore(); + expect(store.scorePercent).toBe(0); + }); + + it('returns 100 when all lines correct', () => { + const store = useSessionsStore(); + store.startSession('song-1', 'quiz'); + store.recordLine(true); + store.recordLine(true); + expect(store.scorePercent).toBe(100); + }); + + it('returns 0 when all lines incorrect', () => { + const store = useSessionsStore(); + store.startSession('song-1', 'quiz'); + store.recordLine(false); + store.recordLine(false); + expect(store.scorePercent).toBe(0); + }); + + it('rounds to nearest integer', () => { + const store = useSessionsStore(); + store.startSession('song-1', 'quiz'); + // 1/3 = 33.33... → 33 + store.recordLine(true); + store.recordLine(false); + store.recordLine(false); + expect(store.scorePercent).toBe(33); + }); + }); + + // ── endSession ─────────────────────────────────────────────────────────── + describe('endSession', () => { + it('calls createPracticeSession API when session is active and user logged in', async () => { + const authStore = useAuthStore(); + authStore.user = mockUser as ReturnType['user']; + + mockInvoke.mockResolvedValueOnce({ + id: 'session-1', + user_id: 'user-1', + song_id: 'song-1', + mode: 'karaoke', + score: 100, + lines_practiced: 2, + lines_correct: 2, + duration_seconds: 30, + practiced_at: '2024-01-01T00:00:00Z', + }); + + const store = useSessionsStore(); + store.startSession('song-1', 'karaoke'); + store.recordLine(true); + store.recordLine(true); + + await store.endSession(); + + expect(mockInvoke).toHaveBeenCalledWith('cmd_create_practice_session', expect.objectContaining({ + userId: 'user-1', + songId: 'song-1', + mode: 'karaoke', + score: 100, + linesPracticed: 2, + linesCorrect: 2, + })); + }); + + it('does nothing when session is not started', async () => { + const authStore = useAuthStore(); + authStore.user = mockUser as ReturnType['user']; + + const store = useSessionsStore(); + await store.endSession(); + + expect(mockInvoke).not.toHaveBeenCalled(); + }); + + it('does nothing when user is not authenticated', async () => { + const store = useSessionsStore(); + store.startSession('song-1', 'karaoke'); + + await store.endSession(); + + expect(mockInvoke).not.toHaveBeenCalled(); + }); + + it('does not throw when API call fails (non-fatal)', async () => { + const authStore = useAuthStore(); + authStore.user = mockUser as ReturnType['user']; + mockInvoke.mockRejectedValueOnce(new Error('Backend unavailable')); + + const store = useSessionsStore(); + store.startSession('song-1', 'karaoke'); + + await expect(store.endSession()).resolves.toBeUndefined(); + }); + }); + + // ── reset ──────────────────────────────────────────────────────────────── + describe('reset', () => { + it('clears all session state', () => { + const store = useSessionsStore(); + store.startSession('song-1', 'karaoke'); + store.recordLine(true); + + store.reset(); + + expect(store.currentSongId).toBeNull(); + expect(store.mode).toBeNull(); + expect(store.score).toBe(0); + expect(store.totalLines).toBe(0); + expect(store.correctLines).toBe(0); + expect(store.startedAt).toBeNull(); + expect(store.sessionActive).toBe(false); + expect(store.scorePercent).toBe(0); + }); + }); +}); diff --git a/lyremember-app/src/stores/__tests__/songs.spec.ts b/lyremember-app/src/stores/__tests__/songs.spec.ts new file mode 100644 index 0000000..0344a46 --- /dev/null +++ b/lyremember-app/src/stores/__tests__/songs.spec.ts @@ -0,0 +1,272 @@ +import { setActivePinia, createPinia } from 'pinia'; +import { beforeEach, describe, it, expect, vi } from 'vitest'; +import { useSongsStore } from '../songs'; +import { useAuthStore } from '../auth'; + +vi.mock('@tauri-apps/api/core', () => ({ + invoke: vi.fn(), +})); + +import { invoke } from '@tauri-apps/api/core'; +const mockInvoke = vi.mocked(invoke); + +const mockUser = { + id: 'user-1', + username: 'testuser', + email: 'test@example.com', + genius_token: null, + created_at: '2024-01-01T00:00:00Z', +}; + +const mockSong = { + id: 'song-1', + title: 'La Vie en Rose', + artist: 'Édith Piaf', + language: 'fr', + lyrics: ['Des yeux qui font baisser les miens', 'Un rire qui se perd sur sa bouche'], + phonetic_lyrics: null, + translations: null, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', +}; + +const mockSong2 = { + id: 'song-2', + title: 'Bohemian Rhapsody', + artist: 'Queen', + language: 'en', + lyrics: ['Is this the real life?', 'Is this just fantasy?'], + phonetic_lyrics: null, + translations: null, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', +}; + +describe('songsStore', () => { + beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + + // Set up authenticated user in authStore + const authStore = useAuthStore(); + authStore.user = mockUser as ReturnType['user']; + }); + + // ── Initial state ──────────────────────────────────────────────────────── + describe('initial state', () => { + it('starts with empty songs list', () => { + const store = useSongsStore(); + expect(store.songs).toEqual([]); + expect(store.currentSong).toBeNull(); + expect(store.loading).toBe(false); + expect(store.error).toBeNull(); + }); + + it('totalSongs is 0 initially', () => { + const store = useSongsStore(); + expect(store.totalSongs).toBe(0); + }); + }); + + // ── fetchAllSongs ──────────────────────────────────────────────────────── + describe('fetchAllSongs', () => { + it('populates songs array', async () => { + mockInvoke.mockResolvedValueOnce([mockSong, mockSong2]); + const store = useSongsStore(); + + await store.fetchAllSongs(); + + expect(store.songs).toHaveLength(2); + expect(store.songs[0]).toEqual(mockSong); + expect(store.loading).toBe(false); + expect(store.error).toBeNull(); + }); + + it('sets error on fetch failure', async () => { + mockInvoke.mockRejectedValueOnce(new Error('Network error')); + const store = useSongsStore(); + + await expect(store.fetchAllSongs()).rejects.toThrow(); + expect(store.error).toBe('Network error'); + expect(store.loading).toBe(false); + }); + }); + + // ── fetchSongs / fetchUserSongs ────────────────────────────────────────── + describe('fetchSongs', () => { + it('fetches user songs when user is authenticated', async () => { + mockInvoke.mockResolvedValueOnce([mockSong]); + const store = useSongsStore(); + + await store.fetchSongs(); + + expect(store.songs).toHaveLength(1); + expect(store.songs[0]).toEqual(mockSong); + }); + + it('throws when user is not authenticated', async () => { + const authStore = useAuthStore(); + authStore.user = null; + const store = useSongsStore(); + + await expect(store.fetchSongs()).rejects.toThrow('User not authenticated'); + }); + }); + + // ── fetchSong ──────────────────────────────────────────────────────────── + describe('fetchSong', () => { + it('sets currentSong on success', async () => { + mockInvoke.mockResolvedValueOnce(mockSong); + const store = useSongsStore(); + + const result = await store.fetchSong('song-1'); + + expect(result).toEqual(mockSong); + expect(store.currentSong).toEqual(mockSong); + expect(store.loading).toBe(false); + }); + }); + + // ── createSong ─────────────────────────────────────────────────────────── + describe('createSong', () => { + it('adds song to store on success', async () => { + mockInvoke.mockResolvedValueOnce(mockSong); + const store = useSongsStore(); + + const result = await store.createSong('La Vie en Rose', 'Édith Piaf', 'fr', mockSong.lyrics); + + expect(result).toEqual(mockSong); + expect(store.songs).toHaveLength(1); + expect(store.songs[0]).toEqual(mockSong); + }); + + it('sets error on failure', async () => { + mockInvoke.mockRejectedValueOnce(new Error('Create failed')); + const store = useSongsStore(); + + await expect(store.createSong('Title', 'Artist', 'fr', [])).rejects.toThrow(); + expect(store.error).toBe('Create failed'); + }); + }); + + // ── addSong ────────────────────────────────────────────────────────────── + describe('addSong', () => { + it('creates song and adds to repertoire', async () => { + // First invoke: createSong, Second invoke: addToRepertoire (void) + mockInvoke + .mockResolvedValueOnce(mockSong) + .mockResolvedValueOnce(undefined); + + const store = useSongsStore(); + const result = await store.addSong('La Vie en Rose', 'Édith Piaf', 'fr', mockSong.lyrics); + + expect(result).toEqual(mockSong); + expect(mockInvoke).toHaveBeenCalledTimes(2); + }); + }); + + // ── deleteSong / removeSong ────────────────────────────────────────────── + describe('deleteSong', () => { + it('removes song from store after deletion', async () => { + const store = useSongsStore(); + store.songs = [mockSong, mockSong2]; + mockInvoke.mockResolvedValueOnce(undefined); + + await store.deleteSong('song-1'); + + expect(store.songs).toHaveLength(1); + expect(store.songs[0].id).toBe('song-2'); + }); + + it('clears currentSong if deleted song was current', async () => { + const store = useSongsStore(); + store.songs = [mockSong]; + store.currentSong = mockSong; + mockInvoke.mockResolvedValueOnce(undefined); + + await store.deleteSong('song-1'); + + expect(store.currentSong).toBeNull(); + }); + }); + + describe('removeSong', () => { + it('delegates to deleteSong', async () => { + const store = useSongsStore(); + store.songs = [mockSong]; + mockInvoke.mockResolvedValueOnce(undefined); + + await store.removeSong('song-1'); + + expect(store.songs).toHaveLength(0); + }); + }); + + // ── Getters ────────────────────────────────────────────────────────────── + describe('filteredSongs', () => { + beforeEach(() => { + const store = useSongsStore(); + store.songs = [mockSong, mockSong2]; + }); + + it('returns all songs when no filter', () => { + const store = useSongsStore(); + expect(store.filteredSongs).toHaveLength(2); + }); + + it('filters by search query on title', () => { + const store = useSongsStore(); + store.setSearchQuery('rose'); + expect(store.filteredSongs).toHaveLength(1); + expect(store.filteredSongs[0].id).toBe('song-1'); + }); + + it('filters by search query on artist', () => { + const store = useSongsStore(); + store.setSearchQuery('queen'); + expect(store.filteredSongs).toHaveLength(1); + expect(store.filteredSongs[0].id).toBe('song-2'); + }); + + it('filters by language', () => { + const store = useSongsStore(); + store.setSelectedLanguage('fr'); + expect(store.filteredSongs).toHaveLength(1); + expect(store.filteredSongs[0].id).toBe('song-1'); + }); + + it('returns empty array when no match', () => { + const store = useSongsStore(); + store.setSearchQuery('zzznomatch'); + expect(store.filteredSongs).toHaveLength(0); + }); + }); + + describe('songsByLanguage', () => { + it('groups songs by language', () => { + const store = useSongsStore(); + store.songs = [mockSong, mockSong2]; + const grouped = store.songsByLanguage; + expect(grouped['fr']).toHaveLength(1); + expect(grouped['en']).toHaveLength(1); + }); + }); + + describe('totalSongs', () => { + it('reflects number of songs', () => { + const store = useSongsStore(); + store.songs = [mockSong, mockSong2]; + expect(store.totalSongs).toBe(2); + }); + }); + + // ── clearError ─────────────────────────────────────────────────────────── + describe('clearError', () => { + it('resets error to null', () => { + const store = useSongsStore(); + store.error = 'Some error'; + store.clearError(); + expect(store.error).toBeNull(); + }); + }); +}); diff --git a/lyremember-app/src/views/SongDetailView.vue b/lyremember-app/src/views/SongDetailView.vue index e5380a0..2836ea2 100644 --- a/lyremember-app/src/views/SongDetailView.vue +++ b/lyremember-app/src/views/SongDetailView.vue @@ -1,144 +1,263 @@ diff --git a/lyremember-app/vite.config.ts b/lyremember-app/vite.config.ts index 812e61c..1686fc1 100644 --- a/lyremember-app/vite.config.ts +++ b/lyremember-app/vite.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; +import path from "path"; // @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; @@ -8,6 +9,12 @@ const host = process.env.TAURI_DEV_HOST; export default defineConfig(async () => ({ plugins: [vue()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent Vite from obscuring rust errors @@ -29,4 +36,15 @@ export default defineConfig(async () => ({ ignored: ["**/src-tauri/**"], }, }, + + test: { + environment: "happy-dom", + globals: true, + coverage: { + provider: "v8", + reporter: ["text"], + include: ["src/stores/**"], + thresholds: { lines: 80 }, + }, + }, })); From d104c816c6a2569c315f9bff4bce23c9a39719d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Guillois?= Date: Tue, 26 May 2026 18:05:13 +0200 Subject: [PATCH 02/25] M9/#39: add aria-labels, aria-live, autocomplete, and role attributes across components --- .../src/components/layout/AppHeader.vue | 24 +- .../src/components/layout/AppSidebar.vue | 13 +- lyremember-app/src/components/ui/Input.vue | 21 +- lyremember-app/src/views/LoginView.vue | 91 ++++- .../src/views/PracticeFillBlanksView.vue | 307 ++++++++++++++++ .../src/views/PracticeKaraokeView.vue | 252 +++++++++++++ lyremember-app/src/views/PracticeQuizView.vue | 339 ++++++++++++++++++ lyremember-app/src/views/RegisterView.vue | 154 ++++++-- lyremember-app/src/views/SongsView.vue | 142 +++++--- lyremember-app/src/views/StatsView.vue | 241 +++++++++++++ 10 files changed, 1464 insertions(+), 120 deletions(-) create mode 100644 lyremember-app/src/views/PracticeFillBlanksView.vue create mode 100644 lyremember-app/src/views/PracticeKaraokeView.vue create mode 100644 lyremember-app/src/views/PracticeQuizView.vue create mode 100644 lyremember-app/src/views/StatsView.vue diff --git a/lyremember-app/src/components/layout/AppHeader.vue b/lyremember-app/src/components/layout/AppHeader.vue index 0e16b1d..d6bd7aa 100644 --- a/lyremember-app/src/components/layout/AppHeader.vue +++ b/lyremember-app/src/components/layout/AppHeader.vue @@ -5,6 +5,8 @@ @@ -18,7 +20,7 @@ - + - +
-
- + + - - + class="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500" + />
- +
- +

Loading songs...

- -
+ +

- {{ songsStore.searchQuery ? 'No songs found' : 'No songs yet' }} + {{ searchQuery || selectedLanguage !== 'all' ? 'No songs found' : 'No songs yet' }}

- {{ songsStore.searchQuery ? 'Try a different search' : 'Add your first song to get started!' }} + {{ searchQuery || selectedLanguage !== 'all' + ? 'Try a different search or language filter' + : 'Add your first song to get started!' }}

- -
- +
-
+
-
+

{{ song.title }}

-

+

{{ song.artist }}

- + {{ song.language.toUpperCase() }}
- +
- + {{ song.lyrics.length }} lines
- -
- - - Phonetic - - +
+ + Phonetic + + + Translation + +
+
- +
diff --git a/lyremember-app/src/views/StatsView.vue b/lyremember-app/src/views/StatsView.vue new file mode 100644 index 0000000..0ab8769 --- /dev/null +++ b/lyremember-app/src/views/StatsView.vue @@ -0,0 +1,241 @@ + + + From 565a49562e2510b1334b3e083a79bd03245f854a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Guillois?= Date: Tue, 26 May 2026 18:05:38 +0200 Subject: [PATCH 03/25] M9/#43: set CSP in tauri.conf.json; pnpm audit --prod shows no vulnerabilities; no v-html usage --- lyremember-app/src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lyremember-app/src-tauri/tauri.conf.json b/lyremember-app/src-tauri/tauri.conf.json index b868771..d2b40e0 100644 --- a/lyremember-app/src-tauri/tauri.conf.json +++ b/lyremember-app/src-tauri/tauri.conf.json @@ -18,7 +18,7 @@ } ], "security": { - "csp": null + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://api.genius.com https://libretranslate.de https://libretranslate.com; img-src 'self' data:" } }, "bundle": { From 4734dc868338a43f38fd30acb971323e1b90d932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Guillois?= Date: Tue, 26 May 2026 18:16:54 +0200 Subject: [PATCH 04/25] fix/FIX-1: add ESLint flat config, lint scripts, fix all warnings --- lyremember-app/eslint.config.js | 52 +++ lyremember-app/package.json | 8 +- lyremember-app/pnpm-lock.yaml | 717 ++++++++++++++++++++++++++++++ lyremember-app/src/stores/auth.ts | 2 +- lyremember-app/src/vite-env.d.ts | 1 + 5 files changed, 778 insertions(+), 2 deletions(-) create mode 100644 lyremember-app/eslint.config.js diff --git a/lyremember-app/eslint.config.js b/lyremember-app/eslint.config.js new file mode 100644 index 0000000..603e378 --- /dev/null +++ b/lyremember-app/eslint.config.js @@ -0,0 +1,52 @@ +import pluginVue from 'eslint-plugin-vue' +import tsParser from '@typescript-eslint/parser' +import tsPlugin from '@typescript-eslint/eslint-plugin' + +export default [ + // Vue flat/recommended already includes vue-eslint-parser for .vue files + ...pluginVue.configs['flat/recommended'], + + // TypeScript parser for pure .ts files (overrides vue-eslint-parser) + { + files: ['**/*.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + }, + }, + plugins: { + '@typescript-eslint': tsPlugin, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + }, + }, + + // TypeScript rules for .vue files (vue-eslint-parser delegates to tsParser via parserOptions.parser) + { + files: ['**/*.vue'], + plugins: { + '@typescript-eslint': tsPlugin, + }, + languageOptions: { + parserOptions: { + parser: tsParser, + sourceType: 'module', + ecmaVersion: 'latest', + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'vue/multi-word-component-names': 'off', + }, + }, + + // Ignored paths + { + ignores: ['dist/', 'node_modules/', 'src-tauri/'], + }, +] diff --git a/lyremember-app/package.json b/lyremember-app/package.json index a6e9a4d..1436f89 100644 --- a/lyremember-app/package.json +++ b/lyremember-app/package.json @@ -10,7 +10,9 @@ "tauri": "tauri", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "lint": "eslint src --ext .ts,.vue --max-warnings 0", + "lint:fix": "eslint src --ext .ts,.vue --fix" }, "dependencies": { "@tauri-apps/api": "^2", @@ -22,10 +24,14 @@ }, "devDependencies": { "@tauri-apps/cli": "^2", + "@typescript-eslint/eslint-plugin": "^8.60.0", + "@typescript-eslint/parser": "^8.60.0", "@vitejs/plugin-vue": "^5.2.1", "@vitest/coverage-v8": "^4.1.7", "@vue/test-utils": "^2.4.10", "autoprefixer": "^10.4.24", + "eslint": "^10.4.0", + "eslint-plugin-vue": "^10.9.1", "happy-dom": "^20.9.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", diff --git a/lyremember-app/pnpm-lock.yaml b/lyremember-app/pnpm-lock.yaml index d555c34..a48d3c8 100644 --- a/lyremember-app/pnpm-lock.yaml +++ b/lyremember-app/pnpm-lock.yaml @@ -30,6 +30,12 @@ importers: '@tauri-apps/cli': specifier: ^2 version: 2.11.2 + '@typescript-eslint/eslint-plugin': + specifier: ^8.60.0 + version: 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3))(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3) + '@typescript-eslint/parser': + specifier: ^8.60.0 + version: 8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3) '@vitejs/plugin-vue': specifier: ^5.2.1 version: 5.2.4(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7))(vue@3.5.34(typescript@5.6.3)) @@ -42,6 +48,12 @@ importers: autoprefixer: specifier: ^10.4.24 version: 10.5.0(postcss@8.5.15) + eslint: + specifier: ^10.4.0 + version: 10.4.0(jiti@1.21.7) + eslint-plugin-vue: + specifier: ^10.9.1 + version: 10.9.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3))(eslint@10.4.0(jiti@1.21.7))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@1.21.7))) happy-dom: specifier: ^20.9.0 version: 20.9.0 @@ -247,6 +259,56 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -494,9 +556,15 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@25.9.1': resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} @@ -506,6 +574,65 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.60.0': + resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.60.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.60.0': + resolution: {integrity: sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.60.0': + resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.60.0': + resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.60.0': + resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.60.0': + resolution: {integrity: sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.60.0': + resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.60.0': + resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.60.0': + resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.60.0': + resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitejs/plugin-vue@5.2.4': resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -626,6 +753,19 @@ packages: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + alien-signals@1.0.13: resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} @@ -672,6 +812,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.10.32: resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} engines: {node: '>=6.0.0'} @@ -684,9 +828,16 @@ packages: birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@2.1.1: resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -751,6 +902,18 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -794,20 +957,89 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-vue@10.9.1: + resolution: {integrity: sha512-cHB0Tf4Duvzwecwd/AqWzZvF/QszE13BhjVUpVXWCy9AeMR5GjkAjP3i85vqgLgOuTmkHR1OJ5oMeqLHtuw8zg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + vue-eslint-parser: ^10.3.0 + peerDependenciesMeta: + '@stylistic/eslint-plugin': + optional: true + '@typescript-eslint/parser': + optional: true + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.4.0: + resolution: {integrity: sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -820,10 +1052,25 @@ packages: picomatch: optional: true + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -874,6 +1121,18 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -939,6 +1198,22 @@ packages: js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -946,6 +1221,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -973,6 +1252,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -984,6 +1267,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -995,6 +1281,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-releases@2.0.46: resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} engines: {node: '>=18'} @@ -1008,6 +1297,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1019,12 +1311,28 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1110,6 +1418,10 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -1117,9 +1429,17 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1248,9 +1568,19 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + typescript@5.6.3: resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} @@ -1265,6 +1595,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -1355,6 +1688,12 @@ packages: vue-component-type-helpers@3.3.2: resolution: {integrity: sha512-l4Z2Y34m7nFMlx8vrslJaVtXxUpzgDMSESC7TakG/c5kwjYT/do+E0NcT2/vWDzaoIhsShg/2OKwX7Q4nbzC0g==} + vue-eslint-parser@10.4.0: + resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + vue-router@4.6.4: resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} peerDependencies: @@ -1388,6 +1727,10 @@ packages: engines: {node: '>=8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -1408,6 +1751,14 @@ packages: utf-8-validate: optional: true + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -1505,6 +1856,52 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.4.0(jiti@1.21.7))': + dependencies: + eslint: 10.4.0(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -1682,8 +2079,12 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} + '@types/json-schema@7.0.15': {} + '@types/node@25.9.1': dependencies: undici-types: 7.24.6 @@ -1694,6 +2095,97 @@ snapshots: dependencies: '@types/node': 25.9.1 + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3))(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/type-utils': 8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3) + '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.60.0 + eslint: 10.4.0(jiti@1.21.7) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.60.0 + debug: 4.4.3 + eslint: 10.4.0(jiti@1.21.7) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.60.0(typescript@5.6.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.6.3) + '@typescript-eslint/types': 8.60.0 + debug: 4.4.3 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.60.0': + dependencies: + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/visitor-keys': 8.60.0 + + '@typescript-eslint/tsconfig-utils@8.60.0(typescript@5.6.3)': + dependencies: + typescript: 5.6.3 + + '@typescript-eslint/type-utils@8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.6.3) + '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3) + debug: 4.4.3 + eslint: 10.4.0(jiti@1.21.7) + ts-api-utils: 2.5.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.60.0': {} + + '@typescript-eslint/typescript-estree@8.60.0(typescript@5.6.3)': + dependencies: + '@typescript-eslint/project-service': 8.60.0(typescript@5.6.3) + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.6.3) + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/visitor-keys': 8.60.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.6.3) + eslint: 10.4.0(jiti@1.21.7) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.60.0': + dependencies: + '@typescript-eslint/types': 8.60.0 + eslint-visitor-keys: 5.0.1 + '@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7))(vue@3.5.34(typescript@5.6.3))': dependencies: vite: 6.4.2(@types/node@25.9.1)(jiti@1.21.7) @@ -1869,6 +2361,19 @@ snapshots: abbrev@2.0.0: {} + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + alien-signals@1.0.13: {} ansi-regex@5.0.1: {} @@ -1909,16 +2414,24 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + baseline-browser-mapping@2.10.32: {} binary-extensions@2.3.0: {} birpc@2.9.0: {} + boolbase@1.0.0: {} + brace-expansion@2.1.1: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -1982,6 +2495,12 @@ snapshots: de-indent@1.0.2: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + didyoumean@1.2.2: {} dlv@1.1.3: {} @@ -2038,14 +2557,97 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} + + eslint-plugin-vue@10.9.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3))(eslint@10.4.0(jiti@1.21.7))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@1.21.7))): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@1.21.7)) + eslint: 10.4.0(jiti@1.21.7) + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 7.1.1 + semver: 7.8.1 + vue-eslint-parser: 10.4.0(eslint@10.4.0(jiti@1.21.7)) + xml-name-validator: 4.0.0 + optionalDependencies: + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@1.21.7))(typescript@5.6.3) + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.4.0(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + expect-type@1.3.0: {} + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2054,6 +2656,10 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -2062,10 +2668,26 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -2119,6 +2741,12 @@ snapshots: html-escaper@2.0.2: {} + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + ini@1.3.8: {} is-binary-path@2.1.0: @@ -2176,10 +2804,29 @@ snapshots: js-tokens@10.0.0: {} + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lru-cache@10.4.3: {} lucide-vue-next@0.574.0(vue@3.5.34(typescript@5.6.3)): @@ -2207,6 +2854,10 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + minimatch@9.0.9: dependencies: brace-expansion: 2.1.1 @@ -2215,6 +2866,8 @@ snapshots: mitt@3.0.1: {} + ms@2.1.3: {} + muggle-string@0.4.1: {} mz@2.7.0: @@ -2225,6 +2878,8 @@ snapshots: nanoid@3.3.12: {} + natural-compare@1.4.0: {} + node-releases@2.0.46: {} nopt@7.2.1: @@ -2233,16 +2888,39 @@ snapshots: normalize-path@3.0.0: {} + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + object-assign@4.1.1: {} object-hash@3.0.0: {} obug@2.1.1: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + package-json-from-dist@1.0.1: {} path-browserify@1.0.1: {} + path-exists@4.0.0: {} + path-key@3.1.1: {} path-parse@1.0.7: {} @@ -2302,6 +2980,11 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-value-parser@4.2.0: {} postcss@8.5.15: @@ -2310,8 +2993,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + proto-list@1.2.4: {} + punycode@2.3.1: {} + queue-microtask@1.2.3: {} read-cache@1.0.0: @@ -2479,8 +3166,16 @@ snapshots: dependencies: is-number: 7.0.0 + ts-api-utils@2.5.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + ts-interface-checker@0.1.13: {} + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + typescript@5.6.3: {} undici-types@7.24.6: {} @@ -2491,6 +3186,10 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + util-deprecate@1.0.2: {} vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7): @@ -2539,6 +3238,18 @@ snapshots: vue-component-type-helpers@3.3.2: {} + vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@1.21.7)): + dependencies: + debug: 4.4.3 + eslint: 10.4.0(jiti@1.21.7) + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + semver: 7.8.1 + transitivePeerDependencies: + - supports-color + vue-router@4.6.4(vue@3.5.34(typescript@5.6.3)): dependencies: '@vue/devtools-api': 6.6.4 @@ -2571,6 +3282,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + word-wrap@1.2.5: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -2584,3 +3297,7 @@ snapshots: strip-ansi: 7.2.0 ws@8.21.0: {} + + xml-name-validator@4.0.0: {} + + yocto-queue@0.1.0: {} diff --git a/lyremember-app/src/stores/auth.ts b/lyremember-app/src/stores/auth.ts index 88f76b0..04e5751 100644 --- a/lyremember-app/src/stores/auth.ts +++ b/lyremember-app/src/stores/auth.ts @@ -73,7 +73,7 @@ export const useAuthStore = defineStore('auth', () => { user.value = authenticatedUser; token.value = savedToken; return true; - } catch (err) { + } catch { // Token invalid, clear it localStorage.removeItem('auth_token'); token.value = null; diff --git a/lyremember-app/src/vite-env.d.ts b/lyremember-app/src/vite-env.d.ts index fc81239..5244053 100644 --- a/lyremember-app/src/vite-env.d.ts +++ b/lyremember-app/src/vite-env.d.ts @@ -2,6 +2,7 @@ declare module "*.vue" { import type { DefineComponent } from "vue"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const component: DefineComponent<{}, {}, any>; export default component; } From 38fba514897ec98caeef86fa3a25a54294be25a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Guillois?= Date: Tue, 26 May 2026 18:17:00 +0200 Subject: [PATCH 05/25] fix/FIX-2: use pnpm instead of npm in tauri.conf.json build commands --- lyremember-app/src-tauri/tauri.conf.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lyremember-app/src-tauri/tauri.conf.json b/lyremember-app/src-tauri/tauri.conf.json index d2b40e0..3b1f7e8 100644 --- a/lyremember-app/src-tauri/tauri.conf.json +++ b/lyremember-app/src-tauri/tauri.conf.json @@ -4,9 +4,9 @@ "version": "0.1.0", "identifier": "com.runner.lyremember-app", "build": { - "beforeDevCommand": "npm run dev", + "beforeDevCommand": "pnpm dev", "devUrl": "http://localhost:1420", - "beforeBuildCommand": "npm run build", + "beforeBuildCommand": "pnpm build", "frontendDist": "../dist" }, "app": { @@ -23,7 +23,8 @@ }, "bundle": { "active": true, - "targets": "all", + "targets": ["deb", "appimage", "dmg", "msi", "nsis"], + "identifier": "com.lyremember.app", "icon": [ "icons/32x32.png", "icons/128x128.png", From c0b698801937e04ebb6433159de5cc999a6ee501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Guillois?= Date: Tue, 26 May 2026 18:17:43 +0200 Subject: [PATCH 06/25] fix/FIX-3: use single cmd_translate_lines IPC call in SongDetailView --- lyremember-app/src-tauri/src/commands.rs | 37 ++++++++++++- lyremember-app/src-tauri/src/lib.rs | 2 + lyremember-app/src/lib/tauri-api.ts | 19 +++++++ lyremember-app/src/views/SongDetailView.vue | 61 ++++++++++++++------- 4 files changed, 97 insertions(+), 22 deletions(-) diff --git a/lyremember-app/src-tauri/src/commands.rs b/lyremember-app/src-tauri/src/commands.rs index 556283e..613f404 100644 --- a/lyremember-app/src-tauri/src/commands.rs +++ b/lyremember-app/src-tauri/src/commands.rs @@ -23,7 +23,6 @@ pub async fn cmd_register( username, email, password, - genius_token: None, }; auth::register(&conn, data) @@ -56,11 +55,23 @@ pub async fn cmd_get_user( state: State<'_, DbState>, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; - + auth::get_user_by_id(&conn, &user_id) .map_err(|e| format!("Failed to get user: {}", e)) } +#[tauri::command] +pub async fn cmd_update_genius_token( + user_id: String, + genius_token: Option, + state: State<'_, DbState>, +) -> Result<(), String> { + let conn = state.0.lock().map_err(|e| e.to_string())?; + + auth::update_genius_token(&conn, &user_id, genius_token.as_deref()) + .map_err(|e| format!("Failed to update Genius token: {}", e)) +} + // ==================== SONGS COMMANDS ==================== #[tauri::command] @@ -236,6 +247,28 @@ pub async fn cmd_translate_text( .map_err(|e| format!("Translation failed: {}", e)) } +/// Translate all lines of a song in a single batch (one IPC call). +/// Empty lines are preserved as empty strings without hitting the network. +#[tauri::command] +pub async fn cmd_translate_lines( + lines: Vec, + source_lang: String, + target_lang: String, +) -> Result, String> { + let mut results: Vec = Vec::with_capacity(lines.len()); + for line in &lines { + if line.trim().is_empty() { + results.push(String::new()); + } else { + let translated = translation::translate_text(line, &source_lang, &target_lang) + .await + .map_err(|e| format!("Translation failed: {}", e))?; + results.push(translated); + } + } + Ok(results) +} + #[tauri::command] pub async fn cmd_generate_phonetic( text: Vec, diff --git a/lyremember-app/src-tauri/src/lib.rs b/lyremember-app/src-tauri/src/lib.rs index 3892fa8..991f717 100644 --- a/lyremember-app/src-tauri/src/lib.rs +++ b/lyremember-app/src-tauri/src/lib.rs @@ -40,6 +40,7 @@ pub fn run() { cmd_login, cmd_verify_token, cmd_get_user, + cmd_update_genius_token, // Songs commands cmd_create_song, cmd_get_songs, @@ -55,6 +56,7 @@ pub fn run() { cmd_get_song_mastery, // Utility commands cmd_translate_text, + cmd_translate_lines, cmd_generate_phonetic, cmd_health_check, ]) diff --git a/lyremember-app/src/lib/tauri-api.ts b/lyremember-app/src/lib/tauri-api.ts index 030949f..a17fa20 100644 --- a/lyremember-app/src/lib/tauri-api.ts +++ b/lyremember-app/src/lib/tauri-api.ts @@ -70,6 +70,13 @@ export async function getUser(userId: string): Promise { return await invoke('cmd_get_user', { userId }); } +export async function updateGeniusToken( + userId: string, + geniusToken: string | null +): Promise { + return await invoke('cmd_update_genius_token', { userId, geniusToken }); +} + // ==================== SONGS API ==================== export async function createSong( @@ -167,6 +174,18 @@ export async function translateText( return await invoke('cmd_translate_text', { text, sourceLang, targetLang }); } +/** + * Translate an array of lyrics lines in a single IPC call. + * Empty lines are preserved without hitting the translation service. + */ +export async function translateLines( + lines: string[], + sourceLang: string, + targetLang: string +): Promise { + return await invoke('cmd_translate_lines', { lines, sourceLang, targetLang }); +} + export async function generatePhonetic( text: string[], language: string diff --git a/lyremember-app/src/views/SongDetailView.vue b/lyremember-app/src/views/SongDetailView.vue index 2836ea2..70ac4ef 100644 --- a/lyremember-app/src/views/SongDetailView.vue +++ b/lyremember-app/src/views/SongDetailView.vue @@ -3,7 +3,10 @@
-
@@ -23,14 +26,24 @@
-
+
-

Chargement de la chanson…

+

+ Chargement de la chanson… +

-
-

Chanson introuvable.

+
+

+ Chanson introuvable. +

@@ -41,10 +54,10 @@
@@ -163,6 +185,7 @@ import { Wand2, Languages, PenLine, + Mic, } from 'lucide-vue-next'; import MainLayout from '../components/layout/MainLayout.vue'; import Card from '../components/ui/Card.vue'; @@ -235,13 +258,11 @@ async function handleGenerateTranslation() { actionError.value = null; generatingTranslation.value = true; try { - // Translate line by line, keeping empty lines as-is - const translated: string[] = await Promise.all( - song.value.lyrics.map((line) => - line.trim() - ? tauriApi.translateText(line, song.value!.language, 'en') - : Promise.resolve('') - ) + // Single IPC call — Rust handles empty lines without hitting the network + const translated = await tauriApi.translateLines( + song.value.lyrics, + song.value.language, + 'en' ); generatedTranslations.value = translated; showTranslation.value = true; @@ -253,7 +274,7 @@ async function handleGenerateTranslation() { } } -function navigateToPractice(mode: 'karaoke' | 'blanks' | 'quiz') { +function navigateToPractice(mode: 'karaoke' | 'blanks' | 'quiz' | 'oral') { if (!song.value) return; // Routes will be created in M4 — navigate with graceful fallback router.push({ path: `/practice/${song.value.id}/${mode}` }).catch(() => { From f41d833014f3bfa46430d6c92c1edf2adb73f4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Guillois?= Date: Tue, 26 May 2026 18:18:00 +0200 Subject: [PATCH 07/25] fix/FIX-4: add similarity.spec.ts with 10 unit tests --- .../src/lib/__tests__/similarity.spec.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 lyremember-app/src/lib/__tests__/similarity.spec.ts diff --git a/lyremember-app/src/lib/__tests__/similarity.spec.ts b/lyremember-app/src/lib/__tests__/similarity.spec.ts new file mode 100644 index 0000000..1e0f464 --- /dev/null +++ b/lyremember-app/src/lib/__tests__/similarity.spec.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest' +import { normalizeText, levenshtein, similarity } from '../similarity' + +describe('normalizeText', () => { + it('lowercases', () => expect(normalizeText('HELLO')).toBe('hello')) + it('trims', () => expect(normalizeText(' hi ')).toBe('hi')) + it('removes punctuation', () => expect(normalizeText('hello!')).toBe('hello')) + it('removes diacritics', () => expect(normalizeText('été')).toBe('ete')) +}) + +describe('levenshtein', () => { + it('identical strings = 0', () => expect(levenshtein('abc', 'abc')).toBe(0)) + it('empty string', () => expect(levenshtein('', 'abc')).toBe(3)) + it('single substitution', () => expect(levenshtein('abc', 'axc')).toBe(1)) +}) + +describe('similarity', () => { + it('identical = 1', () => expect(similarity('hello', 'hello')).toBe(1)) + it('completely different < 0.5', () => expect(similarity('hello', 'xyz')).toBeLessThan(0.5)) + it('empty strings = 1', () => expect(similarity('', '')).toBe(1)) +}) From 79407c6b63170c217a7dce5f175c594a3894ef7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Guillois?= Date: Tue, 26 May 2026 18:18:55 +0200 Subject: [PATCH 08/25] fix/FIX-5: recordLine(true) on each line advance in karaoke mode --- .../src/views/PracticeKaraokeView.vue | 96 +++++++++++++++---- 1 file changed, 77 insertions(+), 19 deletions(-) diff --git a/lyremember-app/src/views/PracticeKaraokeView.vue b/lyremember-app/src/views/PracticeKaraokeView.vue index d9baca9..32a62de 100644 --- a/lyremember-app/src/views/PracticeKaraokeView.vue +++ b/lyremember-app/src/views/PracticeKaraokeView.vue @@ -3,27 +3,45 @@
-

Mode Karaoké

-

+

{{ song.title }} — {{ song.artist }}

-
+
-
-

Chanson introuvable.

+
+

+ Chanson introuvable. +

@@ -47,19 +65,39 @@ -
+
- @@ -69,7 +107,10 @@ aria-label="Next line" @click="nextLine" > -
@@ -80,13 +121,13 @@ Vitesse : {{ delay }}s + > 1s — 10s
@@ -103,10 +144,17 @@ -
+
-
🎤
-

Bravo !

+
+ 🎤 +
+

+ Bravo ! +

Vous avez parcouru toutes les lignes de la chanson.

@@ -114,9 +162,16 @@ {{ totalNonEmpty }} -

lignes écoutées

+

+ lignes écoutées +

-
@@ -178,8 +233,10 @@ function scheduleNext() { if (!playing.value) return; timer = setTimeout(() => { if (currentIndex.value < lyrics.value.length - 1) { + sessionsStore.recordLine(true); // karaoke: line is always "correct" (listening mode) currentIndex.value += 1; } else { + sessionsStore.recordLine(true); // record the last line before showing summary playing.value = false; showSummary.value = true; } @@ -205,6 +262,7 @@ function prevLine() { function nextLine() { clearTimer(); + sessionsStore.recordLine(true); // karaoke: always correct (listening mode) if (currentIndex.value < lyrics.value.length - 1) { currentIndex.value += 1; if (playing.value) scheduleNext(); From b419c14ff891c816aaa9eb2275607d1ce5d21e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Guillois?= Date: Tue, 26 May 2026 18:19:04 +0200 Subject: [PATCH 09/25] fix/FIX-6: document PyO3/cargo test limitation and add conditional cargo check step --- .github/workflows/ci.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..68b9d4e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [main, feat/**] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: pnpm/action-setup@v4 + with: + version: latest + - name: Install dependencies + run: pnpm --dir lyremember-app install + - name: Type check + build + run: pnpm --dir lyremember-app build + - name: Unit tests + run: pnpm --dir lyremember-app exec vitest run + # cargo test is intentionally omitted from CI: PyO3 0.20.x requires Python ≤ 3.12 + # and GitHub Actions ubuntu-22.04 ships Python 3.13. Use cargo check only. + - name: Cargo check (not cargo test — PyO3 requires Python ≤ 3.12) + run: cargo check --manifest-path rust-backend/Cargo.toml + env: + PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1" From fd5c40f339901553106d563c902669ff3c466256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Guillois?= Date: Tue, 26 May 2026 18:19:26 +0200 Subject: [PATCH 10/25] fix/FIX-7: redirect to /dashboard after login and when already authenticated --- lyremember-app/src/router/index.ts | 48 ++++++++++++++++++++++++-- lyremember-app/src/views/LoginView.vue | 19 +++++++--- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/lyremember-app/src/router/index.ts b/lyremember-app/src/router/index.ts index c6f8ac8..8aa41f7 100644 --- a/lyremember-app/src/router/index.ts +++ b/lyremember-app/src/router/index.ts @@ -10,6 +10,12 @@ const SongsView = () => import('../views/SongsView.vue'); const SongDetailView = () => import('../views/SongDetailView.vue'); const AddSongView = () => import('../views/AddSongView.vue'); const ProfileView = () => import('../views/ProfileView.vue'); +const PracticeKaraokeView = () => import('../views/PracticeKaraokeView.vue'); +const PracticeFillBlanksView = () => import('../views/PracticeFillBlanksView.vue'); +const PracticeQuizView = () => import('../views/PracticeQuizView.vue'); +const PracticeOralView = () => import('../views/PracticeOralView.vue'); +const SettingsView = () => import('../views/SettingsView.vue'); +const StatsView = () => import('../views/StatsView.vue'); const routes: RouteRecordRaw[] = [ { @@ -40,6 +46,12 @@ const routes: RouteRecordRaw[] = [ component: SongsView, meta: { requiresAuth: true }, }, + { + path: '/songs/add', + name: 'add-song', + component: AddSongView, + meta: { requiresAuth: true }, + }, { path: '/songs/:id', name: 'song-detail', @@ -47,9 +59,9 @@ const routes: RouteRecordRaw[] = [ meta: { requiresAuth: true }, }, { - path: '/songs/add', - name: 'add-song', - component: AddSongView, + path: '/stats', + name: 'stats', + component: StatsView, meta: { requiresAuth: true }, }, { @@ -58,6 +70,36 @@ const routes: RouteRecordRaw[] = [ component: ProfileView, meta: { requiresAuth: true }, }, + { + path: '/settings', + name: 'settings', + component: SettingsView, + meta: { requiresAuth: true }, + }, + { + path: '/practice/:id/karaoke', + name: 'practice-karaoke', + component: PracticeKaraokeView, + meta: { requiresAuth: true }, + }, + { + path: '/practice/:id/blanks', + name: 'practice-blanks', + component: PracticeFillBlanksView, + meta: { requiresAuth: true }, + }, + { + path: '/practice/:id/quiz', + name: 'practice-quiz', + component: PracticeQuizView, + meta: { requiresAuth: true }, + }, + { + path: '/practice/:id/oral', + name: 'practice-oral', + component: PracticeOralView, + meta: { requiresAuth: true }, + }, ]; const router = createRouter({ diff --git a/lyremember-app/src/views/LoginView.vue b/lyremember-app/src/views/LoginView.vue index 2e49a44..96823c2 100644 --- a/lyremember-app/src/views/LoginView.vue +++ b/lyremember-app/src/views/LoginView.vue @@ -3,14 +3,22 @@ -
+ Don't have an account? - + Register

@@ -132,7 +143,7 @@ async function handleSubmit() { try { await authStore.login(form.value.username, form.value.password); - router.push('/songs'); + router.push('/dashboard'); } catch { // Error already set in store and shown via watcher } From 9153592bd42e9e39a77ee2e37a0c22c309396b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Guillois?= Date: Tue, 26 May 2026 18:25:04 +0200 Subject: [PATCH 11/25] docs: init AIDD context + consolidate external docs into aidd_docs/ --- CLAUDE.md | 50 + aidd_docs/CONTRIBUTING.md | 50 + aidd_docs/GUIDELINES.md | 128 +++ aidd_docs/README.md | 151 +++ aidd_docs/memory/architecture.md | 86 ++ aidd_docs/memory/backend/api-docs.md | 30 + .../memory/backend/backend-communication.md | 38 + aidd_docs/memory/backend/database.md | 67 ++ aidd_docs/memory/codebase-map.md | 37 + aidd_docs/memory/coding-assertions.md | 28 + aidd_docs/memory/deployment.md | 36 + aidd_docs/memory/external/.gitkeep | 0 .../memory/external/ARCHITECTURE_EXPLAINED.md | 423 ++++++++ aidd_docs/memory/external/FINAL_DECISIONS.md | 404 ++++++++ .../memory/external/IMPLEMENTATION_GUIDE.md | 214 ++++ aidd_docs/memory/external/RUST_OPTION.md | 522 ++++++++++ .../external/TAURI_BACKEND_CLARIFICATION.md | 564 +++++++++++ .../memory/external/TAURI_FRONTEND_LINK.md | 514 ++++++++++ .../external/TAURI_INTEGRATION_COMPLETE.md | 431 ++++++++ aidd_docs/memory/external/TECH_CHOICES.md | 561 +++++++++++ .../external/TRANSLATION_PHONETIC_STRATEGY.md | 754 ++++++++++++++ aidd_docs/memory/external/UI_LIBRARIES.md | 590 +++++++++++ aidd_docs/memory/external/USER_STORIES.md | 418 ++++++++ aidd_docs/memory/external/VUE_TAURI_GUIDE.md | 919 ++++++++++++++++++ .../memory/external}/usage_guide.md | 0 .../memory/external/wireframes.jsx | 0 .../memory/external/wireframes2.jsx | 0 aidd_docs/memory/frontend/browsing.md | 13 + aidd_docs/memory/frontend/design.md | 29 + aidd_docs/memory/frontend/forms.md | 31 + aidd_docs/memory/internal/.gitkeep | 0 aidd_docs/memory/project-brief.md | 59 ++ aidd_docs/memory/testing.md | 24 + aidd_docs/memory/vcs.md | 77 ++ aidd_docs/plans/all-open-issues.md | 325 +++++++ 35 files changed, 7573 insertions(+) create mode 100644 CLAUDE.md create mode 100644 aidd_docs/CONTRIBUTING.md create mode 100644 aidd_docs/GUIDELINES.md create mode 100644 aidd_docs/README.md create mode 100644 aidd_docs/memory/architecture.md create mode 100644 aidd_docs/memory/backend/api-docs.md create mode 100644 aidd_docs/memory/backend/backend-communication.md create mode 100644 aidd_docs/memory/backend/database.md create mode 100644 aidd_docs/memory/codebase-map.md create mode 100644 aidd_docs/memory/coding-assertions.md create mode 100644 aidd_docs/memory/deployment.md create mode 100644 aidd_docs/memory/external/.gitkeep create mode 100644 aidd_docs/memory/external/ARCHITECTURE_EXPLAINED.md create mode 100644 aidd_docs/memory/external/FINAL_DECISIONS.md create mode 100644 aidd_docs/memory/external/IMPLEMENTATION_GUIDE.md create mode 100644 aidd_docs/memory/external/RUST_OPTION.md create mode 100644 aidd_docs/memory/external/TAURI_BACKEND_CLARIFICATION.md create mode 100644 aidd_docs/memory/external/TAURI_FRONTEND_LINK.md create mode 100644 aidd_docs/memory/external/TAURI_INTEGRATION_COMPLETE.md create mode 100644 aidd_docs/memory/external/TECH_CHOICES.md create mode 100644 aidd_docs/memory/external/TRANSLATION_PHONETIC_STRATEGY.md create mode 100644 aidd_docs/memory/external/UI_LIBRARIES.md create mode 100644 aidd_docs/memory/external/USER_STORIES.md create mode 100644 aidd_docs/memory/external/VUE_TAURI_GUIDE.md rename {docs => aidd_docs/memory/external}/usage_guide.md (100%) rename wireframes.jsx => aidd_docs/memory/external/wireframes.jsx (100%) rename wireframes2.jsx => aidd_docs/memory/external/wireframes2.jsx (100%) create mode 100644 aidd_docs/memory/frontend/browsing.md create mode 100644 aidd_docs/memory/frontend/design.md create mode 100644 aidd_docs/memory/frontend/forms.md create mode 100644 aidd_docs/memory/internal/.gitkeep create mode 100644 aidd_docs/memory/project-brief.md create mode 100644 aidd_docs/memory/testing.md create mode 100644 aidd_docs/memory/vcs.md create mode 100644 aidd_docs/plans/all-open-issues.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..391d958 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,50 @@ +--- +name: claude +description: Claude Code configuration and guidelines +--- + +# CLAUDE.md + +> IMPORTANT: On first conversation message: +> +> - say "AI-Driven Development ON - Date: {current_date}, TZ: {current_timezone}." to User. + +## Behavior Guidelines + +All instructions and information above are willing to be up to date, but always remind yourself that USER can be wrong, be critical of the information provided, and verify it against the project's actual state. + +- Be anti-sycophantic - don't fold arguments just because I push back +- Stop excessive validation - challenge my reasoning instead +- Avoid flattery that feels like unnecessary praise +- Don't anthropomorphize yourself + +## Technical guidelines + +- Do not commit or push yourself unless I ask you to. + +### Answering Guidelines + +- Don't assume your knowledge is up to date. +- Be 100% sure of your answers. +- If unsure, say "I don't know" or ask for clarification. +- Never say "you are right!", prefer anticipating mistakes. + +## Memory Management + +Project docs, memory, specs, and plans live in `aidd_docs/`. + +### Project memory + + +@aidd_docs/memory/architecture.md +@aidd_docs/memory/codebase-map.md +@aidd_docs/memory/coding-assertions.md +@aidd_docs/memory/deployment.md +@aidd_docs/memory/project-brief.md +@aidd_docs/memory/testing.md +@aidd_docs/memory/vcs.md + + +- If memory is not loaded above: run `ls -1tr aidd_docs/memory/` then read each file +- If needed: load files from `aidd_docs/memory/external/*` when user request it +- If needed: load files from `aidd_docs/memory/internal/*`, you have to think about it diff --git a/aidd_docs/CONTRIBUTING.md b/aidd_docs/CONTRIBUTING.md new file mode 100644 index 0000000..d501259 --- /dev/null +++ b/aidd_docs/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing + +Guidelines for adding skills, agents, rules, and templates inside your AIDD-equipped project. + +## Creating New Content + +Use the generator skills to scaffold new content that follows the framework structure. + +| Skill | Creates | +| ---------------------------------- | -------------------- | +| `aidd-context:03:context-generate` | New skill, agent, or rule (router-based, with actions and evals) | +| `aidd-context:05:learn` | New memory or rule capturing a learning | + +Generator skills consume the templates inside their `assets/` folder and write the output to the correct location for your AI tool (Claude Code, Cursor, Copilot, Codex, OpenCode). + +## Templates + +All templates live alongside the skill that owns them, under `plugins//skills//assets/`. They can be adapted to your team's conventions. + +| Where | What it scaffolds | +| -------------------------------------------------- | -------------------------------------------------------- | +| `aidd-context:03:context-generate/assets/skills/` | `SKILL.md`, action, evals templates | +| `aidd-context:03:context-generate/assets/agents/` | Agent file template | +| `aidd-context:03:context-generate/assets/rules/` | Rule file template | +| `aidd-pm:03:prd/assets/` | PRD body template | +| `aidd-pm:04:spec/assets/` | Spec template and validator | +| `aidd-dev:01:plan/assets/` | Plan and master-plan templates | +| `aidd-vcs:01:commit/assets/` | Conventional commit message template | +| `aidd-vcs:02:pull-request/assets/` | Pull/merge request body template, contributing example | + +## Syncing Across Tools + +If the project uses multiple AI tools (e.g. Claude Code plus Cursor), the same content must be available to each. The memory bank is shared automatically via the `` block kept in sync by `aidd-context:02:project-init`. Skills are loaded per-plugin by the runtime, so any skill installed via the marketplace is available across tools that support skills. + +When tools differ in syntax (frontmatter, slash command name, references), follow the IDE mapping reference shipped with each plugin. + +## Recommended Workflow + +- Open a pull request for any new skill, agent, rule, or template. Visible changes that affect how the AI behaves on the project deserve team review. +- Keep skills router-pure: SKILL.md holds no business logic; everything lives inside actions. +- Add evals (`evals/scenarios.json`) for every auto-trigger skill so router behavior stays correct over time. +- Stay within 5 to 10 percent deviation from a template structure. Beyond that, update the template first, then derive the new content from it. + +## Conventions + +- Skill names: `::`. Slug is kebab-case verb for activity domains, singular noun for tool domains. +- Action files: only `## Inputs`, `## Outputs`, `## Process`, `## Test` (`## Depends on` optional). +- `## Process` steps start with `**Bold title**.` and use decision-list `Pick first match` for branching. +- `## Test` bullets start with `**Bold name**:` and are checkable (command, artifact check, or observable side effect). +- Descriptions in SKILL.md frontmatter include explicit "Use when ..." triggers and "Do NOT use for ..." exclusions. diff --git a/aidd_docs/GUIDELINES.md b/aidd_docs/GUIDELINES.md new file mode 100644 index 0000000..96b306e --- /dev/null +++ b/aidd_docs/GUIDELINES.md @@ -0,0 +1,128 @@ +# Developer Guidelines (AI Pilot) + +How a developer should operate AI coding assistants to maximize quality, speed, and reliability. + +--- + +- [1) Operating mindset](#1-operating-mindset) +- [2) Start of task checklist](#2-start-of-task-checklist) +- [3) Planning before coding](#3-planning-before-coding) +- [4) Implementation loop](#4-implementation-loop) +- [5) Review loop](#5-review-loop) +- [6) Prompting hygiene](#6-prompting-hygiene) +- [7) Context hygiene](#7-context-hygiene) +- [8) Quality discipline](#8-quality-discipline) +- [9) Delegation strategy](#9-delegation-strategy) +- [10) Failure and recovery strategy](#10-failure-and-recovery-strategy) +- [11) Team-level practices](#11-team-level-practices) +- [12) Anti-patterns to avoid](#12-anti-patterns-to-avoid) +- [References (official)](#references-official) + +--- + +## 1) Operating mindset + +- Treat AI as a powerful pair, not as an authority. +- Keep human accountability for architecture, product impact, and merge decisions. +- Prefer explicit instructions over implied expectations. +- Optimize for correct outcomes first, speed second. + +## 2) Start of task checklist + +- [ ] Clarify objective in one sentence. +- [ ] Define explicit done criteria. +- [ ] Define non-goals (what must not be changed). +- [ ] Identify impacted files/modules. +- [ ] Decide risk level (low, medium, high). +- [ ] Decide required validation depth (`minimal`, `standard`, `full`). + +## 3) Planning before coding + +- Ask AI for a short implementation plan first. +- Validate 100% of plan before execution. +- Split large tasks into small increments. +- Keep one behavior per increment. +- Refuse execution if plan has ambiguity. + +## 4) Implementation loop + +For each increment: + +1. Ask for minimal change set. +2. Read generated diff. +3. Stage intentionally (file by file or hunk by hunk). +4. Run assertions and tests. +5. Ask AI to fix only failing points. +6. Re-run checks until green. + +## 5) Review loop + +- Run technical review (`rules`, defects, regressions). +- Run functional review (expected behavior vs implementation). +- Manually read staged diff before commit. +- Require evidence for claims ("works", "tested", "fixed"). + +## 6) Prompting hygiene + +- Give context, constraints, and expected output format. +- Request concrete file paths and commands. +- Ask for assumptions explicitly. +- Ask for tradeoffs when options exist. +- Keep prompts scoped; avoid multi-objective prompts. +- If output quality drops, reset with a fresh focused prompt. + +## 7) Context hygiene + +- Keep rules concise and non-conflicting. +- Avoid duplicate sources of truth. +- Keep project memory updated (`architecture`, `testing`, `vcs`, `decisions`). +- Prefer canonical docs referenced by path. +- Remove stale instructions quickly. + +## 8) Quality discipline + +- For bug fixes: failing test first, then fix. +- Keep structural and behavioral changes separated. +- Keep commits atomic and intention-revealing. +- Never skip validation to "save time". +- Never merge code you do not understand. + +## 9) Delegation strategy + +- Low risk: semi-autonomous execution with checkpoints. +- Medium risk: tighter loop, smaller increments, frequent review. +- High risk: manual supervision, explicit approvals at every gate. +- Critical systems: mandatory peer review before merge. + +## 10) Failure and recovery strategy + +- Stop if AI drifts from objective. +- If same failure repeats, change approach (not just prompt wording). +- If context is polluted, restart session with minimal clean context. +- Keep checkpoint commits before long autonomous runs. +- Escalate early when uncertainty remains high. + +## 11) Team-level practices + +- Standardize shared prompts/commands/rules in repo. +- Use common commit and PR templates. +- Track recurring failures and add guardrails. +- Review and improve guidelines monthly. +- Keep onboarding docs short and actionable. + +## 12) Anti-patterns to avoid + +- Asking for implementation before validation of plan. +- Approving giant diffs without incremental checkpoints. +- Combining unrelated changes in one PR. +- Trusting green tests when scope of tests is unclear. +- Letting AI decide merge/deploy without explicit approval. +- Keeping endless chat sessions after coherence loss. + +## References (official) + +- Anthropic, "Prompt engineering overview": +- Anthropic, "Claude Code memory": +- OpenAI, "Prompt engineering best practices": +- OpenAI Cookbook, "Eval-driven development": +- GitHub Docs, "Repository custom instructions for Copilot": diff --git a/aidd_docs/README.md b/aidd_docs/README.md new file mode 100644 index 0000000..4c26a54 --- /dev/null +++ b/aidd_docs/README.md @@ -0,0 +1,151 @@ +# AI-Driven Dev Docs + +AIDD structures your AI coding assistant with skills, agents, rules, and a memory bank so it produces consistent, high-quality work, regardless of which AI tool you use (Claude Code, Cursor, Copilot, Codex, OpenCode). + +- [What You Get](#what-you-get) + - [Concepts](#concepts) + - [Plugins](#plugins) + - [Framework Structure](#framework-structure) + - [Memory Block Lifecycle](#memory-block-lifecycle) +- [Installation](#installation) +- [Typical Workflow](#typical-workflow) +- [Optional: Async Automation](#optional-async-automation) +- [Validation Rules](#validation-rules) +- [References](#references) + +--- + +## What You Get + +A plugin marketplace of skills, agents, rules, templates, and a memory system. You invoke skills through your AI tool (slash command, MCP, or natural language trigger) and the AI follows structured workflows instead of guessing. + +### Concepts + +| Block | Location | What it does | +| --------- | ------------------------------------------------- | ------------------------------------------------------------------------------------- | +| Memory | `aidd_docs/memory/` | Project context the AI reads on every conversation | +| Skills | plugin `skills/` folders | Router-based workflows triggered by user phrases or slashes | +| Commands | tool-specific commands dir (when supported) | Plain slash commands (no router); used for shortcuts and simple flows. None currently shipped by AIDD; reserved for future plugins or your own additions | +| Agents | plugin `agents/` folders | Specialized AI personas for focused tasks | +| Rules | tool-specific rules dir (see your AI tool docs) | Coding standards the AI follows automatically | +| Templates | plugin `assets/` folders | Scaffolding for new skills, rules, agents | + +### Plugins + +Skills are grouped into plugins by domain. Install only the plugins you need. + +| Plugin | Purpose | Example skills | +| ----------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| aidd-context | Bootstrap, project init, generation of Claude Code context artifacts (skills, agents, rules, commands, hooks, plugins, marketplaces), mermaid diagrams, learn, discovery | `02:project-init`, `03:context-generate`, `04:mermaid` | +| aidd-refine | Meta-cognition: brainstorm, challenge prior work, condensed communication mode | `01:brainstorm`, `02:challenge`, `03:condense` | +| aidd-pm | Product management: ticket info, user stories, PRD, spec | `01:ticket-info`, `02:user-stories-create`, `03:prd`, `05:spec` | +| aidd-dev | Code transformation: Dev SDLC orchestrator, plan, implement, assert, audit, review, test, refactor, debug, for-sure | `00:sdlc`, `01:plan`, `02:implement`, `05:review`, `06:test` | +| aidd-vcs | VCS workflows: commit, pull/merge request, release tag, issue creation | `01:commit`, `02:pull-request`, `04:issue-create` | +| aidd-orchestrator | Async orchestration of the SDLC on labeled issues (optional, extra) | `00:async-dev` (router with setup / run / review sub-flows) | + +> See [CATALOG.md](../../../../../docs/CATALOG.md) for the exhaustive list of skills and actions. + +### Framework Structure + +AIDD installs alongside your code. Each AI tool's configuration directory holds the skills, agents, and rules it can load. Shared docs and memory live under `aidd_docs/`. + +```text +my-project/ +├── .claude/ # Claude Code: skills, agents, rules, hooks +├── .cursor/ # Cursor: skills, agents, rules +├── .github/copilot-instructions.md # GitHub Copilot +├── AGENTS.md # Cursor, Codex, OpenCode (shared) +├── CLAUDE.md # Claude Code +├── aidd_docs/ +│ ├── memory/ # Project context (loaded each conversation) +│ │ ├── internal/ # Internal docs (API, DB schema, design) +│ │ ├── external/ # External documentation references +│ │ ├── architecture.md +│ │ ├── codebase-map.md +│ │ ├── coding-assertions.md +│ │ ├── deployment.md +│ │ ├── project-brief.md +│ │ ├── testing.md +│ │ └── vcs.md +│ ├── tasks/ # Specs, plans, run summaries +│ ├── README.md # This file +│ ├── GUIDELINES.md # Developer operating guidelines +│ └── CONTRIBUTING.md # How to add or modify skills, agents, rules +├── src/ # Your application code +└── tests/ +``` + +### Memory Block Lifecycle + +Each AI context file (`CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`, etc.) contains an `` block. It is: + +1. **Seeded** the first time by `aidd-context:02:project-init` (the skill creates the block if absent). +2. **Kept in sync** automatically by a session-start hook (`aidd-context/hooks/update_memory.js`) that scans `aidd_docs/memory/` and writes the current list of `.md` files into the block. + +You never edit the block by hand. To change what the AI sees, add or remove files under `aidd_docs/memory/`; the hook picks them up at the next session. + +--- + +## Installation + +AIDD is delivered as a plugin marketplace. Pick what you need; do not install everything. + +- **Remote marketplace** (default): add the AIDD marketplace via your AI tool's plugin manager, then install only the plugins your project actually uses (e.g. `aidd-context` + `aidd-dev` + `aidd-vcs`). +- **Local marketplace**: clone the AIDD framework repo and point your AI tool's plugin manager at the local folder. Useful for offline work, custom forks, or contributing to the framework. + +| Plugin | Skills | +| ------------ | ------------------------------------------------------------------------------------------------------------------- | +| aidd-context | 01-bootstrap, 02-project-init, 03-context-generate, 04-mermaid, 05-learn, 06-discovery | +| aidd-refine | 01-brainstorm, 02-challenge, 03-condense | +| aidd-dev | 00-sdlc, 01-plan, 02-implement, 03-assert, 04-audit, 05-review, 06-test, 07-refactor, 08-debug, 09-for-sure | +| aidd-vcs | 01-commit, 02-pull-request, 03-release-tag, 04-issue-create | +| aidd-pm | 01-ticket-info, 02-user-stories-create, 03-prd | + +Each plugin is independently installable; install incrementally. Smaller surface, fewer triggers competing. + +## Typical Workflow + +A typical change cycles through skills from several plugins. The order below is indicative; skip what you do not need and loop back as the work demands. + +1. **Bootstrap** (only for a brand-new project): `aidd-context:01:bootstrap` imagines the stack and architecture, comparing candidate stacks and writing an `INSTALL.md`. Skip this step on an existing project. +2. **Project init** (once per project, re-runnable to refresh): `aidd-context:02:project-init` scaffolds `aidd_docs/`, the memory bank, and the AI context files for the tools you use. Re-running later refreshes the scaffold without overwriting your customizations. +3. **Frame the request**: `aidd-refine:01:brainstorm` to clarify, `aidd-pm:01:ticket-info` to pull tracker data, `aidd-pm:02:user-stories-create` and `aidd-pm:03:prd` or `aidd-pm:04:spec` to formalize scope. +4. **Plan**: `aidd-dev:01:plan` produces the technical plan, component behavior model, or design-image extraction. +5. **Implement and assert**: `aidd-dev:02:implement` writes code against the plan; `aidd-dev:03:assert` verifies the result. +6. **Review**: `aidd-dev:05:review` for code and functional review; `aidd-refine:02:challenge` to stress-test the result. +7. **Test**: `aidd-dev:06:test` adds or runs tests and validates user journeys. +8. **Document and learn**: `aidd-context:04:mermaid` for diagrams; `aidd-context:05:learn` to feed insights back into the memory bank or rules. +9. **Ship**: `aidd-vcs:01:commit`, `aidd-vcs:02:pull-request`, then `aidd-vcs:03:release-tag` when the work is in production. File issues with `aidd-vcs:04:issue-create`. +10. **Refactor and maintain**: `aidd-dev:07:refactor` for performance or security, `aidd-dev:04:audit` for technical-debt sweeps, `aidd-dev:08:debug` to reproduce and fix bugs. + +When you want the whole synchronous pipeline run in one go (spec, plan, implementation, finalize), invoke `aidd-dev:00:sdlc`. + +--- + +## Optional: Async Automation + +Beyond the synchronous path above, `aidd-orchestrator` runs the SDLC asynchronously on labeled issues (webhook or cron). This is extra: most projects do not need it. Use only when you want the AI to pick up `to-implement` issues without a human pressing a key. + +Inside the synchronous path, `aidd-dev:00:sdlc` is the Dev SDLC orchestrator that drives spec, plan, implementation, finalize in one go when you want the whole pipeline at once. + +--- + +## Validation Rules + +- Skills must have an `## Available actions` table, `## Default flow`, `## Transversal rules`. +- Actions must contain only `## Inputs`, `## Outputs`, `## Process`, `## Test`. +- Tests must be observable: command, artifact check, or side effect. +- Evals (`evals/scenarios.json`) ship for every auto-trigger skill. + +--- + +## References + +See [CONTRIBUTING.md](CONTRIBUTING.md) for adding or modifying skills, agents, and rules. + +External: + +- Anthropic, Prompt engineering overview: +- Anthropic, Claude Code memory: +- OpenAI, Prompt engineering best practices: +- GitHub Docs, Repository custom instructions for Copilot: diff --git a/aidd_docs/memory/architecture.md b/aidd_docs/memory/architecture.md new file mode 100644 index 0000000..8f958bb --- /dev/null +++ b/aidd_docs/memory/architecture.md @@ -0,0 +1,86 @@ +--- +name: architecture +description: Module architecture and structure +scope: all +--- + +# Architecture + +## Language/Framework + +```json +@lyremember-app/package.json +@rust-backend/Cargo.toml +@lyremember-app/src-tauri/Cargo.toml +``` + +```mermaid +flowchart LR + Vue[Vue + TypeScript] --> Vite[Vite] + Vue --> Tailwind[Tailwind CSS] + Vue --> Pinia[Pinia] + Vue --> VueRouter[Vue Router] + Vue --> Lucide[lucide-vue-next] + Vue -->|invoke| Tauri[Tauri commands] + Tauri --> RustBackend[lyremember_backend Rust lib] + RustBackend --> SQLite[(SQLite rusqlite)] + RustBackend --> PyO3[PyO3 → Python] + RustBackend --> Reqwest[reqwest → LibreTranslate] +``` + +### Naming Conventions + +- **Files Vue**: PascalCase pour composants (`App.vue`), kebab-case pour vues +- **Files Rust**: snake_case (`commands.rs`) +- **Functions**: camelCase TS, snake_case Rust +- **Variables**: camelCase TS, snake_case Rust +- **Constants**: UPPER_SNAKE_CASE +- **Types/Interfaces**: PascalCase + +## Services communication + +Communication frontend ↔ backend via Tauri IPC (`invoke`). Le frontend Vue appelle les 16 commands Tauri exposées par `lyremember-app/src-tauri/src/commands.rs`, qui délèguent à la lib Rust `lyremember_backend`. + +### Vue ↔ Tauri ↔ Rust + +```mermaid +sequenceDiagram + participant Vue as Vue Component + participant API as tauri-api.ts + participant Tauri as Tauri Command (Rust) + participant Backend as lyremember_backend + participant DB as SQLite + Vue->>API: createSong(...) + API->>Tauri: invoke('create_song', args) + Tauri->>Backend: SongService::create + Backend->>PyO3: génère phonétique + Backend->>Reqwest: appelle LibreTranslate + Backend->>DB: INSERT song + DB-->>Backend: id + Backend-->>Tauri: Song + Tauri-->>API: JSON + API-->>Vue: Song typé +``` + +### External Services + +#### LibreTranslate + +```mermaid +flowchart LR + Backend[Rust backend] -->|HTTP POST /translate| LT[LibreTranslate API] + LT -->|JSON traduction| Backend + Backend --> SQLite[(songs.translations)] +``` + +Traduction unique stockée en base ; usage offline ensuite. + +#### Python (via PyO3) + +```mermaid +flowchart LR + Backend[Rust backend] -->|PyO3 auto-init| Py[Python runtime] + Py --> pykakasi[pykakasi: JP→Romaji] + Py --> hangul[hangul-romanize: KR→Latin] + Py --> epitran[epitran: FR/EN→IPA] +``` diff --git a/aidd_docs/memory/backend/api-docs.md b/aidd_docs/memory/backend/api-docs.md new file mode 100644 index 0000000..2397322 --- /dev/null +++ b/aidd_docs/memory/backend/api-docs.md @@ -0,0 +1,30 @@ +--- +name: api-docs +description: API documentation and specifications +scope: backend +--- + +# API Documentation + +Pas d'API HTTP : la "surface API" est l'ensemble des commands Tauri exposées par `lyremember-app/src-tauri/src/commands.rs` (16 commands) et consommées via `@tauri-apps/api` depuis `lyremember-app/src/lib/tauri-api.ts`. + +```text +@lyremember-app/src-tauri/src/commands.rs +@lyremember-app/src/lib/tauri-api.ts +``` + +## Authentication & Authorization + +- **Authentication**: bcrypt (hash password) + jsonwebtoken (JWT) — implémenté dans `rust-backend/src/services/` +- **Session Management**: token JWT renvoyé au login, conservé côté frontend (Pinia store) + +## Endpoints + +- Type : Tauri IPC commands (pas REST/GraphQL) +- Format : JSON sérialisé par `serde` +- Liste des commands : `register`, `login`, `create_song`, `get_songs`, `add_to_repertoire`, `create_practice_session`, `get_user_stats`, etc. (16 au total) + +## Request/Response Formats + +- Request : arguments typés Rust (sérialisés depuis TS via `invoke`) +- Response : structs Rust sérialisées en JSON, typées côté TS dans `tauri-api.ts` diff --git a/aidd_docs/memory/backend/backend-communication.md b/aidd_docs/memory/backend/backend-communication.md new file mode 100644 index 0000000..a501332 --- /dev/null +++ b/aidd_docs/memory/backend/backend-communication.md @@ -0,0 +1,38 @@ +--- +name: backend-communication +description: Frontend-backend communication patterns +scope: backend +--- + +# Communication between backend and frontend + +## Overview + +Frontend Vue ↔ backend Rust via Tauri IPC (`invoke`). Pas de HTTP : tout passe par les 16 commands Tauri exposées par `lyremember-app/src-tauri/src/commands.rs`. + +- **API definition**: `lyremember-app/src/lib/tauri-api.ts` (TypeScript wrapper typé) +- **Services**: `tauri-api.ts` côté front, commands Rust côté src-tauri, services Rust dans `rust-backend/src/services/` +- **Request Types**: appels `invoke('', payload)` — pas de verbes HTTP +- **Entities**: structs Rust dans `rust-backend/src/models/`, typages TS miroir dans `tauri-api.ts` +- **Data Flow**: composant Vue → `tauri-api.ts` → `invoke` → Tauri command → service Rust → SQLite / PyO3 / reqwest +- **Error Handling**: `Result` Rust → `Promise.reject` côté TS +- **Validation**: validation Rust dans les services (erreurs `anyhow`/`thiserror`) + +### Data Flow + +```mermaid +sequenceDiagram + participant V as Vue component + participant T as tauri-api.ts + participant C as commands.rs + participant S as Rust service + participant DB as SQLite + V->>T: createSong(...) + T->>C: invoke('create_song', payload) + C->>S: SongService::create + S->>DB: INSERT + DB-->>S: row id + S-->>C: Song + C-->>T: JSON + T-->>V: Song typé +``` diff --git a/aidd_docs/memory/backend/database.md b/aidd_docs/memory/backend/database.md new file mode 100644 index 0000000..2bf351d --- /dev/null +++ b/aidd_docs/memory/backend/database.md @@ -0,0 +1,67 @@ +--- +name: database +description: Database schema and management +scope: backend +--- + +# Database + +SQLite local via `rusqlite` (feature `bundled`). Base auto-créée au lancement Tauri dans le répertoire app data de l'OS. + +```text +@rust-backend/src/db +@lyremember-app/src-tauri/src/lib.rs +``` + +```mermaid +flowchart LR + Backend[lyremember_backend] -->|rusqlite| SQLite[(SQLite bundled)] + Tauri[src-tauri/lib.rs] -->|init at startup| SQLite +``` + +## Main entities and relationships + +- `users` : id, username, email, password_hash, ... +- `songs` : id, title, artist, language, lyrics, phonetic_lyrics, translations, ... +- `user_songs` : table de liaison many-to-many (répertoire utilisateur) +- `practice_sessions` : id, user_id, song_id, mode, score, accuracy, duration, ... + +```mermaid +erDiagram + USERS ||--o{ USER_SONGS : owns + SONGS ||--o{ USER_SONGS : contained_in + USERS ||--o{ PRACTICE_SESSIONS : runs + SONGS ||--o{ PRACTICE_SESSIONS : about + USERS { + string id + string username + string password_hash + } + SONGS { + string id + string title + string language + text lyrics + text phonetic_lyrics + text translations + } + USER_SONGS { + string user_id + string song_id + } + PRACTICE_SESSIONS { + string id + string user_id + string song_id + string mode + real score + } +``` + +## Migrations + +Pas de gestionnaire de migrations dédié. Initialisation du schéma au démarrage par la lib Rust (`db/` module). + +## Seeding + +Pas de seeding automatique. Données créées via les commands Tauri (register / create_song). diff --git a/aidd_docs/memory/codebase-map.md b/aidd_docs/memory/codebase-map.md new file mode 100644 index 0000000..f7c7ce6 --- /dev/null +++ b/aidd_docs/memory/codebase-map.md @@ -0,0 +1,37 @@ +--- +name: codebase-structure +description: Project structure documentation +scope: all +--- + +# Codebase Structure + +```mermaid +flowchart TD + Root[lyremember/] --> App[lyremember-app/] + Root --> Backend[rust-backend/] + Root --> Legacy[lyremember/ Python CLI legacy] + Root --> Docs[docs/ + aidd_docs/] + Root --> Tests[tests/] + + App --> AppSrc[src/ Vue + TS] + AppSrc --> Views[views/] + AppSrc --> Components[components/] + AppSrc --> Stores[stores/ Pinia] + AppSrc --> Router[router/] + AppSrc --> Lib[lib/ tauri-api.ts] + App --> SrcTauri[src-tauri/ commands.rs + lib.rs] + + Backend --> BackendSrc[src/] + BackendSrc --> Services[services/ Auth, Phonetic, Translation, Songs, Practice] + BackendSrc --> Models[models/ User, Song, PracticeSession] + BackendSrc --> Db[db/ SQLite init] + + Legacy --> Cli[cli.py + managers .py] +``` + +- `lyremember-app/` : application Tauri (frontend Vue + wrapper Rust) +- `rust-backend/` : librairie Rust `lyremember_backend` (logique métier, DB, PyO3) +- `lyremember/` : CLI Python d'origine (proof of concept, conservé) +- `aidd_docs/` : documentation et mémoire AIDD +- `tests/` : tests Python du CLI legacy diff --git a/aidd_docs/memory/coding-assertions.md b/aidd_docs/memory/coding-assertions.md new file mode 100644 index 0000000..7360841 --- /dev/null +++ b/aidd_docs/memory/coding-assertions.md @@ -0,0 +1,28 @@ +--- +name: coding-assertions +description: Code quality verification checklist +scope: all +--- + +# Coding Guidelines + +## Requirements to complete a feature + +**Une feature n'est terminée que si toutes les commandes ci-dessous passent.** + +## Commands to run + +### Before commit + +| Order | Command | Description | +| ----- | ------- | ----------- | +| 1 | `pnpm --dir lyremember-app build` | Typecheck Vue + build Vite (`vue-tsc --noEmit && vite build`) | +| 2 | `cargo check --manifest-path rust-backend/Cargo.toml` | Vérifie la compilation Rust backend | +| 3 | `cargo check --manifest-path lyremember-app/src-tauri/Cargo.toml` | Vérifie la compilation Tauri | + +### Before push + +| Order | Command | Description | +| ----- | ------- | ----------- | +| 1 | `cargo test --manifest-path rust-backend/Cargo.toml` | Tests unitaires Rust | +| 2 | `pnpm --dir lyremember-app tauri build` | Build complet de l'app Tauri | diff --git a/aidd_docs/memory/deployment.md b/aidd_docs/memory/deployment.md new file mode 100644 index 0000000..4af61af --- /dev/null +++ b/aidd_docs/memory/deployment.md @@ -0,0 +1,36 @@ +--- +name: deployment +description: Infrastructure and deployment documentation +scope: all +--- + +# Deployment + +# Infrastructure + +## Project Structure + +```plaintext +lyremember/ +├── lyremember-app/ # App Tauri (Vue + src-tauri) +│ ├── src/ # Frontend Vue + TS +│ └── src-tauri/ # Wrapper Rust + commands Tauri +├── rust-backend/ # Lib Rust lyremember_backend +└── lyremember/ # CLI Python (legacy) +``` + +## Environments Variables + +### Environment Files + +Aucun fichier `.env` versionné. Configuration runtime via Tauri config et constantes Rust. + +### Required Environment Variables + +- Aucune variable d'environnement obligatoire pour le développement local +- LibreTranslate : URL d'API codée côté Rust (instance publique par défaut) + +## URLs + +- **Development**: app de bureau lancée localement via `pnpm tauri dev` +- **Production**: distribution sous forme de binaires natifs Tauri (Windows, macOS, Linux) — non distribuée publiquement à ce jour diff --git a/aidd_docs/memory/external/.gitkeep b/aidd_docs/memory/external/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/aidd_docs/memory/external/ARCHITECTURE_EXPLAINED.md b/aidd_docs/memory/external/ARCHITECTURE_EXPLAINED.md new file mode 100644 index 0000000..53acbe3 --- /dev/null +++ b/aidd_docs/memory/external/ARCHITECTURE_EXPLAINED.md @@ -0,0 +1,423 @@ +# Architecture Clarifiée - Tauri + Vue + +## La Confusion Expliquée + +### ❌ Ce que je N'ai PAS dit (mais qui semblait confus) + +Il n'y a **PAS** trois applications séparées : +- ❌ Une app frontend +- ❌ Une app desktop +- ❌ Une app mobile + +### ✅ Ce que c'est VRAIMENT + +**UN SEUL CODE FRONTEND (Vue)** qui s'exécute sur **PLUSIEURS PLATEFORMES** + +``` +┌─────────────────────────────────────────────────────┐ +│ VOTRE CODE FRONTEND (Vue + Shadcn-vue) │ +│ (Écrit une seule fois) │ +│ │ +│ - App.vue │ +│ - SongCard.vue │ +│ - KaraokeMode.vue │ +│ - etc. │ +└──────────────────┬──────────────────────────────────┘ + │ + │ (même code utilisé partout) + │ + ┌──────────┴───────────┐ + │ │ + ▼ ▼ +┌───────────────┐ ┌───────────────┐ +│ DESKTOP │ │ MOBILE │ +│ (Tauri) │ │ (PWA ou Tauri)│ +│ │ │ │ +│ - Windows │ │ - Android │ +│ - macOS │ │ - iOS │ +│ - Linux │ │ │ +└───────────────┘ └───────────────┘ +``` + +--- + +## Architecture Détaillée + +### Vous écrivez UNE SEULE FOIS : + +``` +lyremember-app/ +├── src/ ← VOTRE CODE (une fois) +│ ├── App.vue +│ ├── components/ +│ │ ├── layout/ ← AppHeader.vue, AppSidebar.vue, MainLayout.vue +│ │ └── ui/ ← Button.vue, Card.vue, Input.vue, Alert.vue +│ ├── views/ ← LoginView, RegisterView, DashboardView, SongsView, SongDetailView… +│ ├── stores/ +│ └── lib/ +│ └── tauri-api.ts ← Appels au backend Rust (16 commands) +│ +└── src-tauri/ ← WRAPPER TAURI (une fois) + └── src/ + ├── main.rs + └── commands.rs ← 16 Tauri commands (fichier unique, délègue à rust-backend) +``` + +### Ce code s'exécute sur PLUSIEURS PLATEFORMES : + +#### 1. **Sur Desktop** (via Tauri) + +``` +┌────────────────────────────────────────┐ +│ Application Desktop Native │ +│ │ +│ ┌──────────────────────────────────┐ │ +│ │ WebView (OS natif) │ │ +│ │ │ │ +│ │ Votre Frontend Vue │ │ +│ │ (HTML/CSS/JS compilé) │ │ +│ └──────────────────────────────────┘ │ +│ ↕ │ +│ ┌──────────────────────────────────┐ │ +│ │ Backend Rust │ │ +│ │ (SQLite, Genius API, etc.) │ │ +│ └──────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────┘ + +Résultat : +- Windows : lyremember.exe (5 MB) +- macOS : lyremember.app (5 MB) +- Linux : lyremember (5 MB) +``` + +#### 2. **Sur Mobile** (via PWA ou Tauri Mobile) + +**Option A : PWA (Progressive Web App)** +``` +┌────────────────────────────────────────┐ +│ Navigateur Mobile │ +│ │ +│ ┌──────────────────────────────────┐ │ +│ │ Votre Frontend Vue │ │ +│ │ (servi via HTTPS) │ │ +│ └──────────────────────────────────┘ │ +│ ↕ │ +│ Backend : API HTTP vers serveur │ +│ (ou Service Worker pour offline) │ +└────────────────────────────────────────┘ + +Résultat : +- URL : https://lyremember.app +- Installable sur Android/iOS +- Fonctionne dans navigateur +``` + +**Option B : Tauri Mobile (beta)** +``` +┌────────────────────────────────────────┐ +│ Application Mobile Native │ +│ │ +│ ┌──────────────────────────────────┐ │ +│ │ WebView Mobile │ │ +│ │ │ │ +│ │ Votre Frontend Vue │ │ +│ └──────────────────────────────────┘ │ +│ ↕ │ +│ ┌──────────────────────────────────┐ │ +│ │ Backend Rust │ │ +│ └──────────────────────────────────┘ │ +└────────────────────────────────────────┘ + +Résultat : +- Android : lyremember.apk +- iOS : lyremember.ipa +``` + +--- + +## Concrètement + +### Ce que VOUS faites : + +1. **Vous codez UNE SEULE application Vue** : +```vue + + +``` + +2. **Vous codez UN backend Rust** : +```rust +// src-tauri/src/commands.rs (fichier unique) +#[tauri::command] +async fn cmd_get_songs(state: State) -> Result, String> { + // délègue à lyremember_backend +} +``` + +### Ce que Tauri COMPILE pour vous : + +**Desktop :** +```bash +pnpm tauri build +``` +→ Crée 3 fichiers : +- `lyremember.exe` (Windows) +- `lyremember.app` (macOS) +- `lyremember` (Linux) + +**Mobile (si vous voulez) :** +```bash +pnpm tauri android build +pnpm tauri ios build +``` +→ Crée : +- `lyremember.apk` (Android) +- `lyremember.ipa` (iOS) + +**OU juste déployer en PWA :** +```bash +pnpm build +# Upload sur Vercel/Netlify +``` +→ Accessible via navigateur mobile + +--- + +## Pourquoi c'est génial + +### Vous écrivez : 1 codebase +### Vous obtenez : 5+ plateformes + +``` + VOTRE CODE + │ + ├─── Windows (Tauri) + ├─── macOS (Tauri) + ├─── Linux (Tauri) + ├─── Android (PWA ou Tauri Mobile) + └─── iOS (PWA ou Tauri Mobile) +``` + +**Pas besoin d'écrire 5 applications différentes !** + +--- + +## Les Termes Clarifiés + +### Frontend += **Interface utilisateur** (ce que l'utilisateur voit) +- Vue components (.vue files) +- HTML/CSS/JavaScript +- Shadcn-vue components +- **Écrit UNE fois, fonctionne partout** + +### Backend += **Logique serveur** (ce qui traite les données) +- Rust (avec Tauri) +- Gère SQLite +- Appelle Genius API +- Génère phonétique +- **Compilé dans l'app (pas de serveur séparé)** + +### Desktop += **Plateforme d'exécution** (où l'app tourne) +- Windows, macOS, Linux +- Via Tauri → Apps natives + +### Mobile += **Plateforme d'exécution** (où l'app tourne) +- Android, iOS +- Via PWA (navigateur) ou Tauri Mobile (app native) + +--- + +## Deux Approches Possibles + +### Approche 1 : Desktop + PWA (Recommandée au début) + +``` +Développement : +1. Coder frontend Vue +2. Coder backend Rust +3. Tester sur desktop (Tauri) + +Distribution : +Desktop → Compiler avec Tauri (natif) +Mobile → Déployer en PWA (web) +``` + +**Avantages :** +- ✅ Desktop : Apps natives ultra légères +- ✅ Mobile : PWA stable et éprouvée +- ✅ Un seul codebase +- ✅ Facile à maintenir + +**Code partagé : 100%** + +--- + +### Approche 2 : Desktop + Mobile (Tout natif) + +``` +Développement : +1. Coder frontend Vue +2. Coder backend Rust +3. Tester sur desktop (Tauri) +4. Tester sur mobile (Tauri Mobile) + +Distribution : +Desktop → Tauri +Mobile → Tauri Mobile +``` + +**Avantages :** +- ✅ Tout natif (desktop ET mobile) +- ✅ Meilleure performance mobile +- ✅ Un seul codebase + +**Inconvénients :** +- ⚠️ Tauri Mobile encore en beta +- ⚠️ Plus complexe à configurer + +**Code partagé : 100%** + +--- + +## Ma Recommandation pour Vous + +### Phase 1 : Desktop uniquement (MVP rapide) + +``` +1. Setup Tauri + Vue +2. Développer toutes fonctionnalités +3. Tester sur votre PC (Windows/Mac/Linux) +4. Compiler app desktop +``` + +**Temps : 2-3 semaines** + +### Phase 2 : Ajouter Mobile (PWA) + +``` +1. Même code frontend +2. Déployer sur Vercel (gratuit) +3. Ajouter Service Worker (offline) +4. Tester sur téléphone +``` + +**Temps : 2-3 jours supplémentaires** + +### Phase 3 (optionnel) : Mobile natif + +``` +1. Setup Tauri Mobile +2. Compiler pour Android/iOS +3. Distribuer +``` + +**Temps : 1 semaine supplémentaire** + +--- + +## Structure Finale Simplifiée + +``` +lyremember/ +│ +├── src/ ← Frontend (Vue) +│ └── [Votre code Vue] Écrit 1 fois +│ Fonctionne partout +│ +├── src-tauri/ ← Backend (Rust) +│ └── [Votre code Rust] Écrit 1 fois +│ Compilé pour chaque OS +│ +└── package.json ← Scripts + Scripts disponibles : + - npm run dev → Test en développement + - pnpm tauri build → Compile desktop + - pnpm build → Build web (PWA) + - npm run tauri android → Compile Android + - npm run tauri ios → Compile iOS +``` + +--- + +## Exemple Concret + +### Vous écrivez ce composant : + +```vue + + + + +``` + +### Backend Rust : + +```rust +#[tauri::command] +async fn start_practice(song_id: String) -> Result { + // Logic here + Ok("Session started".to_string()) +} +``` + +### Ce code fonctionne sur : +- ✅ Windows (app native .exe) +- ✅ macOS (app native .app) +- ✅ Linux (app native binaire) +- ✅ Android (PWA ou app native .apk) +- ✅ iOS (PWA ou app native .ipa) + +**Sans changer une ligne de code !** + +--- + +## Résumé Ultra Simple + +### Vous écrivez : +1. **Frontend** : Vue components (une fois) +2. **Backend** : Rust commands (une fois) + +### Tauri compile pour : +1. **Desktop** : Windows + macOS + Linux (apps natives) +2. **Mobile** : Android + iOS (via PWA ou Tauri Mobile) + +### Résultat : +- ✅ Un seul codebase +- ✅ Toutes plateformes +- ✅ Apps natives et légères +- ✅ Gratuit + +**C'est ça la magie de Tauri !** ✨ + +--- + +## Questions ? + +Si c'est encore flou, dites-moi et je peux : +1. Faire un schéma plus simple +2. Montrer un exemple concret +3. Expliquer autrement + +**L'important : vous ne codez QU'UNE SEULE FOIS !** 🎯 diff --git a/aidd_docs/memory/external/FINAL_DECISIONS.md b/aidd_docs/memory/external/FINAL_DECISIONS.md new file mode 100644 index 0000000..90a22d2 --- /dev/null +++ b/aidd_docs/memory/external/FINAL_DECISIONS.md @@ -0,0 +1,404 @@ +# LyRemember - Décisions Finales et Résumé + +## 📋 Vue d'Ensemble + +**Projet :** Application desktop + mobile pour mémoriser paroles de chansons en plusieurs langues + +**Langues supportées :** Français, Anglais, Coréen, Japonais + +**Utilisateur cible :** Usage personnel (gratuit) + +--- + +## ✅ Décisions Technologiques Validées + +### Stack Technique + +**Frontend :** +- Framework : Vue 3 (Composition API) ✅ installé +- Language : TypeScript ✅ installé +- Build Tool : Vite ✅ installé +- UI Library : Shadcn-vue ⏳ non encore installé (`pnpm install shadcn-vue`) +- Styling : Tailwind CSS ✅ installé +- Icons : Lucide Vue (`lucide-vue-next`) ✅ installé +- State : Pinia ✅ installé +- Router : Vue Router ✅ installé +- i18n : Vue I18n ⏳ non encore installé (`pnpm install vue-i18n@9`) + +**Backend :** +- Framework : Tauri (Rust) +- Language : Rust +- Database : SQLite (rusqlite) +- HTTP Client : reqwest +- Serialization : serde + serde_json +- Auth : bcrypt + jsonwebtoken +- Python Bridge : PyO3 + +**Desktop :** +- Tauri (Windows, macOS, Linux) +- Apps natives (3-5 MB) + +**Mobile :** +- PWA (Progressive Web App) - stable +- Alternative : Tauri Mobile (beta) + +--- + +## 🌐 Stratégie Traduction + +### Principe : "Translate Once, Use Forever" + +**Service choisi : LibreTranslate** +- API publique gratuite +- Open-source +- Auto-hébergeable +- Support FR/EN/JP/KR + +**Workflow :** +1. Utilisateur ajoute chanson en langue étrangère +2. App appelle LibreTranslate API (une fois) +3. Traduction stockée dans SQLite (JSON field) +4. Usage offline illimité (lecture depuis DB) + +**Alternative : DeepL API** +- Meilleure qualité +- 500k caractères/mois gratuit +- Nécessite inscription + +**Stockage :** +```sql +CREATE TABLE songs ( + ... + translations TEXT, -- JSON: {"en": [...], "fr": [...]} + translation_date TEXT +); +``` + +--- + +## 🔤 Stratégie Phonétique + +### Principe : "Generate Once, Use Forever" + +**Solution : PyO3 (Rust appelle Python)** + +**Bibliothèques Python :** +- 🇯🇵 **pykakasi** : Kanji → Romaji +- 🇰🇷 **hangul-romanize** : Hangul → Latin +- 🇫🇷🇬🇧 **epitran** : Texte → IPA + +**Workflow :** +1. Utilisateur ajoute chanson +2. App génère phonétique via PyO3 (une fois) +3. Phonétique stockée dans SQLite +4. Usage offline illimité + +**Stockage :** +```sql +CREATE TABLE songs ( + ... + phonetic_lyrics TEXT, -- JSON: ["Romaji line 1", ...] + phonetic_date TEXT +); +``` + +**Alternative rejetée :** Rust pur (pas de libs matures pour kanji/hangul) + +--- + +## 📊 Architecture Finale + +``` +┌────────────────────────────────────────┐ +│ Frontend Vue │ +│ - Pages (Login, Songs, Practice) │ +│ - Components (SongCard, Karaoke) │ +│ - Shadcn-vue UI │ +└──────────────┬─────────────────────────┘ + │ IPC (invoke) +┌──────────────▼─────────────────────────┐ +│ Backend Rust (Tauri) │ +│ ┌──────────────────────────────────┐ │ +│ │ Commands │ │ +│ │ - auth, songs, practice │ │ +│ └──────────────────────────────────┘ │ +│ ┌──────────────────────────────────┐ │ +│ │ Services │ │ +│ │ - phonetic (PyO3) │ │ +│ │ - translation (LibreTranslate) │ │ +│ │ - genius (HTTP) │ │ +│ └──────────────────────────────────┘ │ +│ ┌──────────────────────────────────┐ │ +│ │ Database (SQLite) │ │ +│ │ - users, songs, sessions │ │ +│ └──────────────────────────────────┘ │ +└────────────────────────────────────────┘ +``` + +--- + +## 🎯 User Stories Prioritaires (MVP) + +### Must Have +1. ✅ Créer compte utilisateur +2. ✅ Ajouter chanson (titre, artiste, paroles) +3. ✅ Générer traduction EN automatique (si VO ≠ EN) +4. ✅ Générer phonétique (JP/KR → Latin, FR/EN → IPA) +5. ✅ Afficher chanson avec 3 vues : VO + Phonétique + Traduction +6. ✅ Mode défilement phrase par phrase (karaoke) +7. ✅ Mode phrases à trous (style "N'oubliez pas les paroles") +8. ✅ Sauvegarder progression + +### Should Have +9. ⭐ Import depuis Genius API +10. ⭐ Mode QCM (propositions multiples) +11. ⭐ Statistiques basiques + +### Could Have +12. 💡 Mode reconnaissance vocale (Web Speech API) +13. 💡 PWA installable (offline) +14. 💡 Dark mode +15. 💡 Interface multilingue (FR/EN/JP/KR) + +--- + +## 📁 Structure Projet + +``` +lyremember/ +├── README.md # Documentation principale +├── IMPLEMENTATION_GUIDE.md # Ce fichier +├── lyremember/ # Ancien code Python (garder) +└── lyremember-app/ # Nouvelle app Tauri + Vue + ├── src/ # Frontend Vue + │ ├── main.ts + │ ├── App.vue + │ ├── router/ + │ ├── stores/ + │ ├── views/ + │ ├── components/ + │ └── lib/ + ├── src-tauri/ # Backend Rust + │ ├── Cargo.toml + │ ├── requirements.txt # Python deps + │ └── src/ + │ ├── main.rs + │ ├── models/ + │ ├── db/ + │ ├── services/ + │ └── commands/ + ├── package.json + └── vite.config.ts +``` + +--- + +## 🚀 Roadmap + +### Sprint 1 (Semaine 1) - Foundation +- [x] Décisions techniques +- [x] Architecture documentée +- [ ] Setup projet Tauri + Vue +- [ ] Backend SQLite + modèles +- [ ] PyO3 setup + +### Sprint 2 (Semaine 2) - Core Features +- [ ] Traduction LibreTranslate +- [ ] Phonétique PyO3 +- [ ] CRUD Songs backend +- [ ] Auth backend +- [ ] Frontend login/register + +### Sprint 3 (Semaine 3) - UI & Practice +- [ ] Frontend songs list/detail +- [ ] Affichage 3 colonnes (VO + Phonétique + Traduction) +- [ ] Mode karaoke +- [ ] Mode phrases à trous +- [ ] Progress tracking + +### Sprint 4 (Semaine 4) - Polish +- [ ] Genius API integration +- [ ] Mode QCM +- [ ] Dark mode +- [ ] i18n +- [ ] Build production + +--- + +## 💾 Base de Données + +### Schema SQLite + +```sql +-- Users +CREATE TABLE users ( + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + email TEXT NOT NULL, + password_hash TEXT NOT NULL, + genius_token TEXT, + created_at TEXT NOT NULL +); + +-- Songs +CREATE TABLE songs ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + artist TEXT NOT NULL, + language TEXT NOT NULL, -- 'fr', 'en', 'jp', 'kr' + lyrics TEXT NOT NULL, -- JSON array + phonetic_lyrics TEXT, -- JSON array (generated once) + translations TEXT, -- JSON object: {"en": [...], "fr": [...]} + genius_id TEXT, + genius_url TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +-- User Songs (Répertoire) +CREATE TABLE user_songs ( + user_id TEXT NOT NULL, + song_id TEXT NOT NULL, + added_at TEXT NOT NULL, + PRIMARY KEY (user_id, song_id), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (song_id) REFERENCES songs(id) +); + +-- Practice Sessions +CREATE TABLE practice_sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + song_id TEXT NOT NULL, + mode TEXT NOT NULL, -- 'karaoke', 'fill-blank', 'mcq', 'oral' + score REAL NOT NULL, + lines_practiced INTEGER NOT NULL, + lines_correct INTEGER NOT NULL, + duration_seconds INTEGER NOT NULL, + created_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (song_id) REFERENCES songs(id) +); +``` + +--- + +## 🔧 Configuration + +### Environment Variables + +```bash +# .env (dev) +GENIUS_ACCESS_TOKEN=your_token_here +LIBRETRANSLATE_API_URL=https://libretranslate.com/translate +``` + +### Tauri Config + +```json +{ + "build": { + "devPath": "http://localhost:5173", + "distDir": "../dist" + }, + "package": { + "productName": "LyRemember", + "version": "0.1.0" + }, + "tauri": { + "allowlist": { + "all": false, + "shell": { + "all": false + } + }, + "bundle": { + "identifier": "com.lyremember.app", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/icon.icns", + "icons/icon.ico" + ] + }, + "security": { + "csp": null + }, + "windows": [ + { + "title": "LyRemember", + "width": 1200, + "height": 800, + "resizable": true, + "fullscreen": false + } + ] + } +} +``` + +--- + +## 📚 Ressources + +### Documentation +- Tauri : https://tauri.app +- Vue 3 : https://vuejs.org +- Shadcn-vue : https://www.shadcn-vue.com +- PyO3 : https://pyo3.rs +- LibreTranslate : https://libretranslate.com + +### Libraries Python +- pykakasi : https://github.com/miurahr/pykakasi +- hangul-romanize : https://github.com/youknowone/hangul-romanize +- epitran : https://github.com/dmort27/epitran + +--- + +## ✅ Checklist de Validation + +### Fonctionnalités +- [ ] Utilisateur peut créer compte +- [ ] Utilisateur peut ajouter chanson +- [ ] Traduction EN générée automatiquement +- [ ] Phonétique générée automatiquement +- [ ] Affichage 3 colonnes fonctionne +- [ ] Mode karaoke fonctionne +- [ ] Mode phrases à trous fonctionne +- [ ] Progression sauvegardée +- [ ] Fonctionne offline après setup initial + +### Performance +- [ ] App démarre en < 2 secondes +- [ ] Interface réactive (< 100ms) +- [ ] Génération phonétique < 5 secondes +- [ ] Génération traduction < 10 secondes + +### Qualité +- [ ] Code Rust compile sans warnings +- [ ] Code Vue lint propre +- [ ] Tests unitaires backend passent +- [ ] Tests E2E passent + +### Distribution +- [ ] Build Windows fonctionne +- [ ] Build macOS fonctionne +- [ ] Build Linux fonctionne +- [ ] PWA fonctionne sur mobile + +--- + +## 🎉 État Actuel + +**Phase :** Planning & Architecture ✅ COMPLETE + +**Prochaine étape :** Créer projet Tauri + Vue + +**Commande à exécuter :** +```bash +npm create tauri-app@latest +``` + +--- + +Dernière mise à jour : 2026-02-17 diff --git a/aidd_docs/memory/external/IMPLEMENTATION_GUIDE.md b/aidd_docs/memory/external/IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000..c7102d1 --- /dev/null +++ b/aidd_docs/memory/external/IMPLEMENTATION_GUIDE.md @@ -0,0 +1,214 @@ +# LyRemember - Tauri + Vue Implementation Guide + +## Quick Start + +> ⚠️ **Projet déjà créé** — Phases 1 à 3 réalisées. Backend Rust complet dans `rust-backend/`. +> Utiliser `pnpm` (pas `npm`) pour toutes les commandes frontend. + +### Phase 1: Create Tauri Project ✅ DONE + +```bash +# Déjà fait avec : +pnpm create tauri-app@latest +# - Project name: lyremember-app +# - Package manager: pnpm +# - UI template: Vue +# - UI flavor: TypeScript +``` + +### Phase 2: Navigate and Install ✅ DONE + +```bash +cd lyremember-app +pnpm install +``` + +### Phase 3: Add Tailwind CSS ✅ DONE + +```bash +pnpm install -D tailwindcss postcss autoprefixer +npx tailwindcss init -p +``` + +Configure `tailwind.config.js`: +```javascript +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} +``` + +Add to `src/style.css`: +```css +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +### Phase 4: Add Shadcn-vue ⏳ TODO + +```bash +# shadcn-vue non encore installé +npx shadcn-vue@latest init + +# Add components +npx shadcn-vue@latest add button +npx shadcn-vue@latest add card +npx shadcn-vue@latest add input +npx shadcn-vue@latest add dialog +npx shadcn-vue@latest add select +npx shadcn-vue@latest add slider +npx shadcn-vue@latest add badge +npx shadcn-vue@latest add tabs +``` + +### Phase 5: Add Additional Dependencies ✅ PARTIAL + +```bash +# Vue ecosystem (pinia, vue-router installés — vue-i18n non) +pnpm install vue-i18n@9 + +# Icons (déjà installé) +# pnpm install lucide-vue-next ✅ + +# Utilities (non installé) +pnpm install @vueuse/core +``` + +### Phase 6: Configure Rust Backend + +Add to `src-tauri/Cargo.toml`: +```toml +[dependencies] +tauri = { version = "1.5", features = [] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +rusqlite = { version = "0.30", features = ["bundled"] } +reqwest = { version = "0.11", features = ["json"] } +tokio = { version = "1", features = ["full"] } +bcrypt = "0.15" +jsonwebtoken = "9.2" +pyo3 = { version = "0.20", features = ["auto-initialize"] } +``` + +Create Python requirements: +```bash +# src-tauri/requirements.txt +pykakasi>=2.2.1 +hangul-romanize>=0.1.0 +epitran>=1.24 +``` + +### Phase 7: Run Development Server + +```bash +pnpm tauri dev +``` + +### Phase 8: Build for Production + +```bash +pnpm tauri build +``` + +## Project Structure + +``` +lyremember-app/ +├── src/ # Vue frontend +│ ├── main.ts +│ ├── App.vue +│ ├── router/ +│ │ └── index.ts +│ ├── stores/ +│ │ ├── auth.ts +│ │ └── songs.ts +│ ├── views/ +│ │ ├── LoginView.vue +│ │ ├── SongsView.vue +│ │ ├── SongDetailView.vue +│ │ └── PracticeView.vue +│ ├── components/ +│ │ ├── ui/ # Button.vue, Card.vue, Input.vue, Alert.vue +│ │ └── layout/ # AppHeader.vue, AppSidebar.vue, MainLayout.vue +│ └── lib/ +│ └── tauri-api.ts # API client (16 commands) +│ +├── src-tauri/ # Wrapper Tauri +│ ├── Cargo.toml +│ ├── tauri.conf.json +│ └── src/ +│ ├── main.rs +│ ├── lib.rs # DB init + state +│ └── commands.rs # 16 Tauri commands (fichier unique) +│ # Logique métier → rust-backend/ + +Note : La logique métier est dans rust-backend/ (lib séparée) : +rust-backend/src/ +├── services/ (auth.rs, phonetic.rs, songs.rs, translation.rs, practice.rs) +├── models/ +└── db/ +│ +├── package.json +├── vite.config.ts +├── tailwind.config.js +└── tsconfig.json +``` + +## Development Workflow + +### 1. Start dev server +```bash +pnpm tauri dev +``` + +### 2. Frontend development +- Edit Vue files in `src/` +- Hot reload automatic + +### 3. Backend development +- Edit Rust files in `src-tauri/src/` +- Restart dev server to see changes + +### 4. Database development +```bash +# SQLite database will be created at: +# ~/.lyremember/lyremember.db +``` + +## Commands Reference + +### Frontend (Vue) +```bash +pnpm dev # Vite dev server only +pnpm build # Build frontend +pnpm preview # Preview build +``` + +### Tauri (Full Stack) +```bash +pnpm tauri dev # Dev with Tauri +pnpm tauri build # Production build +npm run tauri icon # Generate app icons +``` + +## Next Steps + +1. ✅ Setup project structure (`lyremember-app/`) +2. ✅ Implement Rust backend (`rust-backend/` — 2 400 lignes, 16 commands) +3. ⏳ Implement Vue frontend (vues partiellement créées, composants pratique à faire) +4. ⏳ Install shadcn-vue + vue-i18n +5. ⏳ Build for production + +## Notes + +- Python must be installed on dev machine for PyO3 +- For distribution, Python can be embedded in app +- SQLite database is local (offline-first) +- Translations and phonetics generated once, stored locally diff --git a/aidd_docs/memory/external/RUST_OPTION.md b/aidd_docs/memory/external/RUST_OPTION.md new file mode 100644 index 0000000..a5bc7b4 --- /dev/null +++ b/aidd_docs/memory/external/RUST_OPTION.md @@ -0,0 +1,522 @@ +# Solution Rust pour LyRemember + +## Oui, c'est possible en Rust ! 🦀 + +### Option Recommandée : **Tauri** + +**Tauri** = Framework pour créer des applications desktop natives avec : +- **Backend en Rust** (rapide, sécurisé, léger) +- **Frontend Web** (React, Vue, Svelte, ou HTML/CSS/JS vanilla) +- **Apps natives** pour Windows, macOS, Linux +- **Support mobile** (Android + iOS via Tauri Mobile - en beta) + +--- + +## Comparaison Rust (Tauri) vs JavaScript (PWA) + +### Tauri (Rust + Web Frontend) + +**Avantages :** +- ✅ **Ultra léger** : ~3-10 MB vs 100+ MB pour Electron +- ✅ **Très rapide** : Rust est ultra performant +- ✅ **Sécurisé** : Rust évite les bugs mémoire +- ✅ **Apps natives** : Vraies applications desktop +- ✅ **Moins de RAM** : 50-100 MB vs 300+ MB pour Electron +- ✅ **Pas de Node.js** requis en production +- ✅ **Mobile possible** : Tauri Mobile (beta mais prometteur) +- ✅ **Mono-binaire** : Un seul exécutable par OS + +**Inconvénients :** +- ❌ **Courbe d'apprentissage** : Rust est plus difficile que Python/JS +- ❌ **Moins de libs** : Écosystème plus jeune pour certaines choses +- ❌ **Compile time** : Plus long que du JS interprété +- ❌ **Mobile encore beta** : Pas encore stable + +**Stack avec Tauri :** +``` +Frontend : React/Vue/Svelte (comme PWA) +Backend : Rust (remplace Python FastAPI) +Desktop : Tauri (natif) +Mobile : Tauri Mobile (beta) +BDD : SQLite (via rusqlite) +``` + +--- + +### PWA (JavaScript/Python) + +**Avantages :** +- ✅ **Facile à apprendre** : JS/Python plus accessibles +- ✅ **Écosystème riche** : Beaucoup de libs disponibles +- ✅ **Dev rapide** : Prototypage ultra rapide +- ✅ **Mobile stable** : PWA fonctionne partout +- ✅ **Pas de compilation** : Deploy instantané + +**Inconvénients :** +- ❌ **Moins performant** : Python/JS plus lents que Rust +- ❌ **Plus lourd** : Nécessite serveur backend séparé +- ❌ **Moins natif** : Reste une webapp +- ❌ **Connexion requise** : Sauf si Service Worker configuré + +--- + +## Architecture Tauri Détaillée + +``` +┌─────────────────────────────────────────────────────────────┐ +│ TAURI APPLICATION │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ FRONTEND (WebView) │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ React/Vue/Svelte App │ │ │ +│ │ │ - Pages (Login, Songs, Practice, Stats) │ │ │ +│ │ │ - Components (SongCard, Karaoke, etc.) │ │ │ +│ │ │ - Styles (Tailwind CSS) │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ │ IPC (Inter-Process Comm) │ │ +│ │ ▼ │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ BACKEND (Rust) │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ Tauri Commands (async Rust functions) │ │ │ +│ │ │ - auth::login, auth::register │ │ │ +│ │ │ - songs::list, songs::create, songs::update │ │ │ +│ │ │ - genius::search, genius::import │ │ │ +│ │ │ - phonetic::generate (JP/KR/FR/EN) │ │ │ +│ │ │ - practice::save_session │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ Business Logic (Rust modules) │ │ │ +│ │ │ - db.rs (SQLite via rusqlite/sqlx) │ │ │ +│ │ │ - models.rs (User, Song, Session structs) │ │ │ +│ │ │ - genius.rs (Genius API client) │ │ │ +│ │ │ - phonetic.rs (translitération) │ │ │ +│ │ │ - translation.rs (traduction) │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ SQLite Database (fichier local) │ │ │ +│ │ │ - users, songs, user_songs, sessions │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Stack Technique Complète avec Rust + +### Frontend (identique à PWA) +- **Framework :** React + TypeScript + Vite +- **Styling :** Tailwind CSS +- **Communication :** Tauri IPC (au lieu d'axios HTTP) + +### Backend (Rust au lieu de Python) +- **Framework :** Tauri (avec commands async) +- **BDD :** rusqlite ou sqlx (SQLite en Rust) +- **Auth :** JWT avec jsonwebtoken crate +- **HTTP Client :** reqwest (pour Genius API) +- **Sérialisation :** serde + serde_json + +### Bibliothèques Rust Nécessaires + +**Core Tauri :** +```toml +[dependencies] +tauri = { version = "1.5", features = ["shell-open"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +``` + +**Base de données :** +```toml +rusqlite = { version = "0.30", features = ["bundled"] } +# OU +sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "sqlite"] } +``` + +**Auth & Crypto :** +```toml +jsonwebtoken = "9.2" +bcrypt = "0.15" # hash passwords +``` + +**API Genius :** +```toml +reqwest = { version = "0.11", features = ["json"] } +tokio = { version = "1", features = ["full"] } +``` + +**Translittération/Phonétique :** +- **Problème :** Peu de crates Rust pour phonétique JP/KR +- **Solution 1 :** Utiliser Python via FFI (appeler Python depuis Rust) +- **Solution 2 :** Implémenter en Rust pur (plus complexe) +- **Solution 3 :** Utiliser APIs externes + +**Traduction :** +```toml +# Pas d'équivalent direct de deep-translator en Rust +# Options: +# 1. Appeler API Google Translate directement avec reqwest +# 2. Utiliser Python via PyO3 +# 3. Service externe (DeepL, Google Cloud) +``` + +--- + +## Crates Rust Équivalents + +| Fonctionnalité | Python | Rust | +|----------------|--------|------| +| **Web Framework** | FastAPI | Tauri Commands | +| **BDD SQLite** | sqlite3 | rusqlite / sqlx | +| **Sérialisation** | json | serde_json | +| **HTTP Client** | requests | reqwest | +| **Auth JWT** | python-jose | jsonwebtoken | +| **Password Hash** | passlib | bcrypt | +| **Async** | asyncio | tokio | +| **Genius API** | lyricsgenius | reqwest (manuel) | +| **Traduction** | deep-translator | reqwest (API) | +| **Phonétique JP** | pykakasi | ❌ (FFI Python?) | +| **Phonétique KR** | hangul-romanize | ❌ (implémenter?) | +| **Phonétique FR/EN** | epitran | ❌ (IPA rules?) | + +--- + +## Défis avec Rust + +### 1. Phonétique (Principal défi) +**Problème :** Pas de crates matures pour JP/KR + +**Solutions :** +- **Option A :** Utiliser PyO3 (appeler Python depuis Rust) + - Garder pykakasi, hangul-romanize en Python + - Appeler depuis Rust + - Complexe mais faisable + +- **Option B :** Réimplémenter en Rust + - Longue tâche + - Pas forcément utile + +- **Option C :** API externe + - Trouver service de translittération + - Peut coûter de l'argent + +**Ma recommandation :** Option A (PyO3) pour phonétique uniquement + +### 2. Traduction +**Solutions :** +- Appeler Google Translate API directement avec reqwest +- Ou garder Python pour cette partie aussi + +### 3. Courbe d'apprentissage +Rust est plus strict que Python : +- Ownership & borrowing +- Lifetimes +- Type system rigide +- Mais force à écrire du code correct ! + +--- + +## Avantages Tauri pour Votre Cas + +### Desktop +- ✅ **Windows :** Un .exe de 3-5 MB +- ✅ **macOS :** Un .app de 3-5 MB +- ✅ **Linux :** Un binaire de 3-5 MB + +### Mobile (Tauri Mobile - beta) +- ⚠️ **Android :** Possible mais beta +- ⚠️ **iOS :** Possible mais beta +- Nécessite configuration supplémentaire + +### Performance +- ⚡ **Démarrage :** Instantané +- ⚡ **RAM :** 50-100 MB +- ⚡ **CPU :** Très efficient + +### Distribution +- 📦 **Un fichier** par plateforme +- 📦 **Pas d'installation** de runtime +- 📦 **Mises à jour** possibles (Tauri Updater) + +--- + +## Structure Projet Tauri + +``` +lyremember/ +├── src-tauri/ # Backend Rust +│ ├── Cargo.toml # Dépendances Rust +│ ├── tauri.conf.json # Config Tauri +│ ├── src/ +│ │ ├── main.rs # Entry point +│ │ ├── commands/ # Tauri commands (expose to JS) +│ │ │ ├── auth.rs +│ │ │ ├── songs.rs +│ │ │ ├── genius.rs +│ │ │ └── practice.rs +│ │ ├── models/ +│ │ │ ├── user.rs +│ │ │ ├── song.rs +│ │ │ └── session.rs +│ │ ├── db/ +│ │ │ └── sqlite.rs +│ │ ├── services/ +│ │ │ ├── genius.rs +│ │ │ ├── phonetic.rs +│ │ │ └── translation.rs +│ │ └── utils/ +│ ├── icons/ # App icons +│ └── target/ # Binaires compilés +│ +├── src/ # Frontend (React/Vue/Svelte) +│ ├── main.tsx +│ ├── App.tsx +│ ├── pages/ +│ ├── components/ +│ └── lib/ +│ └── tauri.ts # API Tauri (invoke commands) +│ +├── package.json +├── vite.config.ts +└── README.md +``` + +--- + +## Exemple de Code Tauri + +### Backend Rust (commands) + +```rust +// src-tauri/src/commands/songs.rs + +use serde::{Deserialize, Serialize}; +use tauri::State; + +#[derive(Serialize, Deserialize)] +pub struct Song { + pub id: String, + pub title: String, + pub artist: String, + pub language: String, + pub lyrics: Vec, + pub phonetic_lyrics: Option>, +} + +#[tauri::command] +pub async fn get_songs(user_id: String) -> Result, String> { + // Logic here + Ok(vec![]) +} + +#[tauri::command] +pub async fn create_song( + user_id: String, + title: String, + artist: String, + language: String, + lyrics: Vec, +) -> Result { + // Logic here + todo!() +} + +#[tauri::command] +pub async fn generate_phonetic( + song_id: String, + language: String, +) -> Result, String> { + // Appel à service phonétique + todo!() +} +``` + +### Frontend (React avec Tauri) + +```typescript +// src/lib/tauri.ts +import { invoke } from '@tauri-apps/api/tauri' + +export interface Song { + id: string + title: string + artist: string + language: string + lyrics: string[] + phonetic_lyrics?: string[] +} + +export const songsApi = { + async getSongs(userId: string): Promise { + return await invoke('get_songs', { userId }) + }, + + async createSong(data: { + userId: string + title: string + artist: string + language: string + lyrics: string[] + }): Promise { + return await invoke('create_song', data) + }, + + async generatePhonetic(songId: string, language: string): Promise { + return await invoke('generate_phonetic', { songId, language }) + } +} +``` + +```tsx +// src/components/SongList.tsx +import { useState, useEffect } from 'react' +import { songsApi, Song } from '../lib/tauri' + +export function SongList({ userId }: { userId: string }) { + const [songs, setSongs] = useState([]) + + useEffect(() => { + songsApi.getSongs(userId).then(setSongs) + }, [userId]) + + return ( +
+ {songs.map(song => ( +
+

{song.title}

+

{song.artist}

+
+ ))} +
+ ) +} +``` + +--- + +## Timeline avec Tauri + +### Semaine 1 : Setup + Auth +- Setup Tauri + React +- Rust backend avec SQLite +- Auth (register/login) en Rust +- Tests + +### Semaine 2 : Songs CRUD +- CRUD songs en Rust +- Frontend liste/détails +- Integration tests + +### Semaine 3 : Genius + Phonétique +- Client Genius en Rust +- PyO3 pour phonétique (ou implémentation Rust) +- Affichage VO + phonétique + +### Semaine 4 : Practice Modes +- Mode karaoke +- Mode phrases à trous +- Sauvegarde sessions + +### Semaine 5 : Polish + Build +- Mode QCM +- Traduction auto +- Build pour Windows/Mac/Linux +- Tests sur vraies machines + +--- + +## Comparaison Finale + +| Critère | Tauri (Rust) | PWA (JS/Python) | +|---------|--------------|-----------------| +| **Plateforme Desktop** | ⭐⭐⭐ Natif | ⭐⭐ Web installable | +| **Plateforme Mobile** | ⭐⭐ Beta | ⭐⭐⭐ Stable | +| **Performance** | ⭐⭐⭐ Ultra rapide | ⭐⭐ Correct | +| **Taille app** | ⭐⭐⭐ 3-5 MB | ⭐⭐ Variable | +| **Consommation RAM** | ⭐⭐⭐ 50-100 MB | ⭐⭐ 200+ MB | +| **Dev Speed** | ⭐⭐ Moyen | ⭐⭐⭐ Rapide | +| **Courbe apprentissage** | ⭐ Difficile | ⭐⭐⭐ Facile | +| **Écosystème** | ⭐⭐ Jeune | ⭐⭐⭐ Mature | +| **Phonétique JP/KR** | ⭐ Complexe | ⭐⭐⭐ Facile | +| **Gratuit** | ⭐⭐⭐ Oui | ⭐⭐⭐ Oui | +| **Distribution** | ⭐⭐⭐ Un binaire | ⭐⭐ URL | + +--- + +## Ma Recommandation + +### Si vous connaissez déjà Rust ou voulez l'apprendre : +✅ **Choisir Tauri** +- Excellente performance +- Apps natives desktop +- Bon apprentissage de Rust +- Projet sérieux et professionnel + +### Si vous voulez aller vite et avoir mobile stable : +✅ **Choisir PWA** +- Développement plus rapide +- Mobile 100% fonctionnel +- Moins de complexité +- Plus de libs disponibles + +### Compromis possible : +✅ **Commencer en PWA, migrer vers Tauri plus tard** +- Frontend identique (React) +- Remplacer backend Python par Rust progressivement +- Tester Tauri quand ready + +--- + +## Questions pour vous aider à décider + +1. **Avez-vous de l'expérience avec Rust ?** + - Oui → Tauri est bon choix + - Non → Courbe d'apprentissage à considérer + +2. **Mobile est-il prioritaire ?** + - Oui → PWA plus stable + - Non, desktop suffit → Tauri excellent + +3. **Performance est-elle critique ?** + - Oui → Tauri + - Non → PWA suffisant + +4. **Voulez-vous apprendre Rust ?** + - Oui → Super projet pour apprendre ! + - Non → Rester sur JS/Python + +5. **Timeline ?** + - Rapide (2-3 semaines) → PWA + - Flexible (1-2 mois) → Tauri possible + +--- + +## Verdict + +**Pour votre cas (desktop + mobile, gratuit, FR/EN/KR/JP) :** + +**Option 1 (Recommandée) : Tauri + React** +- ✅ Desktop natif excellent +- ⚠️ Mobile en beta (fonctionne mais moins mature) +- ✅ Ultra performant +- ❌ Phonétique JP/KR nécessite PyO3 ou workaround + +**Option 2 : PWA (React + Python)** +- ✅ Desktop et mobile stables +- ✅ Dev rapide +- ✅ Phonétique facile +- ❌ Moins natif + +**Option 3 : Hybride** +- Commencer PWA pour MVP +- Voir si intérêt +- Migrer vers Tauri si besoin perf/native + +**Que préférez-vous ?** 🤔 diff --git a/aidd_docs/memory/external/TAURI_BACKEND_CLARIFICATION.md b/aidd_docs/memory/external/TAURI_BACKEND_CLARIFICATION.md new file mode 100644 index 0000000..edcd76a --- /dev/null +++ b/aidd_docs/memory/external/TAURI_BACKEND_CLARIFICATION.md @@ -0,0 +1,564 @@ +# Tauri Backend - Clarification + +## ⚠️ Correction Importante + +**Tauri NE gère PAS automatiquement SQLite et les APIs !** + +**Tauri = Infrastructure/Framework** +**VOUS écrivez le code backend (SQLite, API calls, etc.)** + +--- + +## Qu'est-ce que Tauri FOURNIT ? + +### Tauri fournit : + +1. **Le wrapper natif** (fenêtre, WebView) +2. **Le système IPC** (communication JS ↔ Rust) +3. **L'infrastructure** pour écrire du code backend +4. **Des APIs système** (accès fichiers, notifications, etc.) + +### Tauri NE fournit PAS : + +❌ Code SQLite (vous devez l'écrire) +❌ Code API Genius (vous devez l'écrire) +❌ Logique métier (vous devez l'écrire) +❌ Gestion base de données (vous devez l'écrire) + +--- + +## Concrètement : Qui Écrit Quoi ? + +### VOUS Écrivez le Backend + +```rust +// src-tauri/src/commands.rs (pas de sous-dossier — fichier unique) +// ↓ VOUS écrivez tout ça ↓ + +use rusqlite::{Connection, Result}; +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +pub struct Song { + pub id: String, + pub title: String, + pub artist: String, +} + +// ↓ VOUS écrivez cette fonction ↓ +#[tauri::command] +pub fn get_songs_from_db() -> Result, String> { + // ↓ VOUS gérez SQLite ↓ + let conn = Connection::open("songs.db") + .map_err(|e| e.to_string())?; + + // ↓ VOUS écrivez les requêtes SQL ↓ + let mut stmt = conn.prepare("SELECT id, title, artist FROM songs") + .map_err(|e| e.to_string())?; + + // ↓ VOUS parsez les résultats ↓ + let songs = stmt.query_map([], |row| { + Ok(Song { + id: row.get(0)?, + title: row.get(1)?, + artist: row.get(2)?, + }) + }).map_err(|e| e.to_string())? + .collect::, _>>() + .map_err(|e| e.to_string())?; + + Ok(songs) +} + +// ↓ VOUS appelez l'API Genius ↓ +#[tauri::command] +pub async fn search_genius(query: String) -> Result, String> { + // ↓ VOUS gérez les requêtes HTTP ↓ + let client = reqwest::Client::new(); + let response = client + .get("https://api.genius.com/search") + .query(&[("q", query)]) + .header("Authorization", "Bearer YOUR_TOKEN") + .send() + .await + .map_err(|e| e.to_string())?; + + // ↓ VOUS parsez la réponse JSON ↓ + let data: GeniusResponse = response.json() + .await + .map_err(|e| e.to_string())?; + + Ok(data.results) +} +``` + +### Tauri Fournit Juste l'Infrastructure + +```rust +// src-tauri/src/main.rs +// ↓ Tauri setup (boilerplate) ↓ + +fn main() { + tauri::Builder::default() + // ↓ VOUS enregistrez VOS fonctions ↓ + .invoke_handler(tauri::generate_handler![ + get_songs_from_db, // ← Votre fonction + search_genius, // ← Votre fonction + create_song, // ← Votre fonction + // ... toutes VOS fonctions + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +**Tauri dit juste : "Appelle ces fonctions depuis JS via `invoke()`"** + +--- + +## Analogie avec le Web + +### Backend Web Classique (Express/Node.js) + +```javascript +// Express fournit l'infrastructure HTTP +const express = require('express') +const app = express() + +// VOUS écrivez les routes et la logique +app.get('/songs', async (req, res) => { + // ↓ VOUS gérez SQLite ↓ + const db = await sqlite.open('songs.db') + const songs = await db.all('SELECT * FROM songs') + res.json(songs) +}) + +// VOUS écrivez l'appel API +app.get('/genius/search', async (req, res) => { + // ↓ VOUS faites la requête HTTP ↓ + const response = await fetch('https://api.genius.com/search') + const data = await response.json() + res.json(data) +}) + +// Express démarre le serveur (infrastructure) +app.listen(3000) +``` + +**Express = Infrastructure HTTP** +**VOUS = Logique métier (SQLite, API, etc.)** + +### Backend Tauri (Rust) + +```rust +// Tauri fournit l'infrastructure IPC +tauri::Builder::default() + // VOUS écrivez les commandes + .invoke_handler(tauri::generate_handler![ + get_songs, // ← VOUS gérez SQLite + search_genius, // ← VOUS faites l'appel API + ]) + .run(...) +``` + +**Tauri = Infrastructure IPC** +**VOUS = Logique métier (SQLite, API, etc.)** + +--- + +## Schéma Complet : Qui Fait Quoi ? + +``` +┌─────────────────────────────────────────────────────┐ +│ FRONTEND (Vue) │ +│ │ +│ Vous écrivez : │ +│ - App.vue │ +│ - SongCard.vue │ +│ - etc. │ +│ │ +│ Vite compile → HTML/CSS/JS │ +└──────────────────────┬──────────────────────────────┘ + │ + │ invoke('get_songs') + │ +┌──────────────────────▼──────────────────────────────┐ +│ TAURI (Infrastructure) │ +│ │ +│ Tauri fournit : │ +│ - WebView │ +│ - Système IPC (invoke) │ +│ - APIs système │ +│ │ +│ ┌─────────────────────┐ │ +│ │ Votre Code Rust │ ← VOUS écrivez ! │ +│ │ │ │ +│ │ get_songs() { │ │ +│ │ // SQLite │ ← VOUS gérez ! │ +│ │ } │ │ +│ │ │ │ +│ │ search_genius() { │ │ +│ │ // HTTP request │ ← VOUS appelez ! │ +│ │ } │ │ +│ └─────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## Ce que VOUS Devez Coder + +### 1. Code SQLite (VOUS) + +```rust +// rust-backend/src/db/ (la logique DB est dans la lib rust-backend, pas dans src-tauri) +use rusqlite::{Connection, Result}; + +pub fn init_db() -> Result { + let conn = Connection::open("lyremember.db")?; + + // Créer les tables + conn.execute( + "CREATE TABLE IF NOT EXISTS songs ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + artist TEXT NOT NULL, + language TEXT NOT NULL, + lyrics TEXT NOT NULL + )", + [], + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + email TEXT NOT NULL, + password_hash TEXT NOT NULL + )", + [], + )?; + + Ok(conn) +} + +pub fn get_all_songs(conn: &Connection) -> Result> { + let mut stmt = conn.prepare("SELECT * FROM songs")?; + + let songs = stmt.query_map([], |row| { + Ok(Song { + id: row.get(0)?, + title: row.get(1)?, + artist: row.get(2)?, + language: row.get(3)?, + lyrics: serde_json::from_str(&row.get::<_, String>(4)?).unwrap(), + }) + })? + .collect::, _>>()?; + + Ok(songs) +} + +pub fn insert_song(conn: &Connection, song: &Song) -> Result<()> { + conn.execute( + "INSERT INTO songs (id, title, artist, language, lyrics) VALUES (?1, ?2, ?3, ?4, ?5)", + [ + &song.id, + &song.title, + &song.artist, + &song.language, + &serde_json::to_string(&song.lyrics).unwrap(), + ], + )?; + Ok(()) +} +``` + +**Tauri ne fait RIEN de ça. VOUS l'écrivez !** + +--- + +### 2. Code API Genius (VOUS) + +```rust +// rust-backend/src/services/ (auth.rs, phonetic.rs, songs.rs, translation.rs, practice.rs — genius non encore implémenté) +use reqwest; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +struct GeniusSearchResponse { + response: GeniusResponseData, +} + +#[derive(Deserialize)] +struct GeniusResponseData { + hits: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct GeniusHit { + result: GeniusSong, +} + +#[derive(Deserialize, Serialize)] +pub struct GeniusSong { + pub id: u64, + pub title: String, + pub primary_artist: GeniusArtist, +} + +#[derive(Deserialize, Serialize)] +pub struct GeniusArtist { + pub name: String, +} + +pub async fn search_genius(query: String, token: String) -> Result, String> { + let client = reqwest::Client::new(); + + let response = client + .get("https://api.genius.com/search") + .query(&[("q", query)]) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; + + if !response.status().is_success() { + return Err(format!("Genius API error: {}", response.status())); + } + + let data: GeniusSearchResponse = response + .json() + .await + .map_err(|e| e.to_string())?; + + let songs = data.response.hits + .into_iter() + .map(|hit| hit.result) + .collect(); + + Ok(songs) +} + +pub async fn get_lyrics(song_id: u64, token: String) -> Result { + // ... VOUS implémentez le scraping ou l'appel API + todo!("Implémenter récupération lyrics") +} +``` + +**Tauri ne fait RIEN de ça. VOUS l'écrivez !** + +--- + +### 3. Code Commandes Tauri (VOUS) + +```rust +// src-tauri/src/commands.rs (fichier unique, délègue à lyremember_backend) +use lyremember_backend; +use crate::services::genius; + +#[tauri::command] +pub async fn get_songs() -> Result, String> { + let conn = sqlite::init_db() + .map_err(|e| e.to_string())?; + + sqlite::get_all_songs(&conn) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn create_song(song: Song) -> Result { + let conn = sqlite::init_db() + .map_err(|e| e.to_string())?; + + sqlite::insert_song(&conn, &song) + .map_err(|e| e.to_string())?; + + Ok(song) +} + +#[tauri::command] +pub async fn search_genius_api(query: String, token: String) -> Result, String> { + genius::search_genius(query, token).await +} +``` + +**VOUS écrivez toute cette logique !** + +--- + +### 4. Enregistrement dans Tauri (Boilerplate) + +```rust +// src-tauri/src/main.rs +mod commands; +mod db; +mod services; + +fn main() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![ + commands::get_songs, + commands::create_song, + commands::search_genius_api, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +**Tauri fournit juste `.invoke_handler()` - VOUS fournissez les fonctions !** + +--- + +## Ce que Tauri FOURNIT (APIs Built-in) + +Tauri a quelques APIs système intégrées : + +```javascript +// Frontend Vue +import { + save, + open +} from '@tauri-apps/api/dialog' + +import { + writeTextFile, + readTextFile +} from '@tauri-apps/api/fs' + +import { + sendNotification +} from '@tauri-apps/api/notification' + +// Ouvrir dialogue fichier (fourni par Tauri) +const file = await open() + +// Lire fichier (fourni par Tauri) +const content = await readTextFile(file) + +// Notification (fourni par Tauri) +await sendNotification('Titre', 'Message') +``` + +**Mais SQLite, API calls, logique métier = VOUS !** + +--- + +## Résumé : Division des Responsabilités + +### Tauri (Framework) +✅ Créer fenêtre native +✅ Intégrer WebView +✅ Système IPC (invoke) +✅ APIs système (fichiers, dialogues, notifications) +✅ Build natif (compile en .exe/.app) + +### VOUS (Développeur Backend) +✅ Gérer SQLite (connexion, requêtes, migrations) +✅ Appeler API Genius (HTTP requests, parsing JSON) +✅ Implémenter logique métier (auth, validation, etc.) +✅ Générer phonétique (appeler libs ou Python) +✅ Traduire (appeler services de traduction) +✅ Sauvegarder progression +✅ Gérer sessions practice +✅ Tout le reste ! + +--- + +## Analogie Finale + +### Express (Web) +``` +Express dit : "Je gère HTTP" +VOUS dites : "Je gère SQLite, APIs, logique" +``` + +### Tauri (Desktop) +``` +Tauri dit : "Je gère WebView et IPC" +VOUS dites : "Je gère SQLite, APIs, logique" +``` + +--- + +## Ce que Ça Signifie pour Votre Projet + +### Vous devez apprendre/coder : + +1. **Rust** (bases) + - Syntax + - Ownership + - Error handling (Result) + - Async/await + +2. **rusqlite** (SQLite en Rust) + - Connexion DB + - Requêtes SQL + - Mapping rows → structs + +3. **reqwest** (HTTP client Rust) + - GET/POST requests + - Headers (Authorization) + - Parsing JSON + +4. **serde** (Sérialisation Rust) + - Serialize/Deserialize + - JSON <-> Rust structs + +5. **Logique métier** + - Authentification + - CRUD songs + - Practice sessions + - Progress tracking + +### Tauri s'occupe de : +- ✅ Afficher votre Vue app +- ✅ Communiquer JS ↔ Rust +- ✅ Build natif + +--- + +## Bon ou Mauvais ? + +### Avantage +✅ **Flexibilité totale** - Vous codez exactement ce dont vous avez besoin +✅ **Pas de bloatware** - Pas de code inutile +✅ **Contrôle** - Vous savez ce qui se passe + +### Défi +❌ **Plus de code à écrire** qu'avec un framework full-stack (Django, Rails) +❌ **Apprendre Rust** nécessaire +❌ **Gérer erreurs** Rust (strict mais sûr) + +--- + +## Donc : Tauri = Backend ? + +### ❌ Non, Tauri ≠ Backend +**Tauri = Infrastructure pour VOTRE backend** + +### ✅ Oui, vous écrivez un backend en Rust +**Backend = Votre code Rust (SQLite, API calls, logique)** + +### 🎯 Tauri facilite : +- Communication JS ↔ Rust +- Build natif +- APIs système + +### 👨‍💻 VOUS implémentez : +- SQLite +- API Genius +- Phonétique +- Traduction +- Toute la logique + +--- + +## C'est Plus Clair ? + +**Tauri ne fait PAS la magie.** + +**Tauri donne les outils, VOUS construisez le backend.** + +**Mais c'est une bonne chose ! Vous avez le contrôle total.** 💪 diff --git a/aidd_docs/memory/external/TAURI_FRONTEND_LINK.md b/aidd_docs/memory/external/TAURI_FRONTEND_LINK.md new file mode 100644 index 0000000..4c433e0 --- /dev/null +++ b/aidd_docs/memory/external/TAURI_FRONTEND_LINK.md @@ -0,0 +1,514 @@ +# Comment Tauri et le Frontend Interagissent + +## La Réponse Simple + +**Tauri NE construit PAS les pages.** + +**Votre code Vue construit les pages, comme d'habitude.** + +**Tauri = Conteneur natif qui affiche votre site web Vue** + +--- + +## Analogie Simple + +Imaginez : + +### Sans Tauri (Web classique) +``` +┌─────────────────────────────────────┐ +│ Navigateur Chrome │ +│ ┌───────────────────────────────┐ │ +│ │ Votre site Vue │ │ +│ │ (HTML/CSS/JavaScript) │ │ +│ │ │ │ +│ │ construit par Vite/Webpack │ │ +│ └───────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +### Avec Tauri (Desktop) +``` +┌─────────────────────────────────────┐ +│ Application Desktop Tauri │ +│ ┌───────────────────────────────┐ │ +│ │ WebView (navigateur intégré) │ │ +│ │ ┌─────────────────────────┐ │ │ +│ │ │ Votre site Vue │ │ │ +│ │ │ (HTML/CSS/JavaScript) │ │ │ +│ │ │ │ │ │ +│ │ │ construit par Vite │ │ │ +│ │ └─────────────────────────┘ │ │ +│ └───────────────────────────────┘ │ +│ ↕ │ +│ ┌───────────────────────────────┐ │ +│ │ Backend Rust (bonus!) │ │ +│ │ (SQLite, fichiers, etc.) │ │ +│ └───────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +**Tauri = Chrome/Edge intégré dans votre app + Backend Rust** + +--- + +## Comment Ça Marche Exactement + +### Étape 1 : Vous développez normalement en Vue + +```vue + + + + + + +``` + +### Étape 2 : Vite compile votre Vue en HTML/CSS/JS + +Quand vous lancez `npm run dev` ou `npm run build` : + +**Vite transforme votre Vue en :** +```html + + + + + + + +
+ + + +``` + +```javascript +// dist/assets/index.js (simplifié) +const message = ref('Hello from Vue!') +// ... tout votre code Vue compilé +``` + +```css +/* dist/assets/index.css */ +h1 { color: blue; } +``` + +**C'est EXACTEMENT comme un site web normal !** + +### Étape 3 : Tauri affiche ce HTML/CSS/JS + +```rust +// src-tauri/src/main.rs +fn main() { + tauri::Builder::default() + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +**Ce que fait ce code Rust :** +1. Créer une fenêtre native (Windows/Mac/Linux) +2. Intégrer un WebView (mini navigateur) +3. Charger votre `index.html` dedans +4. Voilà ! Votre app Vue tourne + +--- + +## Le WebView : Le Navigateur Caché + +### Qu'est-ce qu'un WebView ? + +**WebView = Navigateur sans barre d'adresse, ni onglets, ni boutons** + +**Chaque OS a son WebView natif :** + +| OS | WebView utilisé | +|---|---| +| **Windows** | WebView2 (basé sur Edge/Chromium) | +| **macOS** | WKWebView (basé sur Safari/WebKit) | +| **Linux** | WebKitGTK (basé sur WebKit) | + +**Votre Vue app tourne DANS ce WebView, comme dans un navigateur !** + +--- + +## Flux Complet + +### En Développement (`npm run tauri dev`) + +``` +1. Vite démarre serveur de dev + → http://localhost:5173 + → Votre Vue app est servie ici + +2. Tauri ouvre une fenêtre + → WebView charge http://localhost:5173 + → Vous voyez votre app Vue dans une fenêtre desktop + +3. Hot Module Replacement (HMR) + → Vous modifiez App.vue + → Vite recharge automatiquement + → La fenêtre Tauri se met à jour +``` + +### En Production (`npm run tauri build`) + +``` +1. Vite build votre Vue + → Génère dist/ + ├── index.html + ├── assets/ + ├── index.js (votre Vue compilé) + └── index.css + +2. Tauri compile le Rust + → Crée l'exécutable (.exe, .app, etc.) + +3. Tauri EMBED les fichiers dist/ dans l'exe + → L'exe contient : + - WebView runtime + - Votre HTML/CSS/JS + - Backend Rust + +4. Résultat : Un seul fichier exécutable + → lyremember.exe (Windows) + → Tout est dedans ! +``` + +--- + +## Communication Frontend ↔ Backend + +### Frontend (Vue) appelle Backend (Rust) + +```vue + + + +``` + +### Backend (Rust) répond + +```rust +// src-tauri/src/main.rs +#[tauri::command] +fn get_songs() -> Vec { + vec!["Song 1".to_string(), "Song 2".to_string()] +} + +fn main() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![get_songs]) + .run(tauri::generate_context!()) + .expect("error"); +} +``` + +**Comment ça communique ?** + +``` +Frontend Vue (JavaScript) + │ + │ invoke('get_songs') + │ + ▼ +IPC (Inter-Process Communication) + │ + ▼ +Backend Rust + │ + │ get_songs() → Vec + │ + ▼ +IPC (retour) + │ + ▼ +Frontend Vue (JavaScript) + │ + │ reçoit ["Song 1", "Song 2"] +``` + +**IPC = Message passing entre JavaScript et Rust** + +--- + +## Comparaison avec d'Autres Technologies + +### Application Web Classique +``` +Frontend (Vue) + ↓ HTTP +Backend (Node.js/Python sur serveur) + ↓ +Base de données (serveur séparé) +``` +**3 processus séparés, sur le réseau** + +### Electron (Concurrent de Tauri) +``` +Frontend (Vue) + ↓ IPC +Backend (Node.js embarqué) + ↓ +Base de données (fichier local) +``` +**Tout dans l'app, mais lourd (100+ MB)** + +### Tauri +``` +Frontend (Vue dans WebView) + ↓ IPC +Backend (Rust embarqué) + ↓ +Base de données (fichier local) +``` +**Tout dans l'app, ultra léger (5 MB)** + +--- + +## Exemple Concret + +### Votre code Vue (normal) + +```vue + + + + +``` + +### Ce que Vite compile (HTML/JS) + +```html + + + + +
+ + + +``` + +```javascript +// dist/assets/index.js (pseudo-code simplifié) +function SongList() { + const songs = ref([]) + + onMounted(async () => { + // Ce invoke() envoie un message à Rust + songs.value = await window.__TAURI__.invoke('get_songs_from_db') + }) + + // Render logic + return html` +
+

Mes Chansons

+
    + ${songs.value.map(song => `
  • ${song.title}
  • `)} +
+
+ ` +} +``` + +### Backend Rust qui répond + +```rust +// src-tauri/src/commands/songs.rs +use rusqlite::Connection; + +#[tauri::command] +fn get_songs_from_db() -> Result, String> { + let conn = Connection::open("songs.db") + .map_err(|e| e.to_string())?; + + let mut stmt = conn.prepare("SELECT * FROM songs") + .map_err(|e| e.to_string())?; + + let songs = stmt.query_map([], |row| { + Ok(Song { + id: row.get(0)?, + title: row.get(1)?, + }) + }).map_err(|e| e.to_string())? + .collect::, _>>() + .map_err(|e| e.to_string())?; + + Ok(songs) +} +``` + +### Workflow complet + +``` +1. Utilisateur ouvre l'app + → Tauri lance WebView + → WebView charge index.html + +2. Vue s'initialise + → onMounted() s'exécute + → invoke('get_songs_from_db') + +3. Message envoyé à Rust via IPC + → Rust reçoit 'get_songs_from_db' + → Exécute la fonction get_songs_from_db() + → Lit SQLite + → Retourne Vec + +4. Vue reçoit la réponse + → songs.value = [...] + → Vue re-render + → Liste affichée +``` + +--- + +## Donc, Qui Fait Quoi ? + +### Vue + Vite +✅ Construit vos pages (HTML/CSS/JS) +✅ Gère la réactivité +✅ Affiche l'interface +✅ Gère le routing, state, etc. + +**Vite compile Vue → HTML/CSS/JS standard** + +### Tauri +✅ Crée la fenêtre native +✅ Intègre le WebView (mini navigateur) +✅ Charge votre HTML/CSS/JS dedans +✅ Fournit le backend Rust +✅ Gère la communication JS ↔ Rust (IPC) +✅ Compile tout en un seul exécutable + +**Tauri = Conteneur + Backend + IPC** + +--- + +## Avantages de Cette Architecture + +### Pourquoi pas juste un site web ? +❌ Pas d'accès fichiers locaux +❌ Pas d'accès SQLite local +❌ Pas offline complet +❌ Pas d'icône sur bureau +❌ Nécessite serveur backend séparé + +### Pourquoi pas Electron ? +❌ Lourd (100+ MB, embarque tout Chromium) +❌ Consomme beaucoup de RAM +❌ Backend Node.js moins performant que Rust + +### Pourquoi Tauri ? ✅ +✅ Léger (5 MB, utilise WebView de l'OS) +✅ Performant (Rust ultra rapide) +✅ Accès système complet (fichiers, SQLite, etc.) +✅ App native (icône, installeur, etc.) +✅ Code web standard (Vue fonctionne normalement) + +--- + +## En Résumé + +### Question : Tauri construit-il les pages ? +**Réponse : NON** + +### Qui construit les pages ? +**Vite compile votre Vue en HTML/CSS/JS** + +### Que fait Tauri ? +**Tauri affiche cet HTML/CSS/JS dans un WebView natif** + +### Bonus ? +**Tauri ajoute un backend Rust accessible depuis JavaScript** + +--- + +## Schéma Final + +``` +VOUS ÉCRIVEZ + │ + ├─ Frontend Vue (.vue files) + │ │ + │ ▼ + │ VITE COMPILE + │ │ + │ ▼ + │ HTML/CSS/JS (standard web) + │ │ + │ ▼ + │ TAURI AFFICHE dans WebView natif + │ │ + │ ▼ + │ App Desktop (avec icône, fenêtre, etc.) + │ + └─ Backend Rust (.rs files) + │ + ▼ + TAURI COMPILE + │ + ▼ + Code natif (rapide) + │ + ▼ + Accessible depuis Vue via invoke() +``` + +--- + +## C'est Plus Clair ? + +**L'important à retenir :** + +1. **Vue reste Vue** (vous codez normalement) +2. **Vite compile Vue** (en HTML/CSS/JS) +3. **Tauri affiche** cet HTML/CSS/JS dans une fenêtre +4. **Tauri ajoute** un backend Rust puissant + +**Tauri = Smart WebView + Backend Rust** + +**Pas de magie, juste du web dans une fenêtre native !** ✨ diff --git a/aidd_docs/memory/external/TAURI_INTEGRATION_COMPLETE.md b/aidd_docs/memory/external/TAURI_INTEGRATION_COMPLETE.md new file mode 100644 index 0000000..28d1941 --- /dev/null +++ b/aidd_docs/memory/external/TAURI_INTEGRATION_COMPLETE.md @@ -0,0 +1,431 @@ +# 🎉 LyRemember - Intégration Tauri Complète + +## ✅ Résumé de l'Intégration + +L'intégration du backend Rust avec Tauri est **100% fonctionnelle**. + +### Ce qui a été Implémenté + +#### 1. Backend Rust (rust-backend/) +- ✅ **2,400 lignes** de code Rust +- ✅ **5 services** complets (auth, phonetic, translation, songs, practice) +- ✅ **SQLite** avec schéma complet (4 tables) +- ✅ **PyO3** pour phonétique JP/KR/FR/EN +- ✅ **LibreTranslate** pour traduction automatique +- ✅ **11 tests** unitaires passants +- ✅ Documentation complète + +#### 2. Application Tauri (lyremember-app/) +- ✅ **Projet créé** avec Vue 3 + TypeScript + Vite +- ✅ **16 commandes Tauri** exposées +- ✅ **TypeScript API** type-safe (200 lignes) +- ✅ **UI de test** d'intégration +- ✅ **Base de données** auto-initialisée +- ✅ Documentation README complète + +#### 3. Intégration Backend ↔ Frontend +- ✅ **Shared state** avec Mutex +- ✅ **Erreur handling** complet +- ✅ **Types TypeScript** pour tous les modèles +- ✅ **Async/await** support +- ✅ Communication IPC fonctionnelle + +## 🏗️ Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ Frontend Vue 3 + TypeScript │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ App.vue (Integration Test UI) │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ lib/tauri-api.ts (TypeScript API) │ │ +│ │ • register(), login(), verifyToken() │ │ +│ │ • createSong(), getSongs(), etc. │ │ +│ │ • Full type definitions │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ │ +│ invoke() │ +│ ↓ │ +├─────────────────────────────────────────────────────┤ +│ Tauri IPC Layer │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ commands.rs (16 Tauri Commands) │ │ +│ │ • cmd_register, cmd_login │ │ +│ │ • cmd_create_song, cmd_get_songs │ │ +│ │ • cmd_create_practice_session │ │ +│ │ • Error handling Result │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ lib.rs (Database State Management) │ │ +│ │ • DbState(Mutex) │ │ +│ │ • Auto DB initialization │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ │ +│ ↓ │ +├─────────────────────────────────────────────────────┤ +│ Backend Rust (lyremember_backend) │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ services/auth.rs │ │ +│ │ • register() - bcrypt hashing │ │ +│ │ • login() - JWT generation │ │ +│ │ • verify_token() - JWT validation │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ services/phonetic.rs (PyO3 Bridge) │ │ +│ │ • japanese_to_romaji() → pykakasi │ │ +│ │ • korean_to_roman() → hangul-romanize │ │ +│ │ • to_ipa() → epitran │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ services/translation.rs │ │ +│ │ • translate_text() → LibreTranslate API │ │ +│ │ • Retry logic, timeout handling │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ services/songs.rs │ │ +│ │ • create_song() - Auto phonetic + trans │ │ +│ │ • get_songs(), update_song(), delete_song() │ │ +│ │ • User repertoire management │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ services/practice.rs │ │ +│ │ • create_session() - Track practice │ │ +│ │ • get_user_stats() - Aggregate stats │ │ +│ │ • get_song_mastery() - Mastery level │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ │ +│ ↓ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ db/sqlite.rs │ │ +│ │ • init_database() - Schema + migrations │ │ +│ │ • 4 tables: users, songs, user_songs, │ │ +│ │ practice_sessions │ │ +│ │ • Indexes, foreign keys │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ │ +│ ↓ │ +└─────────────────────────────────────────────────────┘ + │ + ↓ + ┌────────────────────────────────┐ + │ SQLite Database │ + │ ~/.local/share/.../app.db │ + │ • users │ + │ • songs (lyrics + phonetic) │ + │ • user_songs (repertoire) │ + │ • practice_sessions │ + └────────────────────────────────┘ + │ + ↓ + ┌────────────────────────────────┐ + │ Python (via PyO3) │ + │ • pykakasi (JP) │ + │ • hangul-romanize (KR) │ + │ • epitran (FR/EN) │ + └────────────────────────────────┘ +``` + +## 📦 Fichiers Créés + +### Backend Rust (rust-backend/) +``` +rust-backend/ +├── src/ +│ ├── lib.rs ✅ Public API +│ ├── error.rs ✅ Error types (thiserror) +│ ├── models/ ✅ Data models (3 files) +│ │ ├── user.rs +│ │ ├── song.rs +│ │ └── session.rs +│ ├── db/ ✅ Database layer (2 files) +│ │ ├── mod.rs +│ │ └── sqlite.rs +│ └── services/ ✅ Business logic (6 files) +│ ├── auth.rs (bcrypt + JWT) +│ ├── phonetic.rs (PyO3 bridge) +│ ├── translation.rs (LibreTranslate) +│ ├── songs.rs (CRUD + auto-gen) +│ ├── practice.rs (Sessions + stats) +│ └── mod.rs +├── examples/ +│ └── basic_usage.rs ✅ Working demo +├── Cargo.toml ✅ Dependencies +├── requirements.txt ✅ Python deps +└── README.md ✅ Documentation +``` + +### Application Tauri (lyremember-app/) +``` +lyremember-app/ +├── src/ ✅ Frontend Vue +│ ├── App.vue ✅ Integration test UI (160 lines) +│ ├── lib/ +│ │ └── tauri-api.ts ✅ TypeScript API (200 lines) +│ └── main.ts +│ +├── src-tauri/ ✅ Backend Tauri +│ ├── src/ +│ │ ├── lib.rs ✅ Setup + DB init (60 lines) +│ │ ├── commands.rs ✅ 16 Tauri commands (270 lines) +│ │ └── main.rs +│ ├── Cargo.toml ✅ Dependencies (includes rust-backend) +│ └── tauri.conf.json +│ +├── package.json ✅ npm scripts +└── README.md ✅ Complete documentation +``` + +### Documentation (root/) +``` +Documentation créée: +├── FINAL_DECISIONS.md ✅ Résumé décisions tech +├── IMPLEMENTATION_GUIDE.md ✅ Guide step-by-step +├── IMPLEMENTATION_SUMMARY.md ✅ Résumé implémentation +├── USER_STORIES_V2.md ✅ 29 user stories détaillées +├── VUE_TAURI_GUIDE.md ✅ Guide Vue + Tauri +├── TAURI_FRONTEND_LINK.md ✅ Explication architecture +├── TAURI_BACKEND_CLARIFICATION.md ✅ Rôle de Tauri +├── TRANSLATION_PHONETIC_STRATEGY.md ✅ Stratégies +├── ARCHITECTURE_EXPLAINED.md ✅ Architecture claire +├── TECH_CHOICES.md ✅ Comparaison technos +├── RUST_OPTION.md ✅ Analyse Rust/Tauri +└── UI_LIBRARIES.md ✅ Comparaison UI libs +``` + +## 🧪 Test d'Intégration + +### Comment Tester + +1. **Installer les dépendances:** +```bash +# Python (pour phonétique) +cd rust-backend +pip install -r requirements.txt + +# npm +cd ../lyremember-app +npm install +``` + +2. **Lancer l'application:** +```bash +npm run tauri dev +``` + +3. **Cliquer sur "Run Integration Test"** + +### Ce qui est Testé + +✅ **8 fonctionnalités complètes:** +1. Health check (backend connecté) +2. User registration (bcrypt + SQLite) +3. User login (JWT tokens) +4. Song creation avec phonétique JP (PyO3 + pykakasi) +5. Auto-translation EN (LibreTranslate) +6. Add to repertoire (many-to-many) +7. Practice session tracking +8. User statistics aggregation + +### Logs Attendus + +``` +[14:23:45] 🚀 LyRemember - Backend Integration Test Ready +[14:23:47] 🔍 Testing health check... +[14:23:47] ✅ Health check: Backend is healthy! +[14:23:48] 👤 Testing user registration... +[14:23:48] ✅ User registered: user_1708267428123 (ID: ...) +[14:23:48] 🔐 Testing login... +[14:23:48] ✅ Login successful, token: eyJ0eXAiOiJKV1QiLCJ... +[14:23:48] 🎵 Testing song creation with phonetics... +[14:23:50] ✅ Song created: 千本桜 by 初音ミク +[14:23:50] 📝 Phonetic: senbonzakura, yoru ni magire, kimi no koe mo todoka nai yo +[14:23:50] 🌐 Translation available: en +[14:23:50] 📚 Adding song to user's repertoire... +[14:23:50] ✅ Song added to repertoire +[14:23:50] 📖 Getting user's songs... +[14:23:50] ✅ User has 1 songs +[14:23:50] 🎮 Creating practice session... +[14:23:50] ✅ Practice session created: karaoke, score: 85.5% +[14:23:50] 📊 Getting user statistics... +[14:23:50] ✅ Stats: 1 sessions, avg score: 85.5% +[14:23:50] 🎉 Integration test completed successfully! +``` + +## 💻 API Utilisation + +### Exemples de Code + +```typescript +import * as api from './lib/tauri-api'; + +// 1. Register user +const user = await api.register( + 'musiclover', + 'email@example.com', + 'password123' +); + +// 2. Login +const token = await api.login('musiclover', 'password123'); +localStorage.setItem('token', token); + +// 3. Create song with auto phonetic + translation +const song = await api.createSong( + '千本桜', // Title (Japanese) + '初音ミク', // Artist + 'jp', // Language code + [ + '千本桜', + '夜ニ紛レ', + '君ノ声モ届カナイヨ' + ], + true // Auto-translate to EN +); + +console.log(song.phonetic_lyrics); +// → ['senbonzakura', 'yoru ni magire', 'kimi no koe mo todoka nai yo'] + +console.log(song.translations); +// → { en: ['Thousand Cherry Blossoms', 'Lost in the night', ...] } + +// 4. Add to user's repertoire +await api.addToRepertoire(user.id, song.id); + +// 5. Get user's songs +const mySongs = await api.getUserSongs(user.id); +console.log(`You have ${mySongs.length} songs`); + +// 6. Practice! +const session = await api.createPracticeSession( + user.id, + song.id, + 'karaoke', // Mode + 85.5, // Score (%) + 10, // Lines practiced + 8, // Lines correct + 120 // Duration (seconds) +); + +// 7. Get statistics +const stats = await api.getUserStats(user.id); +console.log(`Average score: ${stats.average_score}%`); +console.log(`Total time: ${stats.total_practice_time}s`); + +// 8. Get song mastery +const mastery = await api.getSongMastery(user.id, song.id); +console.log(`Mastery level: ${mastery.mastery_level}`); +console.log(`Best score: ${mastery.best_score}%`); +``` + +## 🎯 Fonctionnalités Clés + +### 1. "Generate Once, Store Forever" +- ✅ Phonétique générée **une seule fois** à la création +- ✅ Traduction générée **une seule fois** à la création +- ✅ Stockage dans SQLite (JSON fields) +- ✅ Pas d'appels API lors de l'affichage → **100% offline** + +### 2. Multi-Langues Support +- ✅ **Japonais**: Kanji → Romaji (pykakasi) +- ✅ **Coréen**: Hangul → Latin (hangul-romanize) +- ✅ **Français**: Texte → IPA (epitran) +- ✅ **Anglais**: Texte → IPA (epitran) + +### 3. Traduction Automatique +- ✅ **LibreTranslate API** (gratuit) +- ✅ Retry logic pour rate limiting +- ✅ Timeout 30s +- ✅ Support batch + +### 4. Sécurité +- ✅ **bcrypt** pour hash de mots de passe +- ✅ **JWT** tokens (30 jours expiry) +- ✅ Token verification +- ✅ Duplicate username check + +### 5. Practice Tracking +- ✅ **4 modes** supportés: karaoke, fill-blank, mcq, oral +- ✅ **Score tracking** (%) +- ✅ **Duration tracking** (seconds) +- ✅ **Lines practiced/correct** +- ✅ **Aggregate stats** par utilisateur +- ✅ **Mastery level** par chanson + +## 📊 Métriques + +### Code +- **Backend Rust**: 2,400 lignes +- **Tauri Commands**: 270 lignes +- **TypeScript API**: 200 lignes +- **Integration Test UI**: 160 lignes +- **Total**: ~3,000 lignes + +### Tests +- **11 tests** unitaires (Rust) +- **4 tests** ignorés (network/Python deps) +- **8 fonctionnalités** testées (integration UI) + +### Dependencies +- **Rust**: 11 crates (rusqlite, serde, reqwest, tokio, bcrypt, jwt, pyo3, etc.) +- **Python**: 3 packages (pykakasi, hangul-romanize, epitran) +- **npm**: 53 packages (Vue, Tauri, TypeScript, Vite) + +## 🚀 Prochaines Étapes + +### Pour Développement Complet + +**Phase 1: Core UI** (2-3 jours) +- [ ] Vue Router (navigation) +- [ ] Pinia (state management) +- [ ] Login/Register views +- [ ] Dashboard +- [ ] Song List view + +**Phase 2: Practice Modes UI** (3-4 jours) +- [ ] Karaoke mode (auto-scroll + controls) +- [ ] Fill-blank mode (NOPLP style) +- [ ] MCQ mode (propositions multiples) +- [ ] Oral mode (speech recognition) + +**Phase 3: Advanced** (2-3 jours) +- [ ] Genius API search +- [ ] Import wizard +- [ ] Dark mode +- [ ] i18n (FR/EN/KR/JP) + +**Phase 4: Polish** (1-2 jours) +- [ ] Icons & branding +- [ ] Animations +- [ ] Keyboard shortcuts +- [ ] Error handling UI + +### Pour Production + +- [ ] Build desktop executables +- [ ] Create installers +- [ ] Setup auto-updater +- [ ] PWA for mobile +- [ ] CI/CD pipeline + +## ✅ Status Final + +**Intégration Backend Rust + Tauri: 100% COMPLETE** 🎉 + +- ✅ Backend fonctionnel et testé +- ✅ Tauri commands exposées +- ✅ TypeScript API type-safe +- ✅ Database auto-initialized +- ✅ Integration test UI +- ✅ Documentation complète + +**Prêt pour développement frontend Vue!** 🚀 diff --git a/aidd_docs/memory/external/TECH_CHOICES.md b/aidd_docs/memory/external/TECH_CHOICES.md new file mode 100644 index 0000000..d9c51b4 --- /dev/null +++ b/aidd_docs/memory/external/TECH_CHOICES.md @@ -0,0 +1,561 @@ +# Choix Technologiques - LyRemember + +## Contexte +Nous devons choisir la stack technique AVANT de créer les user stories détaillées et de coder. + +--- + +## 1. Type d'Application + +### Option A : Application Web (Recommandé) +**Avantages :** +- ✅ Accessible depuis n'importe quel appareil (PC, tablette, smartphone) +- ✅ Pas d'installation nécessaire +- ✅ Mise à jour centralisée +- ✅ Meilleure pour affichage multi-colonnes (VO + traduction + phonétique) +- ✅ Meilleur support audio/micro pour mode oral +- ✅ Interface riche pour mode karaoke/défilement + +**Inconvénients :** +- ❌ Nécessite hébergement +- ❌ Plus complexe à développer qu'une CLI +- ❌ Nécessite connexion internet (sauf si PWA) + +**Stack possible :** +- Frontend : React / Vue.js / Svelte +- Backend : Node.js (Express/Fastify) ou Python (Flask/Django) ou Go +- Base de données : PostgreSQL / MongoDB / SQLite +- Hébergement : Vercel / Netlify / Heroku / Railway + +--- + +### Option B : Application Desktop +**Avantages :** +- ✅ Fonctionne offline +- ✅ Bonnes performances +- ✅ Accès complet au système (micro, fichiers) + +**Inconvénients :** +- ❌ Installation nécessaire +- ❌ Maintenance multi-OS (Windows, Mac, Linux) +- ❌ Pas accessible sur mobile + +**Stack possible :** +- Electron (JavaScript/TypeScript) +- Tauri (Rust + Web) +- Python + PyQt/Tkinter + +--- + +### Option C : Application Mobile +**Avantages :** +- ✅ Toujours avec soi +- ✅ Bon pour micro/audio +- ✅ Notifications possibles + +**Inconvénients :** +- ❌ Écran petit pour affichage multi-colonnes +- ❌ Développement iOS + Android +- ❌ App stores (délais, coûts) + +**Stack possible :** +- React Native +- Flutter +- Swift (iOS) + Kotlin (Android) + +--- + +### Option D : CLI (Ligne de Commande) +**Avantages :** +- ✅ Développement rapide +- ✅ Léger +- ✅ Parfait pour développeurs + +**Inconvénients :** +- ❌ Interface limitée (pas idéal pour karaoke/défilement) +- ❌ Difficile pour affichage phonétique/multi-langues +- ❌ Pas adapté pour mode oral (micro compliqué en CLI) +- ❌ Pas user-friendly pour utilisateurs non-techniques + +**Stack possible :** +- Python (déjà commencé) +- Node.js +- Go + +--- + +### Option E : Hybride (Web + Mobile PWA) +**Avantages :** +- ✅ Un seul code pour web + mobile +- ✅ Installable comme une app +- ✅ Fonctionne offline (si configuré) +- ✅ Notifications + +**Inconvénients :** +- ❌ Limites PWA sur iOS +- ❌ Performances légèrement inférieures aux apps natives + +**Stack possible :** +- Frontend : React/Vue/Svelte +- Backend : Idem Option A +- Service Worker pour offline + +--- + +## 2. Langage Backend + +### Option A : Python +**Pour :** +- ✅ Déjà commencé +- ✅ Excellentes bibliothèques NLP/traduction +- ✅ Facile pour API Genius, translittération +- ✅ SpeechRecognition disponible + +**Contre :** +- ❌ Moins performant que Go/Rust +- ❌ Déploiement un peu plus lourd + +**Frameworks :** +- FastAPI (moderne, rapide, async) +- Flask (simple, léger) +- Django (tout inclus, mais lourd pour ce projet) + +--- + +### Option B : Node.js (JavaScript/TypeScript) +**Pour :** +- ✅ Même langage frontend/backend +- ✅ Performant (async) +- ✅ NPM riche en packages +- ✅ Bon écosystème + +**Contre :** +- ❌ Moins de libs pour translittération/NLP que Python +- ❌ Reconnaissance vocale limitée + +**Frameworks :** +- Express (classique) +- Fastify (rapide) +- NestJS (structuré) + +--- + +### Option C : Go +**Pour :** +- ✅ Très performant +- ✅ Compilé (déploiement simple) +- ✅ Bon pour APIs + +**Contre :** +- ❌ Écosystème moins riche pour NLP/traduction +- ❌ Courbe d'apprentissage + +--- + +## 3. Base de Données + +### Option A : PostgreSQL +**Pour :** +- ✅ Robuste, relationnel +- ✅ Bon pour relations User-Songs +- ✅ Support JSON pour métadonnées +- ✅ Gratuit, open-source + +**Contre :** +- ❌ Nécessite serveur séparé +- ❌ Plus complexe que SQLite + +--- + +### Option B : MongoDB +**Pour :** +- ✅ Flexible (schema-less) +- ✅ Bon pour données non structurées +- ✅ Facile à débuter + +**Contre :** +- ❌ Pas idéal pour relations complexes +- ❌ Peut devenir désordonné + +--- + +### Option C : SQLite +**Pour :** +- ✅ Simple, fichier unique +- ✅ Pas de serveur nécessaire +- ✅ Parfait pour MVP + +**Contre :** +- ❌ Pas adapté pour multi-utilisateurs simultanés +- ❌ Moins de fonctionnalités + +--- + +### Option D : JSON Files (actuel) +**Pour :** +- ✅ Ultra simple +- ✅ Humainement lisible +- ✅ Bon pour prototype + +**Contre :** +- ❌ Pas scalable +- ❌ Pas de requêtes complexes +- ❌ Pas de transactions +- ❌ Risque de corruption + +--- + +## 4. Frontend (si Web) + +### Option A : React +**Pour :** +- ✅ Écosystème énorme +- ✅ Beaucoup de composants disponibles +- ✅ Bon pour applications complexes + +**Contre :** +- ❌ Courbe d'apprentissage +- ❌ Boilerplate + +--- + +### Option B : Vue.js +**Pour :** +- ✅ Plus simple que React +- ✅ Documentation excellente +- ✅ Bon compromis + +**Contre :** +- ❌ Écosystème plus petit que React + +--- + +### Option C : Svelte +**Pour :** +- ✅ Très performant +- ✅ Moins de code +- ✅ Compilation, pas de runtime + +**Contre :** +- ❌ Écosystème jeune +- ❌ Moins de ressources + +--- + +## 5. Reconnaissance Vocale (pour mode oral) + +### Option A : Web Speech API (Browser) +**Pour :** +- ✅ Gratuit +- ✅ Intégré au navigateur +- ✅ Facile à utiliser + +**Contre :** +- ❌ Fonctionne uniquement en ligne +- ❌ Support limité (Chrome > Firefox > Safari) +- ❌ Envoie audio à Google + +--- + +### Option B : Google Cloud Speech-to-Text +**Pour :** +- ✅ Très précis +- ✅ Multi-langues +- ✅ API complète + +**Contre :** +- ❌ Payant (après quota gratuit) +- ❌ Nécessite connexion + +**Prix :** 0.006$/15s (gratuit : 60min/mois) + +--- + +### Option C : Whisper (OpenAI) - Offline possible +**Pour :** +- ✅ Très précis +- ✅ Multi-langues +- ✅ Peut tourner en local + +**Contre :** +- ❌ Nécessite GPU pour temps réel +- ❌ Lourd + +--- + +### Option D : SpeechRecognition (Python) +**Pour :** +- ✅ Facile (déjà dans requirements) +- ✅ Plusieurs backends possibles + +**Contre :** +- ❌ Principalement pour CLI/desktop +- ❌ Qualité variable + +--- + +## 6. Traduction Automatique + +### Option A : Google Translate API +**Pour :** +- ✅ Excellente qualité +- ✅ Beaucoup de langues + +**Contre :** +- ❌ Payant (après quota) + +**Prix :** 20$/million caractères + +--- + +### Option B : DeepL API +**Pour :** +- ✅ Meilleure qualité que Google +- ✅ Spécialisé traduction + +**Contre :** +- ❌ Payant +- ❌ Moins de langues + +**Prix :** 5€/mois (500k caractères) + +--- + +### Option C : LibreTranslate (Open Source) +**Pour :** +- ✅ Gratuit +- ✅ Auto-hébergeable +- ✅ Open-source + +**Contre :** +- ❌ Qualité inférieure +- ❌ Moins de langues + +--- + +### Option D : deep-translator (Python lib) +**Pour :** +- ✅ Gratuit (utilise Google Translate non-officiel) +- ✅ Facile + +**Contre :** +- ❌ Pas officiel, peut casser +- ❌ Rate limiting + +--- + +## 7. Translittération/Phonétique + +### Option A : epitran +**Pour :** +- ✅ Multi-langues +- ✅ Basé sur IPA (International Phonetic Alphabet) + +**Contre :** +- ❌ Limité pour idéogrammes + +--- + +### Option B : pykakasi (Japonais) +**Pour :** +- ✅ Spécialisé japonais +- ✅ Kanji → Romaji très bon + +**Contre :** +- ❌ Japonais uniquement + +--- + +### Option C : pinyin (Chinois) +**Pour :** +- ✅ Chinois → Pinyin + +--- + +### Option D : hangul-romanize (Coréen) +**Pour :** +- ✅ Coréen → Romanisation + +--- + +## 8. Hébergement (si Web) + +### Option A : Vercel +**Pour :** +- ✅ Gratuit pour hobby +- ✅ Déploiement facile +- ✅ Excellent pour Next.js/React + +**Contre :** +- ❌ Serverless (limites pour certaines fonctionnalités) + +--- + +### Option B : Railway / Render +**Pour :** +- ✅ Gratuit avec limites +- ✅ Bon pour full-stack +- ✅ Base de données incluse + +**Contre :** +- ❌ Sleep après inactivité (version gratuite) + +--- + +### Option C : Heroku +**Pour :** +- ✅ Classique +- ✅ Add-ons faciles + +**Contre :** +- ❌ Plus de plan gratuit depuis 2022 + +--- + +### Option D : VPS (DigitalOcean, Linode, etc.) +**Pour :** +- ✅ Contrôle total +- ✅ Prévisible + +**Contre :** +- ❌ Nécessite administration +- ❌ ~5$/mois minimum + +--- + +## Recommandations par Scénario + +### Scénario 1 : MVP Rapide (1-2 semaines) +**Type :** Application Web simple +**Stack :** +- Frontend : Vue.js ou React (create-react-app) +- Backend : Python FastAPI (continuer ce qui existe) +- BDD : SQLite +- Hébergement : Vercel (front) + Railway (back) +- Traduction : deep-translator (gratuit) +- Phonétique : epitran + pykakasi +- Vocal : Web Speech API (gratuit) + +**Pourquoi :** Balance rapidité/fonctionnalités, gratuit + +--- + +### Scénario 2 : MVP Premium (4-6 semaines) +**Type :** Application Web PWA +**Stack :** +- Frontend : React + TypeScript +- Backend : Python FastAPI +- BDD : PostgreSQL +- Hébergement : Railway +- Traduction : DeepL API +- Phonétique : epitran + libs spécialisées +- Vocal : Google Cloud Speech-to-Text + +**Pourquoi :** Qualité professionnelle, scalable + +--- + +### Scénario 3 : Application Complète (3+ mois) +**Type :** Web + Mobile (React Native) +**Stack :** +- Frontend Web : Next.js + TypeScript +- Mobile : React Native +- Backend : Node.js (TypeScript) ou Go +- BDD : PostgreSQL +- Hébergement : VPS ou Cloud Platform +- Toutes les fonctionnalités premium + +**Pourquoi :** Solution complète, toutes plateformes + +--- + +## Questions à Répondre + +1. **Quel est le public cible ?** + - [ ] Développeurs/tech-savvy → CLI acceptable + - [ ] Grand public → Web/Mobile nécessaire + +2. **Quel est le budget ?** + - [ ] 0€ → Stack gratuite (deep-translator, Web Speech API, SQLite) + - [ ] ~20€/mois → Services premium (DeepL, Google Speech, PostgreSQL hébergé) + - [ ] ~100€/mois → Infrastructure scalable + +3. **Quelle est la timeline ?** + - [ ] 1-2 semaines → MVP minimal + - [ ] 1-2 mois → MVP solide + - [ ] 3+ mois → Produit complet + +4. **Quelles langues sont prioritaires ?** + - [ ] Japonais/Coréen/Chinois (idéogrammes) → Phonétique crucial + - [ ] Langues européennes → Phonétique moins important + - [ ] Toutes → Solution universelle + +5. **Mode d'utilisation principal ?** + - [ ] Seul à la maison → Desktop/Web OK + - [ ] En déplacement → Mobile nécessaire + - [ ] Les deux → PWA ou Multi-plateforme + +6. **Priorité sur quels modes de pratique ?** + - [ ] Karaoke/défilement → Web meilleur + - [ ] Oral → Bon micro nécessaire + - [ ] Trous/QCM → CLI peut suffire + +--- + +## Ma Recommandation + +**Pour commencer :** + +**🎯 Application Web Progressive (PWA)** + +**Stack :** +- **Frontend :** React + TypeScript + Tailwind CSS +- **Backend :** Python FastAPI (continuer existant) +- **BDD :** PostgreSQL (SQLite pour dev) +- **Auth :** JWT tokens +- **Traduction :** deep-translator (gratuit, upgrade DeepL plus tard) +- **Phonétique :** epitran + pykakasi (JP) + pinyin (CN) +- **Vocal :** Web Speech API (gratuit) +- **Hébergement :** Vercel (front) + Railway (back + BDD) + +**Pourquoi :** +- ✅ Gratuit pour commencer +- ✅ Fonctionne partout (PC, mobile, tablette) +- ✅ Bonne UX pour karaoke/défilement +- ✅ Peut évoluer en mobile app plus tard +- ✅ Réutilise le code Python déjà écrit +- ✅ Stack moderne et demandée + +**Phase 1 (MVP - 2-3 semaines) :** +1. Comptes utilisateurs +2. Import Genius +3. Affichage VO + phonétique +4. Mode défilement basique +5. Mode trous + +**Phase 2 (1-2 semaines) :** +6. Traduction EN +7. Mode QCM +8. Amélioration UI + +**Phase 3 (2-3 semaines) :** +9. Mode vocal +10. Stats avancées +11. PWA (offline, install) + +--- + +## Action Requise + +**Merci de répondre aux questions et de valider :** + +1. Type d'application préféré ? (Web / Desktop / Mobile / CLI / Autre) +2. Budget disponible ? (0€ / ~20€/mois / ~100€/mois / Autre) +3. Timeline souhaitée ? (MVP en combien de temps ?) +4. Langues prioritaires ? (JP/KR/CN / Européennes / Toutes / Autres) +5. D'accord avec ma recommandation ou préférez autre chose ? + +**Une fois validé, je pourrai :** +- Ajuster les user stories selon la stack choisie +- Créer l'architecture technique détaillée +- Commencer l'implémentation diff --git a/aidd_docs/memory/external/TRANSLATION_PHONETIC_STRATEGY.md b/aidd_docs/memory/external/TRANSLATION_PHONETIC_STRATEGY.md new file mode 100644 index 0000000..630d7ac --- /dev/null +++ b/aidd_docs/memory/external/TRANSLATION_PHONETIC_STRATEGY.md @@ -0,0 +1,754 @@ +# Recommandations Techniques : Traduction & Phonétique + +## 1. Stratégie de Traduction (Translate Once, Store Locally) + +### ✅ Votre Approche Est EXCELLENTE + +**Principe : "Translate Once, Use Forever"** + +``` +1. Utilisateur ajoute chanson en japonais +2. App traduit EN automatiquement (via API) +3. Traduction stockée dans SQLite +4. Usage offline ensuite (lecture depuis DB) +``` + +**Avantages :** +- ✅ **Offline** : Fonctionne sans internet après traduction initiale +- ✅ **Rapide** : Pas d'appel API à chaque affichage +- ✅ **Gratuit** : Une seule traduction par chanson (économise quota API) +- ✅ **Cohérent** : Même traduction à chaque fois +- ✅ **Éditable** : Utilisateur peut corriger traduction si besoin + +--- + +## Architecture Traduction + +### Base de Données (SQLite) + +```sql +-- Table songs avec colonne translations +CREATE TABLE songs ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + artist TEXT NOT NULL, + language TEXT NOT NULL, -- 'fr', 'en', 'jp', 'kr' + lyrics TEXT NOT NULL, -- JSON array: ["line 1", "line 2", ...] + + -- Traductions stockées en JSON + translations TEXT, -- JSON: {"en": ["line 1 EN", ...], "fr": [...]} + + -- Métadonnées traduction + translated_languages TEXT, -- JSON: ["en", "fr"] + translation_date TEXT, -- ISO timestamp dernière traduction + + created_at TEXT, + updated_at TEXT +); +``` + +### Workflow Détaillé + +``` +┌─────────────────────────────────────────────────┐ +│ Utilisateur ajoute chanson "千本桜" (JP) │ +└──────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ 1. Sauvegarder chanson avec lyrics originaux │ +│ { │ +│ language: "jp", │ +│ lyrics: ["千本桜", "夜ニ紛レ", ...], │ +│ translations: {} ← vide au début │ +│ } │ +└──────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ 2. Détecter que language != "en" │ +│ → Proposer traduction automatique │ +└──────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ 3. Appeler API traduction (une seule fois) │ +│ Service: Google Translate / DeepL / LibreT │ +│ Input: ["千本桜", "夜ニ紛レ", ...] │ +│ Output: ["Cherry blossoms", "At night", ...]│ +└──────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ 4. Sauvegarder traduction dans SQLite │ +│ UPDATE songs SET │ +│ translations = '{"en": [...]}' │ +│ translation_date = NOW() │ +│ WHERE id = ... │ +└──────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ 5. Usage offline illimité │ +│ SELECT lyrics, translations FROM songs │ +│ → Affichage VO + EN côte à côte │ +│ → Pas d'appel API ! │ +└─────────────────────────────────────────────────┘ +``` + +--- + +## Services de Traduction Recommandés + +### Option 1 : LibreTranslate (RECOMMANDÉ pour gratuit) + +**Service open-source, auto-hébergeable** + +```rust +// Rust backend +use reqwest; + +async fn translate_with_libretranslate( + text: Vec, + source_lang: &str, // "ja", "ko", etc. + target_lang: &str, // "en" +) -> Result, String> { + let client = reqwest::Client::new(); + + let mut translated = Vec::new(); + + for line in text { + let response = client + .post("https://libretranslate.com/translate") // API publique gratuite + .json(&serde_json::json!({ + "q": line, + "source": source_lang, + "target": target_lang, + "format": "text" + })) + .send() + .await + .map_err(|e| e.to_string())?; + + let data: serde_json::Value = response.json() + .await + .map_err(|e| e.to_string())?; + + translated.push( + data["translatedText"] + .as_str() + .unwrap_or("") + .to_string() + ); + } + + Ok(translated) +} +``` + +**Avantages :** +- ✅ **Gratuit** (API publique : 5 req/min) +- ✅ **Open-source** +- ✅ **Auto-hébergeable** si besoin +- ✅ **Pas de compte** nécessaire +- ✅ **Bonne qualité** pour usage basique + +**Limites :** +- ⚠️ Rate limit (5 req/min sur instance publique) +- ⚠️ Qualité inférieure à Google/DeepL +- ⚠️ Langues limitées (mais FR/EN/JP/KR OK) + +--- + +### Option 2 : Google Translate (Unofficial via googletrans-rs) + +```rust +// Utiliser crate googletrans-rs (scraping, pas officiel) +// Gratuit mais peut casser + +use googletrans::{Translator, Lang}; + +async fn translate_with_google_unofficial( + text: Vec, + target_lang: Lang, +) -> Result, String> { + let translator = Translator::new(); + + let mut translated = Vec::new(); + + for line in text { + let result = translator + .translate(&line, Lang::Auto, target_lang) + .await + .map_err(|e| e.to_string())?; + + translated.push(result.text); + } + + Ok(translated) +} +``` + +**Avantages :** +- ✅ **Gratuit** (scraping) +- ✅ **Qualité Google Translate** +- ✅ **Pas de clé API** + +**Inconvénients :** +- ❌ **Non officiel** (peut être bloqué) +- ❌ **Peut casser** à tout moment +- ❌ **Rate limiting** agressif + +--- + +### Option 3 : DeepL API (Meilleure qualité, limité gratuit) + +```rust +use reqwest; + +async fn translate_with_deepl( + text: Vec, + source_lang: &str, + target_lang: &str, + api_key: &str, +) -> Result, String> { + let client = reqwest::Client::new(); + + // DeepL supporte batch translation + let response = client + .post("https://api-free.deepl.com/v2/translate") + .header("Authorization", format!("DeepL-Auth-Key {}", api_key)) + .json(&serde_json::json!({ + "text": text, + "source_lang": source_lang.to_uppercase(), + "target_lang": target_lang.to_uppercase(), + })) + .send() + .await + .map_err(|e| e.to_string())?; + + let data: serde_json::Value = response.json() + .await + .map_err(|e| e.to_string())?; + + let translated: Vec = data["translations"] + .as_array() + .ok_or("Invalid response")? + .iter() + .map(|t| t["text"].as_str().unwrap_or("").to_string()) + .collect(); + + Ok(translated) +} +``` + +**Avantages :** +- ✅ **Meilleure qualité** de traduction +- ✅ **Plan gratuit** : 500k caractères/mois +- ✅ **Officiel et stable** +- ✅ **Batch translation** (plusieurs lignes en une requête) + +**Inconvénients :** +- ⚠️ **Nécessite API key** (inscription) +- ⚠️ **Quota limité** (500k caractères gratuits) +- ⚠️ **Payant** après quota (5€/mois pour 500k supplémentaires) + +--- + +### Recommandation Traduction + +**Pour MVP (gratuit, usage personnel) :** +``` +1. LibreTranslate (API publique) + - Gratuit illimité (5 req/min) + - Bonne qualité + - Auto-hébergeable si besoin + +2. Fallback : Traduction manuelle + - Utilisateur peut ajouter traduction manuellement + - Champ éditable dans l'interface +``` + +**Pour usage intensif :** +``` +DeepL API (500k caractères/mois gratuit) +- Inscription simple +- Excellente qualité +- Batch translation efficace +``` + +--- + +## 2. Solution Phonétique Recommandée + +### Stratégie : Hybride (Bibliothèques + FFI Python) + +### Langues à Supporter + +**Niveau 1 (Prioritaire) :** +- 🇯🇵 **Japonais** → Romaji (Latin) +- 🇰🇷 **Coréen** → Romanization (Latin) + +**Niveau 2 (Bonus) :** +- 🇫🇷 **Français** → IPA ou phonétique simplifiée +- 🇬🇧 **Anglais** → IPA ou phonétique simplifiée + +--- + +### Solution Recommandée : PyO3 (Appeler Python depuis Rust) + +**Pourquoi ?** +- Excellentes libs Python pour phonétique (pykakasi, hangul-romanize) +- Pas d'équivalent mature en Rust +- PyO3 permet d'appeler Python depuis Rust +- Facile à maintenir + +### Architecture + +``` +┌──────────────────────────────────────┐ +│ Frontend Vue (JavaScript) │ +└───────────────┬──────────────────────┘ + │ invoke('generate_phonetic') +┌───────────────▼──────────────────────┐ +│ Backend Rust (Tauri) │ +│ │ +│ #[tauri::command] │ +│ fn generate_phonetic( │ +│ text: Vec, │ +│ language: String │ +│ ) -> Vec { │ +│ match language.as_str() { │ +│ "jp" => call_pykakasi(text),│ ← PyO3 +│ "kr" => call_hangul(text), │ ← PyO3 +│ "fr" => call_epitran(text), │ ← PyO3 +│ "en" => call_epitran(text), │ ← PyO3 +│ _ => text │ +│ } │ +│ } │ +│ │ +│ ┌────────────────────────────────┐ │ +│ │ PyO3 Bridge │ │ +│ │ (Rust appelle Python) │ │ +│ │ │ │ +│ │ ┌──────────────────────────┐ │ │ +│ │ │ Python Environment │ │ │ +│ │ │ - pykakasi (JP) │ │ │ +│ │ │ - hangul-romanize (KR) │ │ │ +│ │ │ - epitran (FR/EN) │ │ │ +│ │ └──────────────────────────┘ │ │ +│ └────────────────────────────────┘ │ +└──────────────────────────────────────┘ +``` + +--- + +### Implémentation PyO3 + +#### 1. Setup PyO3 dans Cargo.toml + +```toml +# src-tauri/Cargo.toml +[dependencies] +pyo3 = { version = "0.20", features = ["auto-initialize"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +``` + +#### 2. Code Rust avec PyO3 + +```rust +// src-tauri/src/phonetic.rs +use pyo3::prelude::*; +use pyo3::types::PyList; + +/// Convertir japonais (kanji) → romaji +pub fn japanese_to_romaji(text: Vec) -> Result, String> { + Python::with_gil(|py| { + // Importer pykakasi + let kakasi_module = py.import("pykakasi") + .map_err(|e| format!("Failed to import pykakasi: {}", e))?; + + // Créer instance kakasi + let kakasi_class = kakasi_module.getattr("kakasi") + .map_err(|e| e.to_string())?; + let kakasi = kakasi_class.call0() + .map_err(|e| e.to_string())?; + + let mut result = Vec::new(); + + for line in text { + // Convertir chaque ligne + let converted = kakasi + .call_method1("convert", (line,)) + .map_err(|e| e.to_string())?; + + // Extraire romaji de chaque segment + let py_list: &PyList = converted.downcast() + .map_err(|e| e.to_string())?; + + let mut romaji_line = String::new(); + for item in py_list { + let dict = item.downcast::() + .map_err(|e| e.to_string())?; + let romaji = dict.get_item("hepburn") + .ok_or("No hepburn key")? + .ok_or("hepburn is None")? + .extract::() + .map_err(|e| e.to_string())?; + romaji_line.push_str(&romaji); + } + + result.push(romaji_line); + } + + Ok(result) + }) +} + +/// Convertir coréen (hangul) → romanization +pub fn korean_to_roman(text: Vec) -> Result, String> { + Python::with_gil(|py| { + // Importer hangul_romanize + let module = py.import("hangul_romanize") + .map_err(|e| format!("Failed to import hangul_romanize: {}", e))?; + + let romanize_fn = module.getattr("Transliter") + .map_err(|e| e.to_string())?; + + let mut result = Vec::new(); + + for line in text { + let romanized = romanize_fn + .call1((line,)) + .map_err(|e| e.to_string())? + .extract::() + .map_err(|e| e.to_string())?; + + result.push(romanized); + } + + Ok(result) + }) +} + +/// Convertir français/anglais → IPA +pub fn to_ipa(text: Vec, lang: &str) -> Result, String> { + Python::with_gil(|py| { + let epitran_module = py.import("epitran") + .map_err(|e| format!("Failed to import epitran: {}", e))?; + + let epitran_class = epitran_module.getattr("Epitran") + .map_err(|e| e.to_string())?; + + // Créer instance pour la langue + let lang_code = match lang { + "fr" => "fra-Latn", + "en" => "eng-Latn", + _ => return Err("Unsupported language".to_string()), + }; + + let epitran = epitran_class.call1((lang_code,)) + .map_err(|e| e.to_string())?; + + let mut result = Vec::new(); + + for line in text { + let ipa = epitran + .call_method1("transliterate", (line,)) + .map_err(|e| e.to_string())? + .extract::() + .map_err(|e| e.to_string())?; + + result.push(ipa); + } + + Ok(result) + }) +} + +/// Fonction principale exposée à Tauri +pub fn generate_phonetic(text: Vec, language: &str) -> Result, String> { + match language { + "jp" => japanese_to_romaji(text), + "kr" => korean_to_roman(text), + "fr" => to_ipa(text, "fr"), + "en" => to_ipa(text, "en"), + _ => Ok(text), // Retourner texte original si langue non supportée + } +} +``` + +#### 3. Commande Tauri + +```rust +// src-tauri/src/commands/phonetic.rs +use crate::phonetic; + +#[tauri::command] +pub async fn generate_phonetic_lyrics( + lyrics: Vec, + language: String, +) -> Result, String> { + // Appeler fonction phonetic avec PyO3 + phonetic::generate_phonetic(lyrics, &language) +} +``` + +#### 4. Enregistrer dans main.rs + +```rust +// src-tauri/src/main.rs +mod phonetic; +mod commands; + +fn main() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![ + commands::phonetic::generate_phonetic_lyrics, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +--- + +### Setup Python Environment pour PyO3 + +#### requirements.txt pour Python +```txt +pykakasi>=2.2.1 +hangul-romanize>=0.1.0 +epitran>=1.24 +``` + +#### Installation + +```bash +# Dans le projet Tauri +pip install pykakasi hangul-romanize epitran + +# Ou via script de build +``` + +#### Distribution + +**Problème :** L'app doit avoir Python installé pour utiliser PyO3 + +**Solutions :** + +**Option A : Embedded Python (Recommandé)** +```toml +# Cargo.toml +[dependencies] +pyo3 = { version = "0.20", features = ["auto-initialize", "extension-module"] } +``` +- Embarquer Python dans l'app +- Utilisateur n'a pas besoin d'installer Python +- App standalone + +**Option B : Require Python** +- Documenter que Python 3.8+ requis +- Script d'installation vérifie Python +- Plus simple mais moins user-friendly + +--- + +### Alternative Pure Rust (Plus complexe) + +Si vous voulez éviter PyO3, implémentation Rust pure : + +#### Japonais : wana_kana crate + +```rust +// Japonais basique (hiragana/katakana → romaji) +use wana_kana; + +fn japanese_basic_romaji(text: String) -> String { + wana_kana::to_romaji(&text) +} +``` + +**Limite :** Ne gère PAS kanji → romaji (nécessite dictionnaire) + +#### Coréen : Implémenter manuellement + +```rust +// Algorithme de romanization coréen +// Complexe mais faisable +fn korean_to_roman_pure_rust(hangul: &str) -> String { + // Décomposer hangul en jamo (consonnes/voyelles) + // Appliquer règles romanization + // ~200 lignes de code + todo!() +} +``` + +**Complexité :** Haute, nécessite maintenir dictionnaire + +--- + +## Recommandation Finale Phonétique + +### Pour MVP (Recommandé) + +**PyO3 + Python libs** +``` +✅ Japonais : pykakasi (excellent) +✅ Coréen : hangul-romanize (bon) +✅ Français : epitran (basique) +✅ Anglais : epitran (basique) +``` + +**Avantages :** +- Qualité excellente (libs matures) +- Facile à implémenter +- Maintenable + +**Inconvénient :** +- Dépendance Python (mais embarquable) + +--- + +### Pour Production (Plus tard) + +Si vous voulez éliminer dépendance Python : + +**Services externes** +- API cloud pour phonétique +- Translate once, store locally (même stratégie) + +**Ou Rust pur + dictionnaires** +- Complexe mais pas de dépendance +- Nécessite beaucoup plus de code + +--- + +## Stockage Phonétique (Même Stratégie que Traduction) + +### "Phonetize Once, Use Forever" + +```sql +CREATE TABLE songs ( + id TEXT PRIMARY KEY, + language TEXT NOT NULL, + lyrics TEXT NOT NULL, -- Original + phonetic_lyrics TEXT, -- Phonétique (généré une fois) + phonetic_date TEXT, -- Timestamp génération + translations TEXT -- {"en": [...]} +); +``` + +### Workflow + +``` +1. Utilisateur ajoute chanson japonaise + → lyrics: ["千本桜", ...] + → phonetic_lyrics: NULL + +2. App génère phonétique (PyO3 + pykakasi) + → phonetic_lyrics: ["Senbonzakura", ...] + → Stocke dans SQLite + +3. Usage offline illimité + → SELECT lyrics, phonetic_lyrics + → Affichage côte à côte + → Pas de recalcul ! +``` + +--- + +## Architecture Complète : Traduction + Phonétique + +``` +┌────────────────────────────────────────────────────┐ +│ Utilisateur ajoute "上を向いて歩こう" (JP) │ +└──────────────────┬─────────────────────────────────┘ + │ + ┌──────────────┴─────────────────┐ + │ │ + ▼ ▼ +┌─────────────────┐ ┌──────────────────────┐ +│ Traduction EN │ │ Phonétique Romaji │ +│ (API - 1 fois) │ │ (PyO3 - 1 fois) │ +└────────┬────────┘ └──────────┬───────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────┐ +│ SQLite Database │ +│ │ +│ lyrics: ["上を向いて歩こう"] │ +│ phonetic_lyrics: ["Ue wo muite arukou"] │ +│ translations: { │ +│ "en": ["Let's walk looking up"] │ +│ } │ +└─────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ Usage Offline Illimité │ +│ ┌───────────────────────────────────────────┐ │ +│ │ Original (JP) : 上を向いて歩こう │ │ +│ │ Phonétique : Ue wo muite arukou │ │ +│ │ Traduction EN : Let's walk looking up │ │ +│ └───────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +--- + +## Résumé Recommandations + +### Traduction +✅ **LibreTranslate** (gratuit, public API) +- Traduire lors de l'ajout de chanson +- Stocker dans SQLite (JSON field) +- Offline ensuite + +**Alternative :** DeepL (meilleure qualité, 500k/mois gratuit) + +### Phonétique +✅ **PyO3 + Python libs** +- pykakasi (JP → romaji) +- hangul-romanize (KR → roman) +- epitran (FR/EN → IPA) +- Générer lors de l'ajout +- Stocker dans SQLite +- Offline ensuite + +**Alternative :** Rust pur (plus complexe, pas de dépendance Python) + +### Architecture +``` +┌──────────────────────────────┐ +│ Add Song Flow │ +├──────────────────────────────┤ +│ 1. Save original lyrics │ +│ 2. Translate (LibreTranslate)│ +│ 3. Generate phonetic (PyO3) │ +│ 4. Store all in SQLite │ +└──────────────────────────────┘ + │ + ▼ +┌──────────────────────────────┐ +│ Display Flow (Offline) │ +├──────────────────────────────┤ +│ SELECT FROM songs │ +│ → Original │ +│ → Phonetic │ +│ → Translation │ +│ No API calls! │ +└──────────────────────────────┘ +``` + +--- + +## Prêt à Implémenter ? + +**Phase 1 :** Backend Rust avec PyO3 +**Phase 2 :** Traduction avec LibreTranslate +**Phase 3 :** Stockage SQLite +**Phase 4 :** Frontend Vue affichage + +**Voulez-vous que je commence l'implémentation ?** 🚀 diff --git a/aidd_docs/memory/external/UI_LIBRARIES.md b/aidd_docs/memory/external/UI_LIBRARIES.md new file mode 100644 index 0000000..87413dc --- /dev/null +++ b/aidd_docs/memory/external/UI_LIBRARIES.md @@ -0,0 +1,590 @@ +# Meilleures Bibliothèques UI Multi-OS pour LyRemember + +## Contexte +Besoin : Interface utilisateur fonctionnant sur Desktop (Windows, macOS, Linux) ET Mobile (Android, iOS) + +--- + +## 🏆 TOP 3 Recommandations + +### 1. **React + Tauri (Frontend Web)** ⭐ RECOMMANDÉ + +**Concept :** UI web (HTML/CSS/JS) dans un WebView natif + +**Stack :** +- **UI Library :** React + Tailwind CSS (ou Shadcn/ui, Material UI, Chakra UI) +- **Runtime :** WebView natif de l'OS (pas de Chromium embarqué) +- **Wrapper :** Tauri (desktop) + Tauri Mobile (mobile beta) + +**Pour :** +- ✅ **Très léger** : Utilise le WebView de l'OS (3-5 MB) +- ✅ **Un seul code** pour toutes plateformes +- ✅ **Écosystème React** : Milliers de composants disponibles +- ✅ **Design moderne** : Tailwind/Shadcn très populaires +- ✅ **Facile** : Si vous connaissez le web +- ✅ **Responsive** : S'adapte automatiquement desktop/mobile +- ✅ **Gratuit** : Tout open-source + +**Contre :** +- ❌ **Pas 100% natif** : Ressemble à une app native mais reste du web +- ❌ **Mobile beta** : Tauri Mobile pas encore stable +- ❌ **Dépend du WebView** : Peut varier selon OS/version + +**Composants UI recommandés :** +- **Shadcn/ui** : Composants modernes, accessibles, customizable +- **Headless UI** : Composants accessibles sans style +- **Radix UI** : Primitives UI de haute qualité +- **Tailwind CSS** : Utility-first CSS framework + +**Exemple :** +```tsx +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" + +function SongCard({ song }) { + return ( + + + {song.title} + + +

{song.artist}

+ +
+
+ ) +} +``` + +--- + +### 2. **Flutter** ⭐⭐ Alternative Solide + +**Concept :** Framework UI de Google avec propre moteur de rendu + +**Pour :** +- ✅ **Vraiment multi-OS** : Mobile (iOS/Android) + Desktop (Win/Mac/Linux) + Web +- ✅ **Performance native** : Compile en code natif +- ✅ **UI cohérente** : Même look sur tous OS +- ✅ **Hot reload** : Dev rapide +- ✅ **Material Design** intégré +- ✅ **Mature** : Utilisé par Google, BMW, Alibaba... + +**Contre :** +- ❌ **Nouveau langage** : Dart (pas Rust, pas JS) +- ❌ **Lourd** : Apps 15-30 MB minimum +- ❌ **Moins de libs** que React pour certaines choses +- ❌ **Pas de Genius API** native (faudra faire appels HTTP) + +**Composants UI :** +- **Material Design 3** : Design system de Google +- **Cupertino** : Style iOS +- **flutter_hooks** : React-like hooks + +**Exemple :** +```dart +Card( + child: Column( + children: [ + ListTile( + title: Text(song.title), + subtitle: Text(song.artist), + ), + ButtonBar( + children: [ + TextButton( + onPressed: () => practice(song), + child: Text('PRATIQUER'), + ), + ], + ), + ], + ), +) +``` + +--- + +### 3. **React Native** ⭐ Pour Mobile Priority + +**Concept :** React qui compile en composants natifs + +**Pour :** +- ✅ **Vraies apps natives** : Composants iOS/Android natifs +- ✅ **React** : Même syntaxe que React web +- ✅ **Performance** : Bon pour mobile +- ✅ **Écosystème** : Énorme (Expo, nombreuses libs) +- ✅ **Hot reload** + +**Contre :** +- ❌ **Pas desktop natif** : Faudra Electron ou autre pour desktop +- ❌ **Setup complexe** : Xcode, Android Studio requis +- ❌ **Lourd** : 30+ MB apps +- ❌ **Bugs plateforme** : Parfois différences iOS/Android + +**Pour Desktop :** +- Utiliser **React Native Windows + macOS** (Microsoft) +- Ou séparer : React Native (mobile) + Tauri (desktop) + +**UI Libraries :** +- **React Native Paper** : Material Design +- **Native Base** : Composants accessibles +- **React Native Elements** + +--- + +## 📊 Comparaison Détaillée + +| Critère | React+Tauri | Flutter | React Native | +|---------|-------------|---------|--------------| +| **Desktop** | ⭐⭐⭐ Excellent | ⭐⭐⭐ Excellent | ⭐⭐ Via RN Windows | +| **Mobile** | ⭐⭐ Beta | ⭐⭐⭐ Excellent | ⭐⭐⭐ Excellent | +| **Taille App** | ⭐⭐⭐ 3-5 MB | ⭐⭐ 15-30 MB | ⭐⭐ 30+ MB | +| **Performance** | ⭐⭐⭐ Très bon | ⭐⭐⭐ Natif | ⭐⭐⭐ Natif | +| **UI Native** | ⭐⭐ Web-like | ⭐⭐ Custom | ⭐⭐⭐ Natif | +| **Courbe apprentissage** | ⭐⭐⭐ Facile (si React) | ⭐⭐ Moyen (Dart) | ⭐⭐⭐ Facile (si React) | +| **Écosystème** | ⭐⭐⭐ React énorme | ⭐⭐ Bon | ⭐⭐⭐ Énorme | +| **Maturité Desktop** | ⭐⭐⭐ Stable | ⭐⭐⭐ Stable | ⭐ Limité | +| **Maturité Mobile** | ⭐ Beta | ⭐⭐⭐ Prod-ready | ⭐⭐⭐ Prod-ready | +| **Hot Reload** | ⭐⭐⭐ Vite HMR | ⭐⭐⭐ Excellent | ⭐⭐⭐ Excellent | +| **Gratuit** | ⭐⭐⭐ Oui | ⭐⭐⭐ Oui | ⭐⭐⭐ Oui | + +--- + +## 🎨 Bibliothèques UI Spécifiques + +### Pour React + Tauri + +#### **1. Shadcn/ui** 🏆 TOP CHOIX +```bash +npx shadcn-ui@latest init +``` + +**Caractéristiques :** +- Composants modernes et beaux +- Basé sur Radix UI (accessible) +- Tailwind CSS +- Vous possédez le code (copié dans votre projet) +- Dark mode inclus +- Responsive + +**Composants disponibles :** +- Button, Card, Dialog, Dropdown, Input, Select +- Tabs, Toast, Tooltip, Popover +- Table, Form, Alert, Badge +- Command palette, Calendar, etc. + +**Parfait pour :** +- Design moderne et professionnel +- Applications avec beaucoup de formulaires +- Besoin de customisation + +--- + +#### **2. Material UI (MUI)** +```bash +npm install @mui/material @emotion/react @emotion/styled +``` + +**Caractéristiques :** +- Material Design (Google) +- Très complet (100+ composants) +- Thèmes personnalisables +- Bien documenté + +**Parfait pour :** +- Design familier (style Android) +- Applications complexes +- Besoin de tout out-of-the-box + +--- + +#### **3. Chakra UI** +```bash +npm install @chakra-ui/react @emotion/react @emotion/styled +``` + +**Caractéristiques :** +- Simple et accessible +- Dark mode facile +- Composants modulaires +- Bon pour prototypage rapide + +--- + +#### **4. Ant Design** +```bash +npm install antd +``` + +**Caractéristiques :** +- Design enterprise (chinois) +- Très complet +- Internationalisation intégrée +- Parfait pour apps complexes + +--- + +### Pour Flutter + +#### **Material Design 3** +```dart +dependencies: + flutter: + sdk: flutter +``` + +**Inclus par défaut :** +- Tous composants Material +- Thèmes personnalisables +- Animations fluides + +--- + +#### **Cupertino (iOS Style)** +```dart +import 'package:flutter/cupertino.dart'; +``` + +**Pour style iOS :** +- Ressemble à apps iOS natives +- Navigation iOS +- Widgets iOS + +--- + +### Pour React Native + +#### **React Native Paper** +```bash +npm install react-native-paper +``` + +**Material Design pour React Native** + +--- + +## 🎯 Recommandation Finale + +### **Pour votre projet (Desktop + Mobile, FR/EN/KR/JP) :** + +### Option 1 : React + Tauri + Shadcn/ui (RECOMMANDÉ) 🏆 + +**Stack complète :** +``` +Frontend : React 18 + TypeScript +UI Library : Shadcn/ui + Tailwind CSS +Icons : Lucide React +Desktop : Tauri (Rust) +Mobile : Tauri Mobile (beta) OU PWA fallback +``` + +**Pourquoi :** +- ✅ **Meilleur compromis** desktop/mobile +- ✅ **UI moderne** et professionnelle +- ✅ **Léger** (3-5 MB desktop) +- ✅ **Un seul codebase** +- ✅ **Gratuit** et open-source +- ✅ **Facile** si vous connaissez React +- ✅ **Shadcn/ui** : Très populaire en 2026, beau, customizable + +**Setup :** +```bash +# 1. Créer projet Tauri +npm create tauri-app + +# 2. Choisir React + TypeScript + Vite + +# 3. Ajouter Tailwind +npm install -D tailwindcss postcss autoprefixer +npx tailwindcss init -p + +# 4. Ajouter Shadcn/ui +npx shadcn-ui@latest init + +# 5. Ajouter composants au besoin +npx shadcn-ui@latest add button card input +``` + +**Exemple composant :** +```tsx +// SongCard.tsx +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Music, Play } from "lucide-react" + +interface Song { + title: string + artist: string + language: string + mastery: number +} + +export function SongCard({ song }: { song: Song }) { + return ( + + + +
+ {song.title} +

{song.artist}

+
+ + {song.language.toUpperCase()} + +
+ +
+ Maîtrise : {song.mastery}% + +
+
+
+ ) +} +``` + +**Design System Shadcn/ui :** +- **Couleurs :** Personnalisables via CSS variables +- **Dark mode :** Toggle facile +- **Responsive :** Mobile-first par défaut +- **Accessible :** ARIA labels, keyboard navigation +- **Animations :** Transitions fluides + +--- + +### Option 2 : Flutter (Si mobile prioritaire) + +**Stack :** +``` +Framework : Flutter +UI : Material Design 3 +Icons : Material Icons +Desktop : Flutter Desktop +Mobile : Flutter Mobile +``` + +**Pourquoi :** +- ✅ **Mobile excellent** +- ✅ **Desktop stable** +- ✅ **UI cohérente** partout +- ❌ Nouveau langage (Dart) +- ❌ Plus lourd + +--- + +### Option 3 : Hybride (Best of both) + +**Desktop :** Tauri + React + Shadcn/ui +**Mobile :** PWA OU React Native + +**Pourquoi :** +- ✅ **Meilleur** de chaque monde +- ✅ **Desktop natif** et ultra léger +- ✅ **Mobile stable** (PWA éprouvé) +- ❌ Deux codebases (partiel) + +--- + +## 🛠️ Exemples de Design + +### Avec Shadcn/ui (React + Tauri) + +**Page de pratique Karaoke :** +```tsx +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Slider } from "@/components/ui/slider" +import { Play, Pause, SkipForward, SkipBack } from "lucide-react" + +export function KaraokeMode({ song }) { + const [isPaused, setIsPaused] = useState(false) + const [speed, setSpeed] = useState([3]) + const [currentLine, setCurrentLine] = useState(0) + + return ( +
+ + + {/* Ligne courante */} +
+

+ Ligne {currentLine + 1} / {song.lyrics.length} +

+

+ {song.lyrics[currentLine]} +

+ {song.phonetic_lyrics && ( +

+ {song.phonetic_lyrics[currentLine]} +

+ )} +
+ + {/* Contrôles */} +
+
+ + + +
+ +
+
+ Vitesse : {speed[0]}s/ligne +
+ +
+
+
+
+
+ ) +} +``` + +**Résultat :** Interface moderne, fluide, responsive, dark mode, animations + +--- + +## 📱 Responsive Design + +### Avec Tailwind (Shadcn/ui) + +```tsx +
+ {songs.map(song => )} +
+``` + +**Breakpoints Tailwind :** +- `sm:` 640px (mobile large) +- `md:` 768px (tablette) +- `lg:` 1024px (desktop) +- `xl:` 1280px (grand écran) + +--- + +## 🎨 Thèmes & Dark Mode + +### Shadcn/ui Dark Mode + +```tsx +// Provider +import { ThemeProvider } from "@/components/theme-provider" + + + + + +// Toggle +import { Moon, Sun } from "lucide-react" +import { Button } from "@/components/ui/button" +import { useTheme } from "@/components/theme-provider" + +export function ThemeToggle() { + const { theme, setTheme } = useTheme() + + return ( + + ) +} +``` + +--- + +## 🌐 Support Multi-Langues UI + +### i18next (pour React) + +```bash +npm install react-i18next i18next +``` + +```tsx +// i18n.ts +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' + +i18n.use(initReactI18next).init({ + resources: { + en: { translation: { practice: "Practice", songs: "Songs" } }, + fr: { translation: { practice: "Pratiquer", songs: "Chansons" } }, + ko: { translation: { practice: "연습", songs: "노래" } }, + ja: { translation: { practice: "練習", songs: "曲" } }, + }, + lng: 'en', + fallbackLng: 'en', +}) + +// Usage +import { useTranslation } from 'react-i18next' + +function MyComponent() { + const { t } = useTranslation() + return +} +``` + +--- + +## ✅ Verdict Final + +### **Pour LyRemember : React + Tauri + Shadcn/ui** + +**Raisons :** +1. ✅ **Desktop excellent** (Tauri natif) +2. ✅ **Mobile via PWA** (stable) ou Tauri Mobile (beta) +3. ✅ **UI moderne** (Shadcn/ui très populaire) +4. ✅ **Léger** (3-5 MB) +5. ✅ **Gratuit** (tout open-source) +6. ✅ **Un codebase** (desktop + mobile) +7. ✅ **React** (énorme communauté) +8. ✅ **Tailwind** (responsive facile) +9. ✅ **Dark mode** inclus +10. ✅ **Customizable** (vous possédez le code Shadcn) + +**Stack finale recommandée :** +``` +Frontend : React 18 + TypeScript + Vite +UI : Shadcn/ui + Tailwind CSS +Icons : Lucide React +State : Zustand +Router : React Router +i18n : react-i18next +Desktop : Tauri (Rust) +Mobile : PWA (Service Worker) +Backend : Rust (Tauri commands) +BDD : SQLite (rusqlite) +``` + +**Cette stack vous donne :** Application ultra moderne, performante, légère, belle, et qui fonctionne partout ! 🚀 diff --git a/aidd_docs/memory/external/USER_STORIES.md b/aidd_docs/memory/external/USER_STORIES.md new file mode 100644 index 0000000..664451e --- /dev/null +++ b/aidd_docs/memory/external/USER_STORIES.md @@ -0,0 +1,418 @@ +# User Stories — LyRemember (Document Unifié) + +> **Source** : fusion de `USER_STORIES.md` (v1, EN) et `USER_STORIES_V2.md` (v2, FR). +> **Contexte** : app desktop **Tauri + Vue 3** — toutes les interactions se font via l'interface graphique, pas en CLI. +> **Légende statut** : ✅ Backend prêt · ⏳ UI à implémenter · ❌ Non commencé + +--- + +## Epic 1 — Gestion des Comptes Utilisateur + +### US-1.1 : Créer un compte ✅ Backend · ⏳ UI +**En tant qu'** utilisateur +**Je veux** créer un compte avec mes informations +**Afin de** sauvegarder mon répertoire personnel de chansons + +**Critères d'acceptation :** +- [ ] Formulaire d'inscription : nom d'utilisateur, email, mot de passe +- [ ] Nom d'utilisateur unique vérifié par le système +- [ ] Mot de passe stocké de manière sécurisée (bcrypt) +- [ ] Message de confirmation affiché après inscription +- [ ] Données persistantes entre les sessions (SQLite) + +**Scénario (UI Tauri) :** +> L'utilisateur ouvre l'app → clique « Créer un compte » → remplit le formulaire → valide → reçoit une confirmation → peut se connecter avec ces identifiants. + +--- + +### US-1.2 : Se connecter ✅ Backend · ⏳ UI +**En tant qu'** utilisateur inscrit +**Je veux** me connecter à mon compte +**Afin d'** accéder à mon répertoire personnel + +**Critères d'acceptation :** +- [ ] Formulaire de connexion : nom d'utilisateur + mot de passe +- [ ] Message d'erreur explicite si identifiants incorrects +- [ ] Session active pendant toute l'utilisation (JWT en mémoire) +- [ ] Bouton de déconnexion accessible depuis l'interface + +--- + +### US-1.3 : Lier un compte Genius ✅ Backend · ⏳ UI +**En tant qu'** utilisateur +**Je veux** lier mon compte Genius +**Afin de** pouvoir importer facilement des paroles depuis Genius + +**Critères d'acceptation :** +- [ ] Champ de saisie du token API Genius dans les paramètres +- [ ] Token stocké de manière sécurisée dans le profil utilisateur +- [ ] Validation de la connexion au token avant sauvegarde +- [ ] Possibilité de modifier ou supprimer le token à tout moment + +> **Note technique** : token obtenu sur https://genius.com/api-clients + +--- + +## Epic 2 — Gestion du Répertoire Personnel + +### US-2.1 : Ajouter une chanson au répertoire ✅ Backend · ⏳ UI +**En tant qu'** utilisateur connecté +**Je veux** ajouter des chansons à mon répertoire +**Afin de** constituer ma collection personnelle + +**Critères d'acceptation :** +- [ ] Ajout manuel : titre, artiste, langue, paroles (copier-coller ou saisie) +- [ ] Import depuis Genius (si token lié) : recherche par titre/artiste +- [ ] Confirmation visible après ajout +- [ ] Chanson visible uniquement dans le répertoire de l'utilisateur + +--- + +### US-2.2 : Voir mon répertoire ✅ Backend · ⏳ UI +**En tant qu'** utilisateur connecté +**Je veux** voir la liste de mes chansons +**Afin de** choisir laquelle pratiquer + +**Critères d'acceptation :** +- [ ] Liste affichant : titre, artiste, langue, progression +- [ ] Filtre par langue +- [ ] Recherche dans le répertoire (titre / artiste) +- [ ] Accès rapide au détail ou à la pratique depuis la liste + +--- + +### US-2.3 : Retirer une chanson du répertoire ✅ Backend · ⏳ UI +**En tant qu'** utilisateur +**Je veux** retirer des chansons de mon répertoire +**Afin de** garder seulement celles qui m'intéressent + +**Critères d'acceptation :** +- [ ] Action « Retirer » avec confirmation +- [ ] La chanson disparaît de la liste après confirmation +- [ ] Progression conservée dans la base (au cas où elle est ré-ajoutée) + +--- + +## Epic 3 — Affichage Multi-Langues et Phonétique + +### US-3.1 : Voir les paroles en version originale ✅ Backend · ⏳ UI +**En tant qu'** utilisateur +**Je veux** voir les paroles dans la langue originale +**Afin de** apprendre la chanson telle qu'elle est chantée + +**Critères d'acceptation :** +- [ ] Affichage des paroles en caractères originaux (japonais, coréen, arabe, etc.) +- [ ] Langue clairement indiquée (badge/label) +- [ ] Lisibilité : une ligne de chanson par ligne d'écran + +--- + +### US-3.2 : Voir la traduction anglaise ✅ Backend · ⏳ UI +**En tant qu'** utilisateur +**Je veux** voir la traduction en anglais des paroles non-anglaises +**Afin de** comprendre le sens de ce que je chante + +**Critères d'acceptation :** +- [ ] Traduction EN affichée si VO ≠ anglais +- [ ] Traduction automatique via LibreTranslate (générée une fois, stockée en SQLite) +- [ ] Affichage aligné ligne par ligne avec la VO +- [ ] Option pour masquer/afficher la traduction + +> **Lié à l'issue** : [#50 — batch LibreTranslate](https://github.com/RebelliousSmile/lyremember/issues/50) + +--- + +### US-3.3 : Voir la translittération phonétique ✅ Backend · ⏳ UI +**En tant qu'** utilisateur +**Je veux** voir la translittération phonétique des paroles +**Afin de** savoir comment prononcer les mots, surtout pour les langues à idéogrammes + +**Critères d'acceptation :** +- [ ] Japonais → Romaji (pykakasi) +- [ ] Coréen → Romanisation (hangul-romanize) +- [ ] Français/Anglais → IPA (epitran) +- [ ] Phonétique générée une fois, stockée en SQLite (offline illimité) +- [ ] Affichage aligné avec les paroles originales +- [ ] Option pour masquer/afficher la phonétique + +--- + +### US-3.4 : Affichage combiné VO + Phonétique + Traduction ✅ Backend · ⏳ UI +**En tant qu'** utilisateur avancé +**Je veux** voir simultanément VO, phonétique et traduction +**Afin d'** avoir toutes les informations en un coup d'œil + +**Critères d'acceptation :** +- [ ] Vue 3 colonnes : VO | Phonétique | Traduction EN +- [ ] Chaque colonne peut être masquée/affichée indépendamment +- [ ] Interface claire et lisible, responsive + +--- + +## Epic 4 — Mode Défilement Karaoke + +### US-4.1 : Défilement automatique des paroles ⏳ UI à implémenter +**En tant qu'** utilisateur +**Je veux** que les paroles défilent automatiquement phrase par phrase +**Afin de** suivre le rythme de la chanson pendant que je chante + +**Critères d'acceptation :** +- [ ] Mode karaoke : une phrase à la fois en surbrillance +- [ ] Délai configurable entre chaque phrase (en secondes) +- [ ] Indicateur visuel de progression (numéro ligne / total) +- [ ] Avancement automatique jusqu'à la fin + +--- + +### US-4.2 : Contrôles de lecture ⏳ UI à implémenter +**En tant qu'** utilisateur +**Je veux** contrôler le défilement (pause, play, vitesse) +**Afin de** m'adapter à mon rythme d'apprentissage + +**Critères d'acceptation :** +- [ ] Pause / Play +- [ ] Navigation : ligne précédente / ligne suivante +- [ ] Réglage de vitesse (lent / normal / rapide) +- [ ] Quitter le mode depuis un bouton ou raccourci clavier + +--- + +## Epic 5 — Mode Vérification Orale + +### US-5.1 : Pratiquer en mode oral ❌ Non commencé +**En tant qu'** utilisateur +**Je veux** dire les paroles à voix haute et que l'application vérifie +**Afin de** m'entraîner à chanter correctement + +**Critères d'acceptation :** +- [ ] Activation du microphone (Web Speech API dans Tauri WebView) +- [ ] Affichage d'une phrase, l'utilisateur la prononce +- [ ] Vérification de la correspondance avec feedback immédiat (✓ / ✗) +- [ ] Affichage de ce que le système a capté + +> **Défi technique** : nécessite Web Speech API (disponible dans WebView Tauri), gestion du bruit de fond, tolérance aux accents. + +--- + +### US-5.2 : Réglages de sensibilité orale ❌ Non commencé +**En tant qu'** utilisateur +**Je veux** ajuster la sensibilité de la vérification orale +**Afin d'** avoir un niveau de difficulté adapté + +**Critères d'acceptation :** +- [ ] Niveaux : permissif (70% similarité) / moyen (80%) / strict (90%+) +- [ ] Feedback affichant le pourcentage de correspondance atteint + +--- + +## Epic 6 — Mode Phrases à Trous (style "N'Oubliez Pas Les Paroles") + +### US-6.1 : Pratiquer en mode trous ✅ Backend (sessions) · ⏳ UI +**En tant qu'** utilisateur +**Je veux** compléter la fin des phrases comme dans "N'Oubliez Pas Les Paroles" +**Afin de** tester ma mémoire de façon ludique + +**Critères d'acceptation :** +- [ ] Affichage de la phrase avec les derniers mots masqués (`___ ___ ___`) +- [ ] Saisie libre des mots manquants +- [ ] Vérification avec tolérance aux fautes de frappe mineures +- [ ] Nombre de mots cachés configurable (1 à 5) +- [ ] Feedback immédiat : ✓ Correct / ✗ avec la bonne réponse + +--- + +### US-6.2 : Difficulté progressive en mode trous ⏳ UI +**En tant qu'** utilisateur +**Je veux** que la difficulté augmente progressivement +**Afin d'** être challengé au fur et à mesure + +**Critères d'acceptation :** +- [ ] Débute avec 1 mot caché +- [ ] Augmente (2, 3…) après succès consécutifs +- [ ] Revient au niveau précédent en cas d'échec +- [ ] Score basé sur le niveau atteint + +--- + +## Epic 7 — Mode QCM (Propositions Multiples) + +### US-7.1 : Pratiquer en mode QCM ✅ Backend (sessions) · ⏳ UI +**En tant qu'** utilisateur +**Je veux** choisir la bonne phrase parmi plusieurs propositions +**Afin de** tester ma reconnaissance des paroles + +**Critères d'acceptation :** +- [ ] 4 propositions pour chaque ligne (A, B, C, D) +- [ ] Une seule réponse correcte +- [ ] Sélection au clic (ou raccourci clavier 1-4 / A-D) +- [ ] Feedback immédiat + explication +- [ ] Score et combo affiché + +--- + +### US-7.2 : Génération intelligente de fausses propositions ⏳ À implémenter +**En tant qu'** utilisateur +**Je veux** que les fausses propositions soient crédibles +**Afin que** le quiz soit réellement challengeant + +**Critères d'acceptation :** +- [ ] Distracteurs basés sur : mots phonétiquement similaires, autres lignes de la chanson, variantes grammaticales +- [ ] Pas de propositions évidemment fausses +- [ ] Difficulté ajustable + +--- + +## Epic 8 — Statistiques et Suivi de Progression + +### US-8.1 : Historique et statistiques par mode ✅ Backend (practice_sessions) · ⏳ UI +**En tant qu'** utilisateur +**Je veux** voir mes stats pour chaque mode de pratique +**Afin de** identifier où je dois m'améliorer + +**Critères d'acceptation :** +- [ ] Stats séparées : karaoke / trous / QCM / oral +- [ ] Taux de réussite par mode et par chanson +- [ ] Temps total pratiqué +- [ ] Chanson la plus pratiquée + +> **Lié à l'issue** : [#44 — stats graphiques + heatmap](https://github.com/RebelliousSmile/lyremember/issues/44) + +--- + +### US-8.2 : Recommandations personnalisées ❌ Non commencé +**En tant qu'** utilisateur +**Je veux** recevoir des suggestions de pratique +**Afin d'** optimiser mon apprentissage + +**Critères d'acceptation :** +- [ ] Suggestion d'une chanson + mode selon maîtrise actuelle +- [ ] Basé sur le temps depuis la dernière pratique +- [ ] Basé sur les difficultés identifiées (lines avec scores faibles) + +--- + +## Epic 9 — Fonctionnalités Complémentaires (depuis v1) + +### US-9.1 : Mode Flashcard ❌ Non commencé +**En tant qu'** utilisateur +**Je veux** un mode flashcard (début de ligne → compléter la suite) +**Afin de** tester ma mémoire de façon progressive + +**Critères d'acceptation :** +- [ ] Affichage du début de la ligne +- [ ] L'utilisateur tape ou révèle la suite +- [ ] Marquage « connu » / « à retravailler » +- [ ] Lignes marquées « à retravailler » apparaissent plus souvent + +--- + +### US-9.2 : Extraction de vocabulaire ❌ Non commencé +**En tant qu'** apprenant de langue +**Je veux** voir les mots-clés d'une chanson mis en valeur +**Afin de** enrichir mon vocabulaire + +**Critères d'acceptation :** +- [ ] Identification des mots importants / peu communs +- [ ] Liste de vocabulaire exportable depuis la vue chanson +- [ ] Pratique du vocabulaire séparément (mode flashcard mot) + +--- + +### US-9.3 : Export / Import du profil ❌ Non commencé +**En tant qu'** utilisateur +**Je veux** exporter et importer mon profil (chansons + progression) +**Afin de** sauvegarder ou migrer mes données + +**Critères d'acceptation :** +- [ ] Export JSON du profil complet (chansons + sessions) +- [ ] Import depuis un fichier JSON exporté +- [ ] Validation du format à l'import + +> **Lié à l'issue** : [#35 — export/import profil JSON](https://github.com/RebelliousSmile/lyremember/issues/35) + +--- + +## Priorités MVP + +### Must Have — P0 (~55 pts) +| Story | Description | Statut | +|-------|-------------|--------| +| US-1.1, 1.2 | Créer compte + Se connecter | ✅ Backend · ⏳ UI | +| US-2.1, 2.2 | Ajouter + Voir répertoire | ✅ Backend · ⏳ UI | +| US-3.1, 3.3 | VO + Phonétique (JP/KR min.) | ✅ Backend · ⏳ UI | +| US-4.1, 4.2 | Défilement karaoke basique | ⏳ UI | +| US-6.1 | Mode trous NOPLP | ✅ Backend · ⏳ UI | + +### Should Have — P1 (~25 pts) +| Story | Description | Statut | +|-------|-------------|--------| +| US-1.3 | Lien compte Genius | ✅ Backend · ⏳ UI | +| US-3.2, 3.4 | Traduction + vue 3 colonnes | ✅ Backend · ⏳ UI | +| US-7.1 | Mode QCM basique | ✅ Backend · ⏳ UI | +| US-5.1 | Vérification orale basique | ❌ Non commencé | + +### Could Have — P2 (~25 pts) +| Story | Description | Statut | +|-------|-------------|--------| +| US-5.2 | Réglages sensibilité orale | ❌ | +| US-6.2 | Difficulté progressive trous | ⏳ | +| US-7.2 | Génération intelligente QCM | ⏳ | +| US-8.1 | Stats détaillées par mode | ✅ Backend · ⏳ UI | +| US-8.2 | Recommandations | ❌ | +| US-9.1 | Flashcard | ❌ | +| US-9.2 | Vocabulaire | ❌ | +| US-9.3 | Export/Import profil | ❌ | + +--- + +## Couverture GitHub Issues + +### Issues existantes et stories associées + +| Issue | Titre | Stories liées | Type | +|-------|-------|---------------|------| +| [#50](https://github.com/RebelliousSmile/lyremember/issues/50) | Batch LibreTranslate | US-3.2 | Amélioration perf | +| [#44](https://github.com/RebelliousSmile/lyremember/issues/44) | Stats graphiques + heatmap | US-8.1 | Amélioration UX | +| [#43](https://github.com/RebelliousSmile/lyremember/issues/43) | Audit sécurité | — | Technique | +| [#41](https://github.com/RebelliousSmile/lyremember/issues/41) | Tests Pinia (stores) | — | Technique | +| [#39](https://github.com/RebelliousSmile/lyremember/issues/39) | Accessibilité (a11y) | — | Technique | +| [#35](https://github.com/RebelliousSmile/lyremember/issues/35) | Export/Import profil JSON | US-9.3 | Feature | +| [#34](https://github.com/RebelliousSmile/lyremember/issues/34) | CI release multi-OS | — | Infrastructure | +| [#33](https://github.com/RebelliousSmile/lyremember/issues/33) | Packaging desktop | — | Infrastructure | + +### ⚠️ Stories sans issue GitHub (à créer) + +Les stories suivantes du MVP et du P1 **n'ont pas d'issue correspondante** dans le dépôt : + +| Priorité | Story | Titre suggéré pour l'issue | +|----------|-------|---------------------------| +| P0 | US-1.1, 1.2 | `feat(auth): page login + register (frontend Vue)` | +| P0 | US-2.1, 2.2 | `feat(songs): liste répertoire + formulaire ajout chanson` | +| P0 | US-3.1, 3.3 | `feat(display): vue paroles VO + phonétique (3 colonnes)` | +| P0 | US-4.1, 4.2 | `feat(karaoke): mode défilement phrase par phrase` | +| P0 | US-6.1 | `feat(practice): mode trous style NOPLP` | +| P1 | US-3.2, 3.4 | `feat(display): affichage traduction + toggle 3 colonnes` | +| P1 | US-7.1 | `feat(practice): mode QCM (choix multiples)` | +| P1 | US-1.3 | `feat(genius): liaison compte Genius + import paroles` | +| P1 | US-5.1 | `feat(oral): mode vérification orale (Web Speech API)` | + +--- + +## Estimations (story points) + +| Epic | Stories | Points | +|------|---------|--------| +| Epic 1 — Comptes | US-1.1, 1.2, 1.3 | 13 | +| Epic 2 — Répertoire | US-2.1, 2.2, 2.3 | 8 | +| Epic 3 — Multi-langues | US-3.1, 3.2, 3.3, 3.4 | 21 | +| Epic 4 — Karaoke | US-4.1, 4.2 | 13 | +| Epic 5 — Oral | US-5.1, 5.2 | 21 | +| Epic 6 — Trous | US-6.1, 6.2 | 8 | +| Epic 7 — QCM | US-7.1, 7.2 | 13 | +| Epic 8 — Stats | US-8.1, 8.2 | 8 | +| Epic 9 — Complémentaire | US-9.1, 9.2, 9.3 | ~13 | +| **Total** | **22 stories** | **~118 pts** | + +--- + +*Dernière mise à jour : 2026-05-26 — Fusion USER_STORIES.md (v1 EN) + USER_STORIES_V2.md (v2 FR), mise à jour contexte Tauri UI.* diff --git a/aidd_docs/memory/external/VUE_TAURI_GUIDE.md b/aidd_docs/memory/external/VUE_TAURI_GUIDE.md new file mode 100644 index 0000000..f442219 --- /dev/null +++ b/aidd_docs/memory/external/VUE_TAURI_GUIDE.md @@ -0,0 +1,919 @@ +# Tauri + Vue.js - Guide Complet + +## ✅ Vue.js fonctionne PARFAITEMENT avec Tauri ! + +Tauri est **framework-agnostic** : vous pouvez utiliser n'importe quel framework frontend. + +--- + +## 🎯 Stack Recommandée : Vue + Tauri + +### Architecture + +``` +Frontend : Vue 3 + TypeScript + Vite +UI Library : Shadcn-vue (équivalent Vue de Shadcn) +Styling : Tailwind CSS +Icons : Lucide Vue (ou Heroicons) +State : Pinia (state management officiel Vue 3) +Router : Vue Router +Desktop : Tauri (Rust backend) +Mobile : Tauri Mobile (beta) ou PWA +Backend : Rust (Tauri commands) +BDD : SQLite (rusqlite) +``` + +--- + +## 🚀 Setup Tauri + Vue + +### Méthode 1 : Create Tauri App (Recommandé) + +```bash +# Créer nouveau projet +pnpm create tauri-app + +# Choisir dans l'assistant : +# ✓ Project name: lyremember +# ✓ Choose your package manager: npm +# ✓ Choose your UI template: Vue +# ✓ Choose your UI flavor: TypeScript +``` + +### Méthode 2 : Ajouter Tauri à projet Vue existant + +```bash +# Si vous avez déjà un projet Vue +npm create vue@latest + +# Puis ajouter Tauri +pnpm install --save-dev @tauri-apps/cli +npx tauri init +``` + +--- + +## 🎨 Shadcn-vue (UI Components pour Vue) + +**Shadcn-vue** = Port officiel de Shadcn/ui pour Vue 3 ! + +### Installation + +```bash +# 1. Setup Vue project avec Tauri +pnpm create tauri-app + +# 2. Ajouter Tailwind CSS +pnpm install -D tailwindcss postcss autoprefixer +npx tailwindcss init -p + +# 3. Configurer Tailwind +# tailwind.config.js +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} + +# 4. Ajouter Shadcn-vue +npx shadcn-vue@latest init + +# 5. Ajouter composants +npx shadcn-vue@latest add button +npx shadcn-vue@latest add card +npx shadcn-vue@latest add input +npx shadcn-vue@latest add dialog +npx shadcn-vue@latest add select +``` + +### Composants Disponibles + +Plus de 45 composants : +- Accordion, Alert, Avatar, Badge, Button +- Card, Checkbox, Combobox, Command, Dialog +- Dropdown Menu, Form, Input, Label, Popover +- Select, Sheet, Slider, Switch, Table +- Tabs, Toast, Tooltip, etc. + +--- + +## 📁 Structure Projet Vue + Tauri + +``` +lyremember/ +├── src/ # Frontend Vue +│ ├── main.ts # Point d'entrée Vue +│ ├── App.vue # Composant root +│ ├── router/ +│ │ └── index.ts # Vue Router +│ ├── stores/ # Pinia stores +│ │ ├── auth.ts +│ │ └── songs.ts +│ ├── views/ # Pages +│ │ ├── LoginView.vue +│ │ ├── RegisterView.vue +│ │ ├── DashboardView.vue +│ │ ├── SongsView.vue +│ │ ├── SongDetailView.vue +│ │ ├── PracticeView.vue +│ │ └── StatsView.vue +│ ├── components/ # Composants +│ │ ├── ui/ # Shadcn-vue components +│ │ │ ├── button/ +│ │ │ ├── card/ +│ │ │ └── ... +│ │ ├── SongCard.vue +│ │ ├── PhoneticDisplay.vue +│ │ ├── KaraokeMode.vue +│ │ ├── FillBlankMode.vue +│ │ └── VoiceMode.vue +│ ├── lib/ +│ │ └── tauri-api.ts # API Tauri (16 commands) +│ ├── composables/ # Vue composables +│ │ └── useSongs.ts +│ ├── types/ +│ │ └── index.ts +│ └── assets/ +│ +├── src-tauri/ # Backend Rust +│ ├── Cargo.toml +│ ├── tauri.conf.json +│ ├── src/ +│ │ ├── main.rs +│ │ ├── commands/ +│ │ ├── models/ +│ │ ├── db/ +│ │ └── services/ +│ └── icons/ +│ +├── package.json +├── vite.config.ts +├── tailwind.config.js +└── tsconfig.json +``` + +--- + +## 💻 Exemples de Code Vue + +### 1. Composant SongCard + +```vue + + + + +``` + +### 2. Vue avec Tauri API + +```typescript +// lib/tauri-api.ts (chemin réel dans le projet) +import { invoke } from '@tauri-apps/api' + +export interface Song { + id: string + title: string + artist: string + language: string + lyrics: string[] + phonetic_lyrics?: string[] +} + +export interface User { + id: string + username: string + email: string +} + +// Auth API +export const authApi = { + async login(username: string, password: string): Promise { + return await invoke('login', { username, password }) + }, + + async register(username: string, email: string, password: string): Promise { + return await invoke('register', { username, email, password }) + }, + + async logout(): Promise { + return await invoke('logout') + } +} + +// Songs API +export const songsApi = { + async getSongs(userId: string): Promise { + return await invoke('get_songs', { userId }) + }, + + async getSong(songId: string): Promise { + return await invoke('get_song', { songId }) + }, + + async createSong(data: { + userId: string + title: string + artist: string + language: string + lyrics: string[] + }): Promise { + return await invoke('create_song', data) + }, + + async generatePhonetic(songId: string, language: string): Promise { + return await invoke('generate_phonetic', { songId, language }) + } +} + +// Genius API +export const geniusApi = { + async search(query: string): Promise { + return await invoke('genius_search', { query }) + }, + + async import(songId: string, language: string): Promise { + return await invoke('genius_import', { songId, language }) + } +} +``` + +### 3. Pinia Store (State Management) + +```typescript +// stores/auth.ts +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { authApi, type User } from '@/lib/tauri-api' + +export const useAuthStore = defineStore('auth', () => { + const user = ref(null) + const isAuthenticated = computed(() => user.value !== null) + + async function login(username: string, password: string) { + try { + user.value = await authApi.login(username, password) + return true + } catch (error) { + console.error('Login failed:', error) + return false + } + } + + async function register(username: string, email: string, password: string) { + try { + user.value = await authApi.register(username, email, password) + return true + } catch (error) { + console.error('Registration failed:', error) + return false + } + } + + async function logout() { + await authApi.logout() + user.value = null + } + + return { + user, + isAuthenticated, + login, + register, + logout + } +}) +``` + +```typescript +// stores/songs.ts +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { songsApi, type Song } from '@/lib/tauri-api' +import { useAuthStore } from './auth' + +export const useSongsStore = defineStore('songs', () => { + const songs = ref([]) + const currentSong = ref(null) + const loading = ref(false) + + async function fetchSongs() { + const authStore = useAuthStore() + if (!authStore.user) return + + loading.value = true + try { + songs.value = await songsApi.getSongs(authStore.user.id) + } finally { + loading.value = false + } + } + + async function fetchSong(songId: string) { + loading.value = true + try { + currentSong.value = await songsApi.getSong(songId) + } finally { + loading.value = false + } + } + + async function createSong(data: { + title: string + artist: string + language: string + lyrics: string[] + }) { + const authStore = useAuthStore() + if (!authStore.user) return null + + const newSong = await songsApi.createSong({ + userId: authStore.user.id, + ...data + }) + + songs.value.push(newSong) + return newSong + } + + return { + songs, + currentSong, + loading, + fetchSongs, + fetchSong, + createSong + } +}) +``` + +### 4. Vue Composable + +```typescript +// composables/useSongs.ts +import { ref, computed } from 'vue' +import { useSongsStore } from '@/stores/songs' +import { storeToRefs } from 'pinia' + +export function useSongs() { + const songsStore = useSongsStore() + const { songs, loading } = storeToRefs(songsStore) + + const songsByLanguage = computed(() => { + const grouped: Record = {} + + songs.value.forEach(song => { + if (!grouped[song.language]) { + grouped[song.language] = [] + } + grouped[song.language].push(song) + }) + + return grouped + }) + + const searchSongs = (query: string) => { + const lowerQuery = query.toLowerCase() + return songs.value.filter(song => + song.title.toLowerCase().includes(lowerQuery) || + song.artist.toLowerCase().includes(lowerQuery) + ) + } + + return { + songs, + loading, + songsByLanguage, + searchSongs, + fetchSongs: songsStore.fetchSongs, + createSong: songsStore.createSong + } +} +``` + +### 5. Page Vue complète + +```vue + + + + +``` + +### 6. Mode Karaoke + +```vue + + + + + + +``` + +--- + +## 🎨 Shadcn-vue vs Shadcn-ui (React) + +| Feature | Shadcn-vue | Shadcn-ui | +|---------|------------|-----------| +| Framework | Vue 3 | React | +| Composants | 45+ | 50+ | +| Tailwind | ✅ | ✅ | +| TypeScript | ✅ | ✅ | +| Dark Mode | ✅ | ✅ | +| Accessible | ✅ | ✅ | +| Code ownership | ✅ | ✅ | + +**Identique en qualité !** Juste adapté pour Vue. + +--- + +## 🌐 i18n avec Vue + +```bash +npm install vue-i18n@9 +``` + +```typescript +// i18n.ts +import { createI18n } from 'vue-i18n' + +const messages = { + en: { + practice: 'Practice', + songs: 'Songs', + addSong: 'Add Song', + }, + fr: { + practice: 'Pratiquer', + songs: 'Chansons', + addSong: 'Ajouter Chanson', + }, + ko: { + practice: '연습', + songs: '노래', + addSong: '노래 추가', + }, + ja: { + practice: '練習', + songs: '曲', + addSong: '曲を追加', + } +} + +export const i18n = createI18n({ + legacy: false, + locale: 'fr', + fallbackLocale: 'en', + messages, +}) +``` + +```vue + + + +``` + +--- + +## ✅ Avantages Vue + Tauri + +### Vue 3 Composition API +- ✅ **Plus simple** que React hooks pour certains +- ✅ **Réactivité** automatique (ref, reactive) +- ✅ **Syntax concise** : `v-model`, `v-if`, `v-for` +- ✅ **Performance** excellente +- ✅ **TypeScript** support natif + +### Écosystème Vue +- ✅ **Pinia** : State management simple et typé +- ✅ **Vue Router** : Routing officiel +- ✅ **Vite** : Build tool ultra rapide (même créateur que Vue) +- ✅ **Shadcn-vue** : Composants modernes +- ✅ **VueUse** : Utilities composables + +--- + +## 🚀 État du Projet + +> ⚠️ Le projet est **déjà créé**. Les étapes 1-4 sont réalisées. +> `shadcn-vue` et `vue-i18n` ne sont pas encore installés. + +```bash +# ✅ Déjà fait : pnpm create tauri-app (lyremember-app/) +# ✅ Déjà fait : Tailwind CSS configuré +# ✅ Déjà fait : Pinia, Vue Router, lucide-vue-next installés +# ⏳ À faire : pnpm install shadcn-vue vue-i18n + +# Lancer en dev +pnpm tauri dev +``` + +--- + +## 📊 Vue vs React avec Tauri + +| Critère | Vue + Tauri | React + Tauri | +|---------|-------------|---------------| +| **Syntaxe** | ⭐⭐⭐ Template-based | ⭐⭐ JSX | +| **Courbe apprentissage** | ⭐⭐⭐ Plus facile | ⭐⭐ Moyen | +| **Performance** | ⭐⭐⭐ Excellent | ⭐⭐⭐ Excellent | +| **Réactivité** | ⭐⭐⭐ Automatique | ⭐⭐ Manuel | +| **TypeScript** | ⭐⭐⭐ Excellent | ⭐⭐⭐ Excellent | +| **Écosystème** | ⭐⭐ Bon | ⭐⭐⭐ Énorme | +| **Tauri Support** | ⭐⭐⭐ Parfait | ⭐⭐⭐ Parfait | +| **UI Libs** | ⭐⭐ Shadcn-vue | ⭐⭐⭐ Shadcn-ui | + +--- + +## ✅ Verdict Final + +### **Pour vous : Tauri + Vue 3 + Shadcn-vue** 🎯 + +**Stack complète :** +``` +Frontend : Vue 3 + TypeScript + Vite +UI : Shadcn-vue + Tailwind CSS +Icons : Lucide Vue +State : Pinia +Router : Vue Router +i18n : Vue I18n +Desktop : Tauri (Rust) +Mobile : PWA ou Tauri Mobile (beta) +Backend : Rust (Tauri commands) +BDD : SQLite (rusqlite) +``` + +**Pourquoi c'est parfait pour vous :** +- ✅ **Tauri** : Performance native, léger +- ✅ **Vue** : Vous connaissez déjà ! +- ✅ **Shadcn-vue** : Composants modernes comme Shadcn-ui +- ✅ **TypeScript** : Type safety +- ✅ **Gratuit** : Tout open-source +- ✅ **Desktop + Mobile** : Un codebase + +**Prêt à commencer ?** 🚀 + +Je peux vous aider à : +1. Setup le projet Tauri + Vue +2. Configurer Shadcn-vue + Tailwind +3. Créer les premiers composants +4. Implémenter le backend Rust + +**Dites-moi et on y va !** 😊 diff --git a/docs/usage_guide.md b/aidd_docs/memory/external/usage_guide.md similarity index 100% rename from docs/usage_guide.md rename to aidd_docs/memory/external/usage_guide.md diff --git a/wireframes.jsx b/aidd_docs/memory/external/wireframes.jsx similarity index 100% rename from wireframes.jsx rename to aidd_docs/memory/external/wireframes.jsx diff --git a/wireframes2.jsx b/aidd_docs/memory/external/wireframes2.jsx similarity index 100% rename from wireframes2.jsx rename to aidd_docs/memory/external/wireframes2.jsx diff --git a/aidd_docs/memory/frontend/browsing.md b/aidd_docs/memory/frontend/browsing.md new file mode 100644 index 0000000..06105da --- /dev/null +++ b/aidd_docs/memory/frontend/browsing.md @@ -0,0 +1,13 @@ +--- +name: browsing +description: Template to explain AI how to browse our project. +scope: frontend +--- + +# Browser Setup + +Application Tauri desktop : il n'y a pas d'URL navigable. Le frontend tourne dans une WebView native lancée par `pnpm --dir lyremember-app tauri dev`. Le serveur Vite local sert l'UI sur `http://localhost:1420` (config Vite par défaut Tauri) pour debug navigateur. + +- **Browsing Tool**: WebView Tauri (native OS) — debug possible via `http://localhost:1420` en mode `dev` +- **Starting URL**: `/` (route racine `App.vue`) +- **Authentication**: login local (bcrypt + JWT) via commands Tauri diff --git a/aidd_docs/memory/frontend/design.md b/aidd_docs/memory/frontend/design.md new file mode 100644 index 0000000..563def5 --- /dev/null +++ b/aidd_docs/memory/frontend/design.md @@ -0,0 +1,29 @@ +--- +name: design +description: Design system and UI guidelines +scope: frontend +--- + +# DESIGN.md + +## Design Implementation + +- **Design System Approach**: Tailwind CSS + composants Vue maison +- **Styling Method**: classes utilitaires Tailwind appliquées directement dans les ` diff --git a/lyremember-app/src/views/StatsView.vue b/lyremember-app/src/views/StatsView.vue index 0ab8769..09304fc 100644 --- a/lyremember-app/src/views/StatsView.vue +++ b/lyremember-app/src/views/StatsView.vue @@ -1,15 +1,25 @@