commit c6f541ed94a63e23b615020169591dd535631fb3 Author: gkxk Date: Tue Oct 20 08:28:20 2026 +0800 feat: initial implementation of Focus RSVP speed reader - Pure frontend SPA (Vite + React 19 + TypeScript + Tailwind CSS v4) - Spritz ORP engine with absolute center crosshair gaze fixation - Multilingual hybrid segmentation via Intl.Segmenter with punctuation attachment - Adaptive breathing rhythm delay algorithm for natural comprehension - IndexedDB local bookshelf, progress persistence, and client-side EPUB/TXT/MD parsing - Curated sci-fi, philosophy, and classic speech samples - Dark/OLED/Sepia/Slate/Nord themes and rich keyboard controls Co-Authored-By: Claude Code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..2d9377b --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + + + Focus — 极速 RSVP 闪屏专注阅读器 + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..2808700 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "focus", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "jszip": "^3.10.2", + "lucide-react": "^1.59.0", + "react": "^19.3.1", + "react-dom": "^19.3.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@types/jszip": "^3.4.1", + "@types/node": "^24.19.2", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", + "oxlint": "^1.88.0", + "tailwindcss": "^4.3.3", + "typescript": "~7.0.2", + "vite": "^8.3.4" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..b317939 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1416 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + jszip: + specifier: ^3.10.2 + version: 3.10.2 + lucide-react: + specifier: ^1.59.0 + version: 1.59.0(@types/react@19.3.0)(react@19.3.1) + react: + specifier: ^19.3.1 + version: 19.3.1 + react-dom: + specifier: ^19.3.1 + version: 19.3.1(react@19.3.1) + devDependencies: + '@tailwindcss/vite': + specifier: ^4.3.3 + version: 4.3.3(vite@8.3.4(@types/node@24.19.2)(jiti@2.7.0)) + '@types/jszip': + specifier: ^3.4.1 + version: 3.4.1 + '@types/node': + specifier: ^24.19.2 + version: 24.19.2 + '@types/react': + specifier: ^19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.3.0 + version: 19.3.0(@types/react@19.3.0) + '@vitejs/plugin-react': + specifier: ^6.1.2 + version: 6.1.2(vite@8.3.4(@types/node@24.19.2)(jiti@2.7.0)) + oxlint: + specifier: ^1.88.0 + version: 1.89.0 + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 + typescript: + specifier: ~7.0.2 + version: 7.0.2 + vite: + specifier: ^8.3.4 + version: 8.3.4(@types/node@24.19.2)(jiti@2.7.0) + +packages: + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@oxc-project/types@0.154.0': + resolution: {integrity: sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==} + + '@oxlint/binding-android-arm-eabi@1.89.0': + resolution: {integrity: sha512-Pflpba5F3gRAQ12sWglRxvWTHEdAusNDvmi4Y5N1T5SY1AGkM7AixOnET8qkrnLJ/F32qAlNokO6eCknT8tmnw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.89.0': + resolution: {integrity: sha512-dYmyD1x7LaPlb79vZWUcpTShk8dI9z1I+IubtqrBxWNJaDhQdPHwcsvg7q5StTU4xWyd9AwPfLaTYFZY8NZgtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.89.0': + resolution: {integrity: sha512-Z/oxOPiIqiS7fItRdxaaOEC+6GD3OpGi0IxoR0dMefzQHolsKLzScw2DNcu/5gUPok3tv+G9mslvYB5bo3i0qg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.89.0': + resolution: {integrity: sha512-eAjTkHSjAxrdCPJxO6y5sPQL5pYLKb2kvxZz9UU7tXnOZk9I/HLtkvx9S7GHY883k+AmL/eC7mqFt+w7WCwk0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.89.0': + resolution: {integrity: sha512-0lNCA9Tl1n1FKpqy2/9ckp0kIuSrtJ/l9bMRgaEhOtM7TsOHshFuwOcvW8oA9teXLMOJT61LjEKWiaHDFGxF/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.89.0': + resolution: {integrity: sha512-OTFEvi9W+JczaZD1Caj17SGCoW8H7TE+fvx0D7ATEWa1Lohn91UMxFQ6r145SiGy/yCxeIwptMys/RAM7gSVSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.89.0': + resolution: {integrity: sha512-fRxgNHvBLswj6MrhW2uBq5UDhPRQlCbHeq/Nf8yuG9zOxhDsXqX/mi2PX1axgsQxMm88el7HLiJAzglAnflYxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.89.0': + resolution: {integrity: sha512-v9ENzHvwrQoYSWTtviH1pOYc7R9UovDxVofnck50cNmPihRRTcrg5X0UlRbLOZ7IVtxPOlh2hb7kDO+FpE8SfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-arm64-musl@1.89.0': + resolution: {integrity: sha512-vfffe8mQ484AzVadlUOFpgL+JDwhvs9hrzxTgQP1FVfi4ZEUUYeffXlzC9dFV7Dgnu5s/KrNKvfmG6HxBd/ISQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-ppc64-gnu@1.89.0': + resolution: {integrity: sha512-Buonlp4hVS7RGNP5M7bPpGNQrenyzA2C64JcO/oz9C1c6dGOXJ7VEAnaiAMY7voou0JYyCJdvDVlFQc5DZX3DQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxlint/binding-linux-riscv64-gnu@1.89.0': + resolution: {integrity: sha512-1eVwJJrJ3lYIPT+cm7BcBBZZXY6IQZP/oyonaruTor/MrVyWOpvnwt1oD5CLdsHg5aVBIxXodQSvt+1gK6ck5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-riscv64-musl@1.89.0': + resolution: {integrity: sha512-fRQQsKUGShdEL5Fl9o9uhj3grkZbeXKLv0icQ5tspUCNYSDy8wzzEcP1WC2bKdTQ0rSszbKlO3iiHORxV22DLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-s390x-gnu@1.89.0': + resolution: {integrity: sha512-dEtYjIy6+fHcjKklsmtmMkZRhwHr2UbzjmpvuPtQYSeN9awQ2hx5JLms5Wvri+8P/NZtD8v5hfgHALUql+/Ycg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxlint/binding-linux-x64-gnu@1.89.0': + resolution: {integrity: sha512-hqjwIj5KOYM8Wjef/p7IruNi1iByLhSzbMBk62y22P/E8q7Jj0Pp+DeUTKTRYyZYCDtU1wEGlnWOnttHbyNIww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-linux-x64-musl@1.89.0': + resolution: {integrity: sha512-iAQN3e6o9UgUCcdGwqRx/jYcKbG3HK7ur3rH6lTqVcQeNGZ85AeWIg6cgn/edwFQ0ofEZZItMwkzyCVTahnvEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-openharmony-arm64@1.89.0': + resolution: {integrity: sha512-MNxjEByz3N/7Q4qFQuPWvc14TpN9LoD3pECEDsJTZbP/wyB7U+hduUXEl7QAbbgVezFwsb9sWtNUO6XVBndOhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.89.0': + resolution: {integrity: sha512-xxe+i9wfuFso/VnoL4mfj0UlzE+cfCFiUB06vkXCMyQdgVVmItF5FfAF4tATAktSU6uHruWR+FjqvAT9cHZZzg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.89.0': + resolution: {integrity: sha512-saFy07cO94E664glwgLmKLI40HgTL1PY86y4T6+ud4niX4H0Kdqjcmqmjps7mQNdBytV5guGbXDdyQtRfeLbLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.89.0': + resolution: {integrity: sha512-hLFJ/o0f/dTJh/EW0vmcbREYSQO9tYgfiAAlsOkiLFiJuRRWvYtFELUXN5o2ijpSo/tlO1n+YlWN58tZwPG18A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/binding-android-arm-eabi@1.2.14': + resolution: {integrity: sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.14': + resolution: {integrity: sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.14': + resolution: {integrity: sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.14': + resolution: {integrity: sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.14': + resolution: {integrity: sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.14': + resolution: {integrity: sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.14': + resolution: {integrity: sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.2.14': + resolution: {integrity: sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.2.14': + resolution: {integrity: sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.2.14': + resolution: {integrity: sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.2.14': + resolution: {integrity: sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.2.14': + resolution: {integrity: sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.2.14': + resolution: {integrity: sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.14': + resolution: {integrity: sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.14': + resolution: {integrity: sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@types/jszip@3.4.1': + resolution: {integrity: sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==} + deprecated: This is a stub types definition. jszip provides its own type definitions, so you do not need this installed. + + '@types/node@24.19.2': + resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@vitejs/plugin-react@6.1.2': + resolution: {integrity: sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.152.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + enhanced-resolve@5.26.0: + resolution: {integrity: sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==} + engines: {node: '>=10.13.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jszip@3.10.2: + resolution: {integrity: sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lucide-react@1.59.0: + resolution: {integrity: sha512-5qgMyyCPU6OvnKpLS2t693+ZtiqA8oaSweWoUows0BLFKFf6V75m4+CptpmohZHpD5IuOVIBXHBBiv2vpLJmtw==} + peerDependencies: + '@types/react': '*' + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + oxlint@1.89.0: + resolution: {integrity: sha512-a25hkfo61okGPvJz432fRIPoxlKtNnOVy9w4rzrmGgNm4iVTPwpSQlmdqNWnp6p9QyyPVfAZRgnf5XvSBZFk/A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2003' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.31: + resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==} + engines: {node: ^10 || ^12 || >=14} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} + peerDependencies: + react: ^19.3.1 + + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + rolldown@1.2.14: + resolution: {integrity: sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@8.3.4: + resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + +snapshots: + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@oxc-project/types@0.154.0': {} + + '@oxlint/binding-android-arm-eabi@1.89.0': + optional: true + + '@oxlint/binding-android-arm64@1.89.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.89.0': + optional: true + + '@oxlint/binding-darwin-x64@1.89.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.89.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.89.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.89.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.89.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.89.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.89.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.89.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.89.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.89.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.89.0': + optional: true + + '@rolldown/binding-android-arm-eabi@1.2.14': + optional: true + + '@rolldown/binding-android-arm64@1.2.14': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.14': + optional: true + + '@rolldown/binding-darwin-x64@1.2.14': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.14': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.14': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.14': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.14': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.14': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.14': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.14': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.26.0 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.2 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.3.4(@types/node@24.19.2)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.3.4(@types/node@24.19.2)(jiti@2.7.0) + + '@types/jszip@3.4.1': + dependencies: + jszip: 3.10.2 + + '@types/node@24.19.2': + dependencies: + undici-types: 7.24.6 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@24.19.2)(jiti@2.7.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.4(@types/node@24.19.2)(jiti@2.7.0) + + core-util-is@1.0.3: {} + + csstype@3.2.3: {} + + detect-libc@2.1.2: {} + + enhanced-resolve@5.26.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + graceful-fs@4.2.11: {} + + immediate@3.0.6: {} + + inherits@2.0.4: {} + + isarray@1.0.0: {} + + jiti@2.7.0: {} + + jszip@3.10.2: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lucide-react@1.59.0(@types/react@19.3.0)(react@19.3.1): + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + nanoid@3.3.20: {} + + oxlint@1.89.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.89.0 + '@oxlint/binding-android-arm64': 1.89.0 + '@oxlint/binding-darwin-arm64': 1.89.0 + '@oxlint/binding-darwin-x64': 1.89.0 + '@oxlint/binding-freebsd-x64': 1.89.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.89.0 + '@oxlint/binding-linux-arm-musleabihf': 1.89.0 + '@oxlint/binding-linux-arm64-gnu': 1.89.0 + '@oxlint/binding-linux-arm64-musl': 1.89.0 + '@oxlint/binding-linux-ppc64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-musl': 1.89.0 + '@oxlint/binding-linux-s390x-gnu': 1.89.0 + '@oxlint/binding-linux-x64-gnu': 1.89.0 + '@oxlint/binding-linux-x64-musl': 1.89.0 + '@oxlint/binding-openharmony-arm64': 1.89.0 + '@oxlint/binding-win32-arm64-msvc': 1.89.0 + '@oxlint/binding-win32-ia32-msvc': 1.89.0 + '@oxlint/binding-win32-x64-msvc': 1.89.0 + + pako@1.0.11: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.31: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + process-nextick-args@2.0.1: {} + + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + + react@19.3.1: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + rolldown@1.2.14: + dependencies: + '@oxc-project/types': 0.154.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.14 + '@rolldown/binding-android-arm64': 1.2.14 + '@rolldown/binding-darwin-arm64': 1.2.14 + '@rolldown/binding-darwin-x64': 1.2.14 + '@rolldown/binding-freebsd-x64': 1.2.14 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.14 + '@rolldown/binding-linux-arm64-gnu': 1.2.14 + '@rolldown/binding-linux-arm64-musl': 1.2.14 + '@rolldown/binding-linux-ppc64-gnu': 1.2.14 + '@rolldown/binding-linux-s390x-gnu': 1.2.14 + '@rolldown/binding-linux-x64-gnu': 1.2.14 + '@rolldown/binding-linux-x64-musl': 1.2.14 + '@rolldown/binding-openharmony-arm64': 1.2.14 + '@rolldown/binding-win32-arm64-msvc': 1.2.14 + '@rolldown/binding-win32-x64-msvc': 1.2.14 + + safe-buffer@5.1.2: {} + + scheduler@0.28.0: {} + + setimmediate@1.0.5: {} + + source-map-js@1.2.2: {} + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + + undici-types@7.24.6: {} + + util-deprecate@1.0.2: {} + + vite@8.3.4(@types/node@24.19.2)(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.31 + rolldown: 1.2.14 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.19.2 + fsevents: 2.3.3 + jiti: 2.7.0 diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..b3a9f79 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..c5924ee --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,399 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ChapterDrawer } from './components/ChapterDrawer'; +import { Controls } from './components/Controls'; +import { FocusDisplay } from './components/FocusDisplay'; +import { Header } from './components/Header'; +import { LibraryModal } from './components/LibraryModal'; +import { PasteModal } from './components/PasteModal'; +import { SettingsModal } from './components/SettingsModal'; +import { ShortcutsModal } from './components/ShortcutsModal'; +import { + deleteBook, + getActiveBookId, + getAllBooksMeta, + getBook, + loadSettings, + saveBook, + saveSettings, + setActiveBookId, +} from './db'; +import { getBuiltinSampleBooks } from './db/samples'; +import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; +import { useReaderEngine } from './hooks/useReaderEngine'; +import type { Book, BookMeta, Chapter, ReaderSettings, ThemeId } from './types'; + +const THEME_ORDER: ThemeId[] = ['oled', 'dark', 'slate', 'sepia', 'nord', 'paper']; + +export default function App() { + const [settings, setSettings] = useState(() => loadSettings()); + const [booksMeta, setBooksMeta] = useState([]); + const [activeBook, setActiveBook] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + // Modals + const [isLibraryOpen, setIsLibraryOpen] = useState(false); + const [isPasteOpen, setIsPasteOpen] = useState(false); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [isShortcutsOpen, setIsShortcutsOpen] = useState(false); + const [isChaptersOpen, setIsChaptersOpen] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); + + // HUD inactivity fading + const [isUserActive, setIsUserActive] = useState(true); + const inactivityTimerRef = useRef(null); + + // Reader engine + const { + currentIndex, + currentToken, + previousToken, + nextToken, + isPlaying, + play, + pause, + togglePlay, + jumpTo, + jumpBy, + jumpToPreviousSentence, + jumpToNextSentence, + jumpToChapter, + restart, + totalTokens, + } = useReaderEngine({ + book: activeBook, + settings, + onBookProgressUpdate: (bookId, progress) => { + setBooksMeta((prev) => + prev.map((b) => (b.id === bookId ? { ...b, currentProgress: progress } : b)) + ); + }, + }); + + // Track fullscreen changes + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement); + }; + document.addEventListener('fullscreenchange', handleFullscreenChange); + return () => + document.removeEventListener('fullscreenchange', handleFullscreenChange); + }, []); + + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen().catch((err) => { + console.warn('Fullscreen request failed:', err); + }); + } else { + document.exitFullscreen().catch((err) => { + console.warn('Exit fullscreen failed:', err); + }); + } + }; + + // Initial load of books and defaults + useEffect(() => { + async function init() { + try { + let metaList = await getAllBooksMeta(); + + if (metaList.length === 0) { + // First visit: inject curated built-in samples + const samples = getBuiltinSampleBooks(); + for (const s of samples) { + await saveBook(s); + } + metaList = await getAllBooksMeta(); + } + + setBooksMeta(metaList); + + // Load active book + const savedActiveId = getActiveBookId(); + const targetId = savedActiveId || metaList[0]?.id; + + if (targetId) { + const loadedBook = await getBook(targetId); + if (loadedBook) { + setActiveBook(loadedBook); + setActiveBookId(loadedBook.id); + } + } + } catch (err) { + console.error('Failed to initialize reader database:', err); + } finally { + setIsLoading(false); + } + } + + init(); + }, []); + + // Update settings handler + const handleUpdateSettings = (newSettings: Partial) => { + setSettings((prev) => { + const updated = { ...prev, ...newSettings }; + saveSettings(updated); + return updated; + }); + }; + + // Theme cycle + const handleCycleTheme = () => { + const currentIdx = THEME_ORDER.indexOf(settings.theme); + const nextIdx = (currentIdx + 1) % THEME_ORDER.length; + handleUpdateSettings({ theme: THEME_ORDER[nextIdx] }); + }; + + // Book selection + const handleSelectBook = async (bookId: string) => { + const book = await getBook(bookId); + if (book) { + setActiveBook(book); + setActiveBookId(book.id); + } + }; + + // Save new book + const handleSaveNewBook = async (newBook: Book) => { + await saveBook(newBook); + const updatedMeta = await getAllBooksMeta(); + setBooksMeta(updatedMeta); + }; + + // Delete book + const handleDeleteBook = async (bookId: string) => { + await deleteBook(bookId); + const updatedMeta = await getAllBooksMeta(); + setBooksMeta(updatedMeta); + + if (activeBook?.id === bookId) { + if (updatedMeta.length > 0) { + handleSelectBook(updatedMeta[0].id); + } else { + setActiveBook(null); + } + } + }; + + // Calculate current chapter based on currentIndex + const currentChapter = useMemo(() => { + if (!activeBook || !activeBook.chapters || activeBook.chapters.length === 0) { + return undefined; + } + const chapters = activeBook.chapters; + for (let i = chapters.length - 1; i >= 0; i--) { + if (currentIndex >= chapters[i].startIndex) { + return chapters[i]; + } + } + return chapters[0]; + }, [activeBook, currentIndex]); + + // Jump to next/prev chapter + const handlePrevChapter = () => { + if (!activeBook || !activeBook.chapters) return; + const chapters = activeBook.chapters; + const currentIdx = currentChapter + ? chapters.findIndex((c) => c.id === currentChapter.id) + : 0; + if (currentIdx > 0) { + jumpToChapter(chapters[currentIdx - 1]); + } else { + jumpTo(0); + } + }; + + const handleNextChapter = () => { + if (!activeBook || !activeBook.chapters) return; + const chapters = activeBook.chapters; + const currentIdx = currentChapter + ? chapters.findIndex((c) => c.id === currentChapter.id) + : 0; + if (currentIdx < chapters.length - 1) { + jumpToChapter(chapters[currentIdx + 1]); + } + }; + + // Mouse activity detection for HUD fading + const resetInactivity = useCallback(() => { + setIsUserActive(true); + if (inactivityTimerRef.current) { + window.clearTimeout(inactivityTimerRef.current); + } + if (isPlaying && settings.autoHideControls) { + inactivityTimerRef.current = window.setTimeout(() => { + setIsUserActive(false); + }, 2200); + } + }, [isPlaying, settings.autoHideControls]); + + useEffect(() => { + if (!isPlaying || !settings.autoHideControls) { + if (inactivityTimerRef.current) { + window.clearTimeout(inactivityTimerRef.current); + } + return; + } + const timer = window.setTimeout(() => { + setIsUserActive(false); + }, 2200); + return () => window.clearTimeout(timer); + }, [isPlaying, settings.autoHideControls]); + + // Keyboard shortcuts + const anyModalOpen = + isLibraryOpen || + isPasteOpen || + isSettingsOpen || + isShortcutsOpen || + isChaptersOpen; + + useKeyboardShortcuts({ + togglePlay, + jumpBy, + jumpToPreviousSentence, + jumpToNextSentence, + changeWpm: (delta) => + handleUpdateSettings({ + wpm: Math.max(100, Math.min(1200, settings.wpm + delta)), + }), + toggleFullscreen, + restart, + prevChapter: handlePrevChapter, + nextChapter: handleNextChapter, + onEscape: () => { + if (anyModalOpen) { + setIsLibraryOpen(false); + setIsPasteOpen(false); + setIsSettingsOpen(false); + setIsShortcutsOpen(false); + setIsChaptersOpen(false); + } else { + pause(); + } + }, + isModalOpen: anyModalOpen, + }); + + const themeClass = `theme-${settings.theme}`; + const isHudDimmed = isPlaying && !isUserActive && settings.autoHideControls; + + return ( +
+ {/* Top Header HUD */} +
+
setIsLibraryOpen(true)} + onOpenPaste={() => setIsPasteOpen(true)} + onOpenSettings={() => setIsSettingsOpen(true)} + onOpenShortcuts={() => setIsShortcutsOpen(true)} + onOpenChapters={() => setIsChaptersOpen(true)} + onToggleFullscreen={toggleFullscreen} + onCycleTheme={handleCycleTheme} + /> +
+ + {/* Main RSVP Focus Arena */} +
+ {isLoading ? ( +
+
+ 正在加载 Focus 阅读器... +
+ ) : ( + + )} +
+ + {/* Bottom Controls HUD */} +
+ handleUpdateSettings({ wpm: newWpm })} + /> +
+ + {/* Modals & Drawers */} + setIsLibraryOpen(false)} + onSelectBook={handleSelectBook} + onSaveNewBook={handleSaveNewBook} + onDeleteBook={handleDeleteBook} + /> + + setIsPasteOpen(false)} + onSaveAndPlay={async (newBook) => { + await handleSaveNewBook(newBook); + setActiveBook(newBook); + setActiveBookId(newBook.id); + play(); + }} + /> + + setIsSettingsOpen(false)} + onUpdateSettings={handleUpdateSettings} + /> + + setIsShortcutsOpen(false)} + /> + + setIsChaptersOpen(false)} + onSelectChapter={(chapter: Chapter) => jumpToChapter(chapter)} + /> +
+ ); +} diff --git a/src/components/ChapterDrawer.tsx b/src/components/ChapterDrawer.tsx new file mode 100644 index 0000000..fa9196b --- /dev/null +++ b/src/components/ChapterDrawer.tsx @@ -0,0 +1,78 @@ +import { Check, List, X } from 'lucide-react'; +import React from 'react'; +import type { Chapter } from '../types'; + +interface ChapterDrawerProps { + isOpen: boolean; + chapters: Chapter[]; + currentChapterId?: string; + onClose: () => void; + onSelectChapter: (chapter: Chapter) => void; +} + +export const ChapterDrawer: React.FC = ({ + isOpen, + chapters, + currentChapterId, + onClose, + onSelectChapter, +}) => { + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+
+ +

+ 章节目录 ({chapters.length}) +

+
+ +
+ + {/* Chapter List */} +
+ {chapters.length === 0 ? ( +
+ 无多章节信息 +
+ ) : ( + chapters.map((chap, idx) => { + const isActive = chap.id === currentChapterId; + return ( + + ); + }) + )} +
+
+
+ ); +}; diff --git a/src/components/Controls.tsx b/src/components/Controls.tsx new file mode 100644 index 0000000..fbd0054 --- /dev/null +++ b/src/components/Controls.tsx @@ -0,0 +1,210 @@ +import { + ChevronLeft, + ChevronRight, + ChevronsLeft, + ChevronsRight, + Minus, + Pause, + Play, + Plus, + RotateCcw, +} from 'lucide-react'; +import React from 'react'; +import { estimateRemainingSeconds, formatDuration } from '../core/timing'; +import type { Chapter, ReaderSettings, Token } from '../types'; + +interface ControlsProps { + currentIndex: number; + totalTokens: number; + tokens: Token[]; + chapters: Chapter[]; + isPlaying: boolean; + settings: ReaderSettings; + onTogglePlay: () => void; + onJumpTo: (index: number) => void; + onJumpBy: (delta: number) => void; + onJumpToPreviousSentence: () => void; + onJumpToNextSentence: () => void; + onRestart: () => void; + onChangeWpm: (wpm: number) => void; +} + +const WPM_PRESETS = [200, 300, 400, 500, 650, 800, 1000]; + +export const Controls: React.FC = ({ + currentIndex, + totalTokens, + tokens, + chapters, + isPlaying, + settings, + onTogglePlay, + onJumpTo, + onJumpBy, + onJumpToPreviousSentence, + onJumpToNextSentence, + onRestart, + onChangeWpm, +}) => { + const percent = + totalTokens > 0 + ? Math.min(100, Math.max(0, (currentIndex / (totalTokens - 1 || 1)) * 100)) + : 0; + + const remainingSeconds = estimateRemainingSeconds( + tokens, + currentIndex, + settings + ); + const remainingText = formatDuration(remainingSeconds); + + return ( +
+ {/* Progress & Timeline Bar */} + {settings.showProgressBar && ( +
+
+ + {currentIndex + 1} / {totalTokens}{' '} + ({percent.toFixed(1)}%) + + + 剩余 ~{remainingText} + +
+ +
+ {/* Chapter tick marks on the track */} + {chapters.length > 1 && + chapters.map((chap, idx) => { + if (idx === 0) return null; + const pos = (chap.startIndex / (totalTokens || 1)) * 100; + return ( +
+ ); + })} + + onJumpTo(Number(e.target.value))} + className="w-full h-1.5 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-rose-500 transition-all focus:outline-none" + /> +
+
+ )} + + {/* Main Playback Buttons */} +
+ {/* Restart */} + + + {/* Rewind 10 */} + + + {/* Previous Sentence */} + + + {/* Primary Play / Pause Button */} + + + {/* Next Sentence */} + + + {/* Forward 10 */} + + + {/* Quick Speed fine-tuning */} +
+ + + {settings.wpm} + + +
+
+ + {/* Preset WPM Chips */} +
+ + 预设语速: + + {WPM_PRESETS.map((preset) => { + const isActive = settings.wpm === preset; + return ( + + ); + })} +
+
+ ); +}; diff --git a/src/components/FocusDisplay.tsx b/src/components/FocusDisplay.tsx new file mode 100644 index 0000000..dc7564e --- /dev/null +++ b/src/components/FocusDisplay.tsx @@ -0,0 +1,165 @@ +import React from 'react'; +import type { FontFamily, FontSize, OrpColor, Token } from '../types'; + +interface FocusDisplayProps { + token: Token | null; + previousToken: Token | null; + nextToken: Token | null; + fontSize: FontSize; + fontFamily: FontFamily; + orpColor: OrpColor; + showGuideLines: boolean; + showContextPreview: boolean; + isPlaying: boolean; + onTogglePlay: () => void; +} + +const ORP_COLOR_MAP: Record = { + rose: { + text: 'text-rose-500', + bg: 'bg-rose-500', + glow: 'drop-shadow-[0_0_12px_rgba(244,63,94,0.45)]', + }, + amber: { + text: 'text-amber-500', + bg: 'bg-amber-500', + glow: 'drop-shadow-[0_0_12px_rgba(245,158,11,0.45)]', + }, + emerald: { + text: 'text-emerald-400', + bg: 'bg-emerald-400', + glow: 'drop-shadow-[0_0_12px_rgba(52,211,153,0.45)]', + }, + cyan: { + text: 'text-cyan-400', + bg: 'bg-cyan-400', + glow: 'drop-shadow-[0_0_12px_rgba(34,211,238,0.45)]', + }, + violet: { + text: 'text-purple-400', + bg: 'bg-purple-400', + glow: 'drop-shadow-[0_0_12px_rgba(192,132,252,0.45)]', + }, +}; + +const FONT_SIZE_MAP: Record = { + sm: 'text-3xl sm:text-4xl h-24 sm:h-28', + base: 'text-4xl sm:text-5xl h-28 sm:h-32', + lg: 'text-5xl sm:text-6xl h-32 sm:h-36', + xl: 'text-6xl sm:text-7xl h-36 sm:h-44', + '2xl': 'text-7xl sm:text-8xl h-44 sm:h-52', + '3xl': 'text-8xl sm:text-9xl h-52 sm:h-64', +}; + +const FONT_FAMILY_MAP: Record = { + mono: 'font-mono tracking-tight', + sans: 'font-sans tracking-normal font-medium', + serif: 'font-serif tracking-normal', +}; + +export const FocusDisplay: React.FC = ({ + token, + previousToken, + nextToken, + fontSize, + fontFamily, + orpColor, + showGuideLines, + showContextPreview, + isPlaying, + onTogglePlay, +}) => { + const colorConfig = ORP_COLOR_MAP[orpColor] || ORP_COLOR_MAP.rose; + const sizeClasses = FONT_SIZE_MAP[fontSize] || FONT_SIZE_MAP.xl; + const familyClasses = FONT_FAMILY_MAP[fontFamily] || FONT_FAMILY_MAP.sans; + + return ( +
+ {/* Contextual Peripheral Ghost Words (Top line) */} + {showContextPreview && ( +
+ + {previousToken ? previousToken.text : ''} + + + + {nextToken ? nextToken.text : ''} + +
+ )} + + {/* Main RSVP Focal Chamber */} +
+ {/* Top & Bottom ORP Crosshair Guide Notches */} + {showGuideLines && ( + <> + {/* Top tick mark */} +
+
+
+ + {/* Bottom tick mark */} +
+
+
+ + {/* Subtle horizontal baseline guides */} +
+ + )} + + {/* Word Display with Spritz Absolute Alignment */} + {token ? ( +
+ {/* Prefix: anchored to the left of the Pivot */} + + {token.prefix} + + + {/* Pivot (ORP): strictly mathematically centered at 50% */} + + {token.pivot || ' '} + + + {/* Suffix: anchored to the right of the Pivot */} + + {token.suffix} + +
+ ) : ( +
+ 请载入文本或选择样篇 +
+ )} + + {/* Play/Pause state watermarks */} + {!isPlaying && token && ( +
+ 已暂停 [空格继续] +
+ )} +
+ + {/* Punctuation / Pause indicator dot */} +
+ {token?.isParagraphEnd && ( + + ¶ 段落停顿 + + )} +
+
+ ); +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..9ae3a40 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,147 @@ +import { + BookOpen, + ClipboardPaste, + HelpCircle, + List, + Maximize, + Minimize, + Moon, + Settings as SettingsIcon, + Sun, +} from 'lucide-react'; +import React from 'react'; +import type { Book, Chapter, ReaderSettings } from '../types'; + +interface HeaderProps { + book: Book | null; + currentChapter?: Chapter; + settings: ReaderSettings; + isFullscreen: boolean; + onOpenLibrary: () => void; + onOpenPaste: () => void; + onOpenSettings: () => void; + onOpenShortcuts: () => void; + onOpenChapters: () => void; + onToggleFullscreen: () => void; + onCycleTheme: () => void; +} + +export const Header: React.FC = ({ + book, + currentChapter, + settings, + isFullscreen, + onOpenLibrary, + onOpenPaste, + onOpenSettings, + onOpenShortcuts, + onOpenChapters, + onToggleFullscreen, + onCycleTheme, +}) => { + return ( +
+ {/* Left: App Brand & Current Book Info */} +
+ {/* Logo */} +
+
+
+
+ Focus +
+ +
+ + {/* Current Book & Chapter pill */} +
+ + + {book && book.chapters.length > 1 && ( + + )} +
+
+ + {/* Right Action Icons */} +
+ {/* Paste Instant Read */} + + + {/* Bookshelf */} + + + {/* Theme Toggle */} + + + {/* Settings */} + + + {/* Shortcuts */} + + + {/* Fullscreen */} + +
+
+ ); +}; diff --git a/src/components/LibraryModal.tsx b/src/components/LibraryModal.tsx new file mode 100644 index 0000000..b043d8f --- /dev/null +++ b/src/components/LibraryModal.tsx @@ -0,0 +1,338 @@ +import { BookOpen, Sparkles, Trash2, Upload, X } from 'lucide-react'; +import React, { useRef, useState } from 'react'; +import { parseEpubFile } from '../core/epub'; +import { tokenizeText } from '../core/tokenizer'; +import { getBuiltinSampleBooks } from '../db/samples'; +import type { Book, BookMeta } from '../types'; + +interface LibraryModalProps { + isOpen: boolean; + books: BookMeta[]; + activeBookId: string | null; + onClose: () => void; + onSelectBook: (bookId: string) => void; + onSaveNewBook: (book: Book) => Promise; + onDeleteBook: (bookId: string) => Promise; +} + +export const LibraryModal: React.FC = ({ + isOpen, + books, + activeBookId, + onClose, + onSelectBook, + onSaveNewBook, + onDeleteBook, +}) => { + const [activeTab, setActiveTab] = useState<'shelf' | 'upload' | 'samples'>('shelf'); + const [isProcessing, setIsProcessing] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const fileInputRef = useRef(null); + + if (!isOpen) return null; + + const handleFileUpload = async (file: File) => { + setIsProcessing(true); + setErrorMessage(null); + try { + const fileName = file.name; + const lower = fileName.toLowerCase(); + let title = fileName.replace(/\.[^/.]+$/, ''); + let author = '未知作者'; + let fullText = ''; + let chapters = undefined; + + if (lower.endsWith('.epub')) { + const parsed = await parseEpubFile(file); + title = parsed.title; + author = parsed.author; + fullText = parsed.fullText; + chapters = parsed.chapters; + } else if (lower.endsWith('.txt') || lower.endsWith('.md')) { + fullText = await file.text(); + } else { + throw new Error('仅支持 .txt、.md 或 .epub 格式文件'); + } + + if (!fullText.trim()) { + throw new Error('文件内容为空'); + } + + const { tokens, chapters: detectedChapters } = tokenizeText( + fullText, + chapters + ); + + const newBook: Book = { + id: `book-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, + title, + author, + format: lower.endsWith('.epub') ? 'epub' : lower.endsWith('.md') ? 'md' : 'txt', + tokens, + chapters: detectedChapters, + currentProgress: 0, + totalTokens: tokens.length, + createdAt: Date.now(), + lastReadAt: Date.now(), + byteSize: file.size, + }; + + await onSaveNewBook(newBook); + onSelectBook(newBook.id); + onClose(); + } catch (err: unknown) { + console.error('File import failed:', err); + setErrorMessage( + err instanceof Error ? err.message : '文件解析失败,请检查格式' + ); + } finally { + setIsProcessing(false); + } + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + handleFileUpload(file); + } + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files?.[0]; + if (file) { + handleFileUpload(file); + } + }; + + const handleLoadSample = async (sampleBook: Book) => { + await onSaveNewBook(sampleBook); + onSelectBook(sampleBook.id); + onClose(); + }; + + const samples = getBuiltinSampleBooks(); + + return ( +
+
+ {/* Header */} +
+
+ +

+ 书架与导入 +

+
+ +
+ + {/* Tab switcher */} +
+ + + +
+ + {/* Content Body */} +
+ {errorMessage && ( +
+ {errorMessage} +
+ )} + + {/* TAB 1: Bookshelf */} + {activeTab === 'shelf' && ( +
+ {books.length === 0 ? ( +
+ +

书架空空如也

+

+ 点击上方“上传文件”或直接体验“精选样篇” +

+
+ ) : ( + books.map((b) => { + const isActive = b.id === activeBookId; + const progressPct = + b.totalTokens > 0 + ? Math.min( + 100, + Math.round( + (b.currentProgress / (b.totalTokens - 1 || 1)) * 100 + ) + ) + : 0; + + return ( +
{ + onSelectBook(b.id); + onClose(); + }} + className={`group relative p-4 rounded-xl border cursor-pointer transition-all ${ + isActive + ? 'border-rose-500/80 bg-rose-500/5 shadow-sm' + : 'border-neutral-200 dark:border-neutral-800 hover:border-neutral-400 dark:hover:border-neutral-700 bg-neutral-50/50 dark:bg-neutral-800/20' + }`} + > +
+
+
+

+ {b.title} +

+ {isActive && ( + + 正在阅读 + + )} +
+

+ {b.author && b.author !== '未知作者' + ? `${b.author} · ` + : ''} + 共 {b.totalTokens.toLocaleString()} 词 · 格式: + {b.format.toUpperCase()} +

+ + {/* Progress bar inside card */} +
+
+
+
+ + {progressPct}% + +
+
+ + {/* Actions */} +
+ +
+
+
+ ); + }) + )} +
+ )} + + {/* TAB 2: Upload Dropzone */} + {activeTab === 'upload' && ( +
+
e.preventDefault()} + onDrop={handleDrop} + onClick={() => fileInputRef.current?.click()} + className="border-2 border-dashed border-neutral-300 dark:border-neutral-700 hover:border-rose-500 dark:hover:border-rose-500 rounded-2xl p-8 text-center cursor-pointer transition-colors bg-neutral-50/50 dark:bg-neutral-800/10" + > + + + +

+ 点击选择文件 或 拖拽至此处 +

+

+ 支持 TXT、Markdown (.md)、EPUB 电子书,浏览器内存级秒解 +

+
+ + {isProcessing && ( +
+
+ 正在解析并生成 ORP 切词数据... +
+ )} +
+ )} + + {/* TAB 3: Curated Samples */} + {activeTab === 'samples' && ( +
+

+ 点击任意样篇,立即开启无干扰的极速心流体验: +

+ {samples.map((sample) => ( +
handleLoadSample(sample)} + className="p-4 rounded-xl border border-neutral-200 dark:border-neutral-800 hover:border-rose-500/80 bg-neutral-50/50 dark:bg-neutral-800/20 cursor-pointer transition-all flex items-center justify-between" + > +
+

+ {sample.title} +

+

+ {sample.author} · {sample.totalTokens} 词 +

+
+ +
+ ))} +
+ )} +
+
+
+ ); +}; diff --git a/src/components/PasteModal.tsx b/src/components/PasteModal.tsx new file mode 100644 index 0000000..8bca2cf --- /dev/null +++ b/src/components/PasteModal.tsx @@ -0,0 +1,164 @@ +import { Clipboard, Play, X } from 'lucide-react'; +import React, { useState } from 'react'; +import { tokenizeText } from '../core/tokenizer'; +import type { Book } from '../types'; + +interface PasteModalProps { + isOpen: boolean; + wpm: number; + onClose: () => void; + onSaveAndPlay: (book: Book) => Promise; +} + +export const PasteModal: React.FC = ({ + isOpen, + wpm, + onClose, + onSaveAndPlay, +}) => { + const [content, setContent] = useState(''); + const [title, setTitle] = useState(''); + const [isReadingClipboard, setIsReadingClipboard] = useState(false); + + if (!isOpen) return null; + + const handlePasteFromClipboard = async () => { + try { + setIsReadingClipboard(true); + const text = await navigator.clipboard.readText(); + if (text) { + setContent(text); + if (!title) { + const firstLine = text.trim().split('\n')[0].substring(0, 30); + setTitle(firstLine || '剪贴板快读'); + } + } + } catch (err) { + console.warn('Clipboard read failed', err); + } finally { + setIsReadingClipboard(false); + } + }; + + const handleStart = async () => { + if (!content.trim()) return; + + let bookTitle = title.trim(); + if (!bookTitle) { + const firstLine = content.trim().split('\n')[0].substring(0, 30); + bookTitle = firstLine || '剪贴板阅读'; + } + + const { tokens, chapters } = tokenizeText(content); + const newBook: Book = { + id: `paste-${Date.now()}`, + title: bookTitle, + author: '剪贴板投放', + format: 'paste', + tokens, + chapters, + currentProgress: 0, + totalTokens: tokens.length, + createdAt: Date.now(), + lastReadAt: Date.now(), + }; + + await onSaveAndPlay(newBook); + onClose(); + setContent(''); + setTitle(''); + }; + + const charCount = content.length; + const approxMinutes = + charCount > 0 ? Math.max(1, Math.ceil((charCount / 2) / Math.max(1, wpm))) : 0; + + return ( +
+
+ {/* Header */} +
+
+ +

+ 剪贴板投放快读 +

+
+ +
+ + {/* Body */} +
+
+ + setTitle(e.target.value)} + className="w-full px-3 py-2 text-sm bg-neutral-100 dark:bg-neutral-800 rounded-lg border border-neutral-300 dark:border-neutral-700 focus:outline-none focus:border-rose-500 text-neutral-900 dark:text-neutral-100" + /> +
+ +
+
+ + +
+