Compare commits

..

42 Commits

Author SHA1 Message Date
JSC
86a04cf5cb feat: add README file to provide project documentation
Some checks failed
Frontend CI / lint (push) Failing after 22s
Frontend CI / build (push) Has been skipped
2025-10-05 16:29:07 +02:00
JSC
0a2b859e7a feat: add border to Play Next Queue section for improved visual separation 2025-10-05 04:09:29 +02:00
JSC
7a6288cc02 feat: update PlayNextQueue component and integrate it into Player; adjust layout in Playlist for improved UI
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped
2025-10-04 21:55:20 +02:00
JSC
0f8b96e73c feat: add PlayNextQueue component to display upcoming tracks in Player 2025-10-04 19:40:01 +02:00
JSC
9a2a9343d2 feat: add context menu to Playlist for adding tracks to play next queue 2025-10-04 19:16:25 +02:00
JSC
4352e4c792 feat: implement lazy loading for routes and add a loading component in App.tsx; configure manual chunks in Vite for optimized builds
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped
2025-10-04 15:44:33 +02:00
JSC
b303d2f5cf feat: add search functionality to filter available sounds in PlaylistEditPage
Some checks failed
Frontend CI / lint (push) Failing after 21s
Frontend CI / build (push) Has been skipped
2025-10-04 15:29:32 +02:00
JSC
9aa628a9d2 refactor: standardize "use client" directive and improve button styles across components
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped
2025-10-04 14:57:18 +02:00
JSC
cbd4b93fd4 chore: update recharts dependency and refactor chart components
Some checks failed
Frontend CI / lint (push) Failing after 21s
Frontend CI / build (push) Has been skipped
- Updated recharts version from 3.2.1 to 2.15.4 in package.json.
- Removed unused imports and types in GlobalSearch component.
- Adjusted spacing in CardHeader component.
- Refactored ChartTooltipContent and ChartLegendContent components to filter out items with type 'none' and improve readability.
2025-10-04 14:50:40 +02:00
JSC
7d94e29dcb feat: add global escape key handling to close search modal
Some checks failed
Frontend CI / lint (push) Failing after 17s
Frontend CI / build (push) Has been skipped
2025-10-04 14:41:59 +02:00
JSC
adf16210a4 feat: implement global search functionality with keyboard shortcut and results display
Some checks failed
Frontend CI / lint (push) Failing after 19s
Frontend CI / build (push) Has been skipped
2025-10-04 14:39:04 +02:00
JSC
adcf9832f4 feat: add search functionality to Playlist component for filtering tracks
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped
2025-10-04 14:09:33 +02:00
JSC
53a39126f4 chore: update dependencies to latest versions
Some checks failed
Frontend CI / lint (push) Failing after 19s
Frontend CI / build (push) Has been skipped
- Updated @radix-ui/react-checkbox, @radix-ui/react-context-menu, @radix-ui/react-dialog, @radix-ui/react-dropdown-menu, @radix-ui/react-popover, @radix-ui/react-scroll-area, @radix-ui/react-select, @radix-ui/react-slider, @radix-ui/react-switch, @radix-ui/react-tabs, @radix-ui/react-tooltip, and @tailwindcss/vite to their latest versions.
- Updated lucide-react, react, react-day-picker, react-dom, react-router, and recharts to their latest versions.
- Updated devDependencies: @eslint/js, @types/node, @types/react, @types/react-dom, @vitejs/plugin-react-swc, eslint, eslint-plugin-react-hooks, eslint-plugin-react-refresh, globals, tw-animate-css, typescript, typescript-eslint, and vite to their latest versions.
2025-10-04 13:55:20 +02:00
JSC
f43fec3362 feat: add TopUsersSection component to DashboardPage for displaying top users
Some checks failed
Frontend CI / lint (push) Failing after 19s
Frontend CI / build (push) Has been skipped
2025-09-27 21:52:10 +02:00
JSC
d17dc5558c feat: add TTS statistics to DashboardPage and StatisticsGrid components 2025-09-27 21:38:07 +02:00
JSC
79d9bd7f76 feat: update SoundCard styling and adjust layout in SoundsPage for better responsiveness
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped
2025-09-22 20:43:09 +02:00
JSC
756e1307f1 Merge branch 'optimised_player'
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped
2025-09-22 20:30:59 +02:00
JSC
a25fb9b5eb feat: implement new compact player components for controls, progress, and track info 2025-09-22 20:28:27 +02:00
JSC
9db9915e56 feat: remove useRenderFlash hook and clean up related code in Player components 2025-09-22 20:14:38 +02:00
JSC
9366dbca14 feat: integrate volume control into PlayerControls and remove PlayerVolume component 2025-09-22 20:10:37 +02:00
JSC
9784d259ba feat: refactor player component structure and introduce new player controls, progress, track info, and volume components 2025-09-22 19:47:13 +02:00
JSC
b77dff03c1 feat: optimize player state updates and memoize calculations to prevent unnecessary re-renders
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped
2025-09-21 20:32:29 +02:00
JSC
8945eb6ad6 feat: remove task update functionality from SchedulersTable and clean up related code
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped
2025-09-21 18:48:05 +02:00
JSC
dc1124dff6 feat: add delete functionality for playlists with confirmation and error handling
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped
2025-09-21 18:32:33 +02:00
JSC
d551223566 Merge branch 'tts'
Some checks failed
Frontend CI / lint (push) Failing after 19s
Frontend CI / build (push) Has been skipped
2025-09-21 18:18:57 +02:00
JSC
1d2c27abbd feat: enhance loading states with table structure and improve sound type filtering in SoundsPage 2025-09-21 18:17:55 +02:00
JSC
41659c9299 feat: add badge to display sound type in SoundCard component 2025-09-21 15:39:04 +02:00
JSC
7faf2d38ab feat: add language selection combobox and update TTS dialog for improved language handling 2025-09-21 15:20:12 +02:00
JSC
7ac979a4f4 refactor: remove unnecessary reset of selected provider in CreateTTSDialog 2025-09-21 14:43:27 +02:00
JSC
92846c6d3a feat: implement TTS event handling for creation, completion, and failure in TTSPage and SocketContext 2025-09-21 14:39:15 +02:00
JSC
75b52caf85 refactor: update TASK_TYPES to comment out 'credit_recharge' in CreateTaskDialog and SchedulersHeader
feat: add filter icon to SelectTrigger in SoundsPage for improved UI
2025-09-21 13:56:50 +02:00
JSC
fde19f47c8 refactor: update TTSTable component to improve date formatting and remove Sound ID display 2025-09-21 13:40:56 +02:00
JSC
3516f7f205 fix: update TTS API endpoints for consistency in request and history retrieval 2025-09-21 13:38:05 +02:00
JSC
d48291f6ed feat: add type filter to SoundsPage for improved sound categorization 2025-09-21 13:31:19 +02:00
JSC
620418c405 refactor: update TTSHeader component for improved sorting and search functionality 2025-09-21 13:21:02 +02:00
JSC
6f477a1aa7 feat: add Text to Speech (TTS) functionality with provider selection, generation, and history management
- Implemented CreateTTSDialog for generating TTS from user input.
- Added TTSHeader for search, sorting, and creation controls.
- Created TTSList to display TTS history with filtering and sorting capabilities.
- Developed TTSLoadingStates for handling loading and error states.
- Introduced TTSRow for individual TTS entries with play and delete options.
- Built TTSTable for structured display of TTS history.
- Integrated TTS service API for generating and managing TTS data.
- Added TTSPage to encapsulate the TTS feature with pagination and state management.
2025-09-20 23:11:21 +02:00
JSC
da4566c789 feat: add 'Stop All Sounds' button to Player component for improved sound control
Some checks failed
Frontend CI / lint (push) Failing after 20s
Frontend CI / build (push) Has been skipped
2025-09-20 15:50:47 +02:00
JSC
7c3a8aab64 fix: add cursor pointer style to StopSoundsButton for better UX
Some checks failed
Frontend CI / lint (push) Failing after 19s
Frontend CI / build (push) Has been skipped
2025-09-20 15:41:07 +02:00
JSC
72914da637 feat: add Dailymotion badge and improve extraction title display in ExtractionsRow
Some checks failed
Frontend CI / lint (push) Failing after 17s
Frontend CI / build (push) Has been skipped
2025-09-20 15:33:10 +02:00
JSC
70131ecd2d feat: add status filter to ExtractionsHeader and update SchedulersHeader layout
Some checks failed
Frontend CI / lint (push) Failing after 22s
Frontend CI / build (push) Has been skipped
2025-09-20 15:03:36 +02:00
JSC
f559a6aa73 fix: remove unused task type 'credit_recharge' from CreateTaskDialog
Some checks failed
Frontend CI / lint (push) Failing after 17s
Frontend CI / build (push) Has been skipped
2025-09-19 11:54:54 +02:00
JSC
b024b19ecc fix: update Toaster position and mark Sequencer link as WIP
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped
2025-09-18 13:57:43 +02:00
64 changed files with 4329 additions and 1130 deletions

0
README.md Normal file
View File

360
bun.lock
View File

@@ -9,55 +9,55 @@
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@number-flow/react": "^0.5.10", "@number-flow/react": "^0.5.10",
"@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-context-menu": "^2.2.15", "@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.14",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"lucide-react": "^0.539.0", "lucide-react": "^0.544.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.1.0", "react": "^19.2.0",
"react-day-picker": "^9.8.1", "react-day-picker": "^9.11.0",
"react-dom": "^19.1.0", "react-dom": "^19.2.0",
"react-router": "^7.7.1", "react-router": "^7.9.3",
"recharts": "2.15.4", "recharts": "2.15.4",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.14",
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.30.1", "@eslint/js": "^9.37.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^24.1.0", "@types/node": "^24.6.2",
"@types/react": "^19.1.8", "@types/react": "^19.2.0",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.2.0",
"@vitejs/plugin-react-swc": "^3.10.2", "@vitejs/plugin-react-swc": "^4.1.0",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^3.2.4",
"eslint": "^9.30.1", "eslint": "^9.37.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^6.1.1",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.23",
"globals": "^16.3.0", "globals": "^16.4.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"tw-animate-css": "^1.3.6", "tw-animate-css": "^1.4.0",
"typescript": "~5.8.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.35.1", "typescript-eslint": "^8.45.0",
"vite": "^7.0.4", "vite": "^7.1.9",
"vitest": "^3.2.4", "vitest": "^3.2.4",
}, },
}, },
@@ -67,17 +67,31 @@
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="],
"@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
"@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="], "@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
"@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
"@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
@@ -87,7 +101,7 @@
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
"@date-fns/tz": ["@date-fns/tz@1.2.0", "", {}, "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg=="], "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
@@ -149,23 +163,23 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="], "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.3.0", "", {}, "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="], "@eslint/config-helpers": ["@eslint/config-helpers@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog=="],
"@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="], "@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.32.0", "", {}, "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg=="], "@eslint/js": ["@eslint/js@9.37.0", "", {}, "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.4", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="],
"@floating-ui/core": ["@floating-ui/core@1.7.2", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw=="], "@floating-ui/core": ["@floating-ui/core@1.7.2", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw=="],
@@ -191,6 +205,8 @@
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
@@ -209,13 +225,13 @@
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA=="], "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
@@ -223,17 +239,17 @@
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw=="], "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="], "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="], "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ=="], "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="], "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
@@ -241,37 +257,37 @@
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew=="], "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw=="], "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="], "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="], "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="], "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.9", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A=="], "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="], "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw=="], "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ=="], "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw=="], "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
@@ -295,7 +311,7 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.35", "", {}, "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.1", "", { "os": "android", "cpu": "arm" }, "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.1", "", { "os": "android", "cpu": "arm" }, "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA=="],
@@ -339,61 +355,61 @@
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
"@swc/core": ["@swc/core@1.13.2", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.2", "@swc/core-darwin-x64": "1.13.2", "@swc/core-linux-arm-gnueabihf": "1.13.2", "@swc/core-linux-arm64-gnu": "1.13.2", "@swc/core-linux-arm64-musl": "1.13.2", "@swc/core-linux-x64-gnu": "1.13.2", "@swc/core-linux-x64-musl": "1.13.2", "@swc/core-win32-arm64-msvc": "1.13.2", "@swc/core-win32-ia32-msvc": "1.13.2", "@swc/core-win32-x64-msvc": "1.13.2" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg=="], "@swc/core": ["@swc/core@1.13.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.5", "@swc/core-darwin-x64": "1.13.5", "@swc/core-linux-arm-gnueabihf": "1.13.5", "@swc/core-linux-arm64-gnu": "1.13.5", "@swc/core-linux-arm64-musl": "1.13.5", "@swc/core-linux-x64-gnu": "1.13.5", "@swc/core-linux-x64-musl": "1.13.5", "@swc/core-win32-arm64-msvc": "1.13.5", "@swc/core-win32-ia32-msvc": "1.13.5", "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ=="],
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw=="], "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ=="],
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.13.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Lb9EZi7X2XDAVmuUlBm2UvVAgSCbD3qKqDCxSI4jEOddzVOpNCnyZ/xEampdngUIyDDhhJLYU9duC+Mcsv5Y+A=="], "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.13.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng=="],
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.13.2", "", { "os": "linux", "cpu": "arm" }, "sha512-9TDe/92ee1x57x+0OqL1huG4BeljVx0nWW4QOOxp8CCK67Rpc/HHl2wciJ0Kl9Dxf2NvpNtkPvqj9+BUmM9WVA=="], "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.13.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ=="],
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.13.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KJUSl56DBk7AWMAIEcU83zl5mg3vlQYhLELhjwRFkGFMvghQvdqQ3zFOYa4TexKA7noBZa3C8fb24rI5sw9Exg=="], "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw=="],
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.13.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-teU27iG1oyWpNh9CzcGQ48ClDRt/RCem7mYO7ehd2FY102UeTws2+OzLESS1TS1tEZipq/5xwx3FzbVgiolCiQ=="], "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ=="],
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.13.2", "", { "os": "linux", "cpu": "x64" }, "sha512-dRPsyPyqpLD0HMRCRpYALIh4kdOir8pPg4AhNQZLehKowigRd30RcLXGNVZcc31Ua8CiPI4QSgjOIxK+EQe4LQ=="], "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA=="],
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.13.2", "", { "os": "linux", "cpu": "x64" }, "sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw=="], "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q=="],
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.13.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Wv/QTA6PjyRLlmKcN6AmSI4jwSMRl0VTLGs57PHTqYRwwfwd7y4s2fIPJVBNbAlXd795dOEP6d/bGSQSyhOX3A=="], "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.13.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw=="],
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.13.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-PuCdtNynEkUNbUXX/wsyUC+t4mamIU5y00lT5vJcAvco3/r16Iaxl5UCzhXYaWZSNVZMzPp9qN8NlSL8M5pPxw=="], "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.13.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw=="],
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.13.2", "", { "os": "win32", "cpu": "x64" }, "sha512-qlmMkFZJus8cYuBURx1a3YAG2G7IW44i+FEYV5/32ylKkzGNAr9tDJSA53XNnNXkAB5EXSPsOz7bn5C3JlEtdQ=="], "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.13.5", "", { "os": "win32", "cpu": "x64" }, "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
"@swc/types": ["@swc/types@0.1.23", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw=="], "@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.14", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.1", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.14" } }, "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.14", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.5.1" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.14", "@tailwindcss/oxide-darwin-arm64": "4.1.14", "@tailwindcss/oxide-darwin-x64": "4.1.14", "@tailwindcss/oxide-freebsd-x64": "4.1.14", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", "@tailwindcss/oxide-linux-x64-musl": "4.1.14", "@tailwindcss/oxide-wasm32-wasi": "4.1.14", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" } }, "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="], "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.14", "", { "os": "android", "cpu": "arm64" }, "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="], "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="], "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="], "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="], "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14", "", { "os": "linux", "cpu": "arm" }, "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="], "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="], "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="], "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="], "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="], "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.14", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.5", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="], "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="], "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.14", "", { "os": "win32", "cpu": "x64" }, "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.1.14", "", { "dependencies": { "@tailwindcss/node": "4.1.14", "@tailwindcss/oxide": "4.1.14", "tailwindcss": "4.1.14" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA=="],
"@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@5.2.2", "", { "dependencies": { "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-svelte", "svelte"] }, "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA=="], "@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@5.2.2", "", { "dependencies": { "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-svelte", "svelte"] }, "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA=="],
@@ -423,33 +439,33 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], "@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="],
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], "@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="],
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="], "@types/react-dom": ["@types/react-dom@19.2.0", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.38.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/type-utils": "8.38.0", "@typescript-eslint/utils": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.45.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/type-utils": "8.45.0", "@typescript-eslint/utils": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.45.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.38.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.45.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.38.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.38.0", "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg=="], "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.45.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.45.0", "@typescript-eslint/types": "^8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0" } }, "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.38.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ=="], "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.45.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/utils": "8.45.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.45.0", "", {}, "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.45.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.45.0", "@typescript-eslint/tsconfig-utils": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.45.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.45.0", "", { "dependencies": { "@typescript-eslint/types": "8.45.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag=="],
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.11.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.27", "@swc/core": "^1.12.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w=="], "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.1.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.35", "@swc/core": "^1.13.5" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-Ff690TUck0Anlh7wdIcnsVMhofeEVgm44Y4OYdeeEEPSKyZHzDI9gfVBvySEhDfXtBp8tLCbfsVKPWEMEjq8/g=="],
"@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.4", "vitest": "3.2.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ=="], "@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.4", "vitest": "3.2.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ=="],
@@ -487,14 +503,20 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-i+sRXGhz4+QW8aACZ3+r1GAKMt0wlFpeA8M5rOQd0HEYw9zhDrlx9Wc8uQ0IdXakjJRthzglEwfB/yqIjO6iDg=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.26.3", "", { "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001747", "", {}, "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg=="],
"chai": ["chai@5.2.1", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A=="], "chai": ["chai@5.2.1", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -515,6 +537,8 @@
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -563,25 +587,29 @@
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.230", "", {}, "sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"engine.io-client": ["engine.io-client@6.6.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w=="], "engine.io-client": ["engine.io-client@6.6.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w=="],
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="], "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"esbuild": ["esbuild@0.25.8", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="], "esbuild": ["esbuild@0.25.8", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.32.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.32.0", "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg=="], "eslint": ["eslint@9.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.4.0", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.37.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@6.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "zod": "^3.22.4 || ^4.0.0", "zod-validation-error": "^3.0.3 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ=="],
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.23", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
@@ -607,7 +635,7 @@
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="], "fast-equals": ["fast-equals@5.3.2", "", {}, "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -617,7 +645,7 @@
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -633,13 +661,15 @@
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
@@ -693,6 +723,8 @@
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
@@ -729,9 +761,9 @@
"loupe": ["loupe@3.2.0", "", {}, "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw=="], "loupe": ["loupe@3.2.0", "", {}, "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw=="],
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.539.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg=="], "lucide-react": ["lucide-react@0.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
@@ -747,9 +779,7 @@
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -759,6 +789,8 @@
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
"node-releases": ["node-releases@2.0.23", "", {}, "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="],
"number-flow": ["number-flow@0.5.8", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-FPr1DumWyGi5Nucoug14bC6xEz70A1TnhgSHhKyfqjgji2SOTz+iLJxKtv37N5JyJbteGYCm6NQ9p1O4KZ7iiA=="], "number-flow": ["number-flow@0.5.8", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-FPr1DumWyGi5Nucoug14bC6xEz70A1TnhgSHhKyfqjgji2SOTz+iLJxKtv37N5JyJbteGYCm6NQ9p1O4KZ7iiA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -799,11 +831,11 @@
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"react-day-picker": ["react-day-picker@9.8.1", "", { "dependencies": { "@date-fns/tz": "^1.2.0", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-kMcLrp3PfN/asVJayVv82IjF3iLOOxuH5TNFWezX6lS/T8iVRFPTETpHl3TUSTH99IDMZLubdNPJr++rQctkEw=="], "react-day-picker": ["react-day-picker@9.11.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-L4FYOaPrr3+AEROeP6IG2mCORZZfxJDkJI2df8mv1jyPrNYeccgmFPZDaHyAuPCBCddQFozkxbikj2NhMEYfDQ=="],
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
@@ -811,7 +843,7 @@
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-router": ["react-router@7.7.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA=="], "react-router": ["react-router@7.9.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg=="],
"react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="], "react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="],
@@ -831,9 +863,9 @@
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
@@ -873,11 +905,11 @@
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], "tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="],
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="], "tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], "tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="],
"test-exclude": ["test-exclude@7.0.1", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^9.0.4" } }, "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg=="], "test-exclude": ["test-exclude@7.0.1", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^9.0.4" } }, "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg=="],
@@ -887,7 +919,7 @@
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
@@ -901,15 +933,17 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tw-animate-css": ["tw-animate-css@1.3.6", "", {}, "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA=="], "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.38.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.38.0", "@typescript-eslint/parser": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/utils": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg=="], "typescript-eslint": ["typescript-eslint@8.45.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.45.0", "@typescript-eslint/parser": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/utils": "8.45.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], "undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
@@ -921,7 +955,7 @@
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
"vite": ["vite@7.0.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg=="], "vite": ["vite@7.1.9", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
@@ -945,23 +979,47 @@
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"@babel/core/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@babel/core/@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
"@babel/core/@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
"@babel/core/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
"@babel/helpers/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], "@tailwindcss/node/magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.6", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -969,6 +1027,10 @@
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"cmdk/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="],
"engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], "engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -977,8 +1039,14 @@
"loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"make-dir/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], "socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
@@ -993,22 +1061,54 @@
"test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"vite-node/vite": ["vite@7.0.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg=="],
"vitest/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"vitest/vite": ["vite@7.0.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
"@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
"@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@tailwindcss/node/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"test-exclude/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "test-exclude/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"vite-node/vite/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
"vite-node/vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"vitest/tinyglobby/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
"vitest/vite/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],

View File

@@ -16,55 +16,55 @@
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@number-flow/react": "^0.5.10", "@number-flow/react": "^0.5.10",
"@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-context-menu": "^2.2.15", "@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.14",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"lucide-react": "^0.539.0", "lucide-react": "^0.544.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.1.0", "react": "^19.2.0",
"react-day-picker": "^9.8.1", "react-day-picker": "^9.11.0",
"react-dom": "^19.1.0", "react-dom": "^19.2.0",
"react-router": "^7.7.1", "react-router": "^7.9.3",
"recharts": "2.15.4", "recharts": "2.15.4",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11" "tailwindcss": "^4.1.14"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.30.1", "@eslint/js": "^9.37.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^24.1.0", "@types/node": "^24.6.2",
"@types/react": "^19.1.8", "@types/react": "^19.2.0",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.2.0",
"@vitejs/plugin-react-swc": "^3.10.2", "@vitejs/plugin-react-swc": "^4.1.0",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^3.2.4",
"eslint": "^9.30.1", "eslint": "^9.37.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^6.1.1",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.23",
"globals": "^16.3.0", "globals": "^16.4.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"tw-animate-css": "^1.3.6", "tw-animate-css": "^1.4.0",
"typescript": "~5.8.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.35.1", "typescript-eslint": "^8.45.0",
"vite": "^7.0.4", "vite": "^7.1.9",
"vitest": "^3.2.4" "vitest": "^3.2.4"
} }
} }

View File

@@ -1,22 +1,38 @@
import { lazy, Suspense } from 'react'
import { Navigate, Route, Routes } from 'react-router' import { Navigate, Route, Routes } from 'react-router'
import { LocaleProvider } from './components/LocaleProvider' import { LocaleProvider } from './components/LocaleProvider'
import { ThemeProvider } from './components/ThemeProvider' import { ThemeProvider } from './components/ThemeProvider'
import { Toaster } from './components/ui/sonner' import { Toaster } from './components/ui/sonner'
import { AuthProvider, useAuth } from './contexts/AuthContext' import { AuthProvider, useAuth } from './contexts/AuthContext'
import { SocketProvider } from './contexts/SocketContext' import { SocketProvider } from './contexts/SocketContext'
import { AccountPage } from './pages/AccountPage'
import { AuthCallbackPage } from './pages/AuthCallbackPage' // Lazy load all pages for code splitting
import { DashboardPage } from './pages/DashboardPage' const AccountPage = lazy(() => import('./pages/AccountPage').then(m => ({ default: m.AccountPage })))
import { ExtractionsPage } from './pages/ExtractionsPage' const AuthCallbackPage = lazy(() => import('./pages/AuthCallbackPage').then(m => ({ default: m.AuthCallbackPage })))
import { LoginPage } from './pages/LoginPage' const DashboardPage = lazy(() => import('./pages/DashboardPage').then(m => ({ default: m.DashboardPage })))
import { PlaylistEditPage } from './pages/PlaylistEditPage' const ExtractionsPage = lazy(() => import('./pages/ExtractionsPage').then(m => ({ default: m.ExtractionsPage })))
import { PlaylistsPage } from './pages/PlaylistsPage' const LoginPage = lazy(() => import('./pages/LoginPage').then(m => ({ default: m.LoginPage })))
import { RegisterPage } from './pages/RegisterPage' const PlaylistEditPage = lazy(() => import('./pages/PlaylistEditPage').then(m => ({ default: m.PlaylistEditPage })))
import { SchedulersPage } from './pages/SchedulersPage' const PlaylistsPage = lazy(() => import('./pages/PlaylistsPage').then(m => ({ default: m.PlaylistsPage })))
import { SequencerPage } from './pages/SequencerPage' const RegisterPage = lazy(() => import('./pages/RegisterPage').then(m => ({ default: m.RegisterPage })))
import { SoundsPage } from './pages/SoundsPage' const SchedulersPage = lazy(() => import('./pages/SchedulersPage').then(m => ({ default: m.SchedulersPage })))
import { SettingsPage } from './pages/admin/SettingsPage' const SequencerPage = lazy(() => import('./pages/SequencerPage').then(m => ({ default: m.SequencerPage })))
import { UsersPage } from './pages/admin/UsersPage' const SoundsPage = lazy(() => import('./pages/SoundsPage').then(m => ({ default: m.SoundsPage })))
const TTSPage = lazy(() => import('./pages/TTSPage').then(m => ({ default: m.TTSPage })))
const SettingsPage = lazy(() => import('./pages/admin/SettingsPage').then(m => ({ default: m.SettingsPage })))
const UsersPage = lazy(() => import('./pages/admin/UsersPage').then(m => ({ default: m.UsersPage })))
// Loading component for lazy-loaded routes
function PageLoader() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div>
<p className="text-muted-foreground text-sm">Loading...</p>
</div>
</div>
)
}
function ProtectedRoute({ children }: { children: React.ReactNode }) { function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth() const { user, loading } = useAuth()
@@ -62,97 +78,107 @@ function AppRoutes() {
const { user } = useAuth() const { user } = useAuth()
return ( return (
<Routes> <Suspense fallback={<PageLoader />}>
<Route <Routes>
path="/login" <Route
element={user ? <Navigate to="/" replace /> : <LoginPage />} path="/login"
/> element={user ? <Navigate to="/" replace /> : <LoginPage />}
<Route />
path="/register" <Route
element={user ? <Navigate to="/" replace /> : <RegisterPage />} path="/register"
/> element={user ? <Navigate to="/" replace /> : <RegisterPage />}
<Route path="/auth/callback" element={<AuthCallbackPage />} /> />
<Route <Route path="/auth/callback" element={<AuthCallbackPage />} />
path="/" <Route
element={ path="/"
<ProtectedRoute> element={
<DashboardPage /> <ProtectedRoute>
</ProtectedRoute> <DashboardPage />
} </ProtectedRoute>
/> }
<Route />
path="/sounds" <Route
element={ path="/sounds"
<ProtectedRoute> element={
<SoundsPage /> <ProtectedRoute>
</ProtectedRoute> <SoundsPage />
} </ProtectedRoute>
/> }
<Route />
path="/playlists" <Route
element={ path="/playlists"
<ProtectedRoute> element={
<PlaylistsPage /> <ProtectedRoute>
</ProtectedRoute> <PlaylistsPage />
} </ProtectedRoute>
/> }
<Route />
path="/playlists/:id/edit" <Route
element={ path="/playlists/:id/edit"
<ProtectedRoute> element={
<PlaylistEditPage /> <ProtectedRoute>
</ProtectedRoute> <PlaylistEditPage />
} </ProtectedRoute>
/> }
<Route />
path="/extractions" <Route
element={ path="/extractions"
<ProtectedRoute> element={
<ExtractionsPage /> <ProtectedRoute>
</ProtectedRoute> <ExtractionsPage />
} </ProtectedRoute>
/> }
<Route />
path="/sequencer" <Route
element={ path="/tts"
<ProtectedRoute> element={
<SequencerPage /> <ProtectedRoute>
</ProtectedRoute> <TTSPage />
} </ProtectedRoute>
/> }
<Route />
path="/schedulers" <Route
element={ path="/sequencer"
<ProtectedRoute> element={
<SchedulersPage /> <ProtectedRoute>
</ProtectedRoute> <SequencerPage />
} </ProtectedRoute>
/> }
<Route />
path="/account" <Route
element={ path="/schedulers"
<ProtectedRoute> element={
<AccountPage /> <ProtectedRoute>
</ProtectedRoute> <SchedulersPage />
} </ProtectedRoute>
/> }
<Route />
path="/admin/users" <Route
element={ path="/account"
<AdminRoute> element={
<UsersPage /> <ProtectedRoute>
</AdminRoute> <AccountPage />
} </ProtectedRoute>
/> }
<Route />
path="/admin/settings" <Route
element={ path="/admin/users"
<AdminRoute> element={
<SettingsPage /> <AdminRoute>
</AdminRoute> <UsersPage />
} </AdminRoute>
/> }
</Routes> />
<Route
path="/admin/settings"
element={
<AdminRoute>
<SettingsPage />
</AdminRoute>
}
/>
</Routes>
</Suspense>
) )
} }
@@ -163,7 +189,7 @@ function App() {
<AuthProvider> <AuthProvider>
<SocketProvider> <SocketProvider>
<AppRoutes /> <AppRoutes />
<Toaster richColors position='top-right' /> <Toaster richColors position='top-center' />
</SocketProvider> </SocketProvider>
</AuthProvider> </AuthProvider>
</ThemeProvider> </ThemeProvider>

View File

@@ -12,8 +12,9 @@ import {
SidebarProvider, SidebarProvider,
SidebarTrigger, SidebarTrigger,
} from '@/components/ui/sidebar' } from '@/components/ui/sidebar'
import { useState } from 'react' import { useEffect, useState } from 'react'
import { AppSidebar } from './AppSidebar' import { AppSidebar } from './AppSidebar'
import { GlobalSearch } from './GlobalSearch'
import { Player, type PlayerDisplayMode } from './player/Player' import { Player, type PlayerDisplayMode } from './player/Player'
interface AppLayoutProps { interface AppLayoutProps {
@@ -43,11 +44,29 @@ export function AppLayout({ children, breadcrumb }: AppLayoutProps) {
}, },
) )
const [isSearchOpen, setIsSearchOpen] = useState(false)
// Handle keyboard shortcut for global search
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault()
setIsSearchOpen(true)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
// Note: localStorage is managed by the Player component // Note: localStorage is managed by the Player component
return ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar showCompactPlayer={playerDisplayMode === 'sidebar'} /> <AppSidebar
showCompactPlayer={playerDisplayMode === 'sidebar'}
onSearchClick={() => setIsSearchOpen(true)}
/>
<SidebarInset> <SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2"> <header className="flex h-16 shrink-0 items-center gap-2">
<div className="flex items-center gap-2 px-4"> <div className="flex items-center gap-2 px-4">
@@ -80,6 +99,7 @@ export function AppLayout({ children, breadcrumb }: AppLayoutProps) {
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">{children}</div> <div className="flex flex-1 flex-col gap-4 p-4 pt-0">{children}</div>
</SidebarInset> </SidebarInset>
<Player onPlayerModeChange={setPlayerDisplayMode} /> <Player onPlayerModeChange={setPlayerDisplayMode} />
<GlobalSearch isOpen={isSearchOpen} onClose={() => setIsSearchOpen(false)} />
</SidebarProvider> </SidebarProvider>
) )
} }

View File

@@ -1,3 +1,4 @@
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { import {
Sidebar, Sidebar,
@@ -16,6 +17,8 @@ import {
Settings, Settings,
Users, Users,
AudioLines, AudioLines,
Mic,
Search,
} from 'lucide-react' } from 'lucide-react'
import { CreditsNav } from './nav/CreditsNav' import { CreditsNav } from './nav/CreditsNav'
import { NavGroup } from './nav/NavGroup' import { NavGroup } from './nav/NavGroup'
@@ -26,9 +29,10 @@ import { CompactPlayer } from './player/CompactPlayer'
interface AppSidebarProps { interface AppSidebarProps {
showCompactPlayer?: boolean showCompactPlayer?: boolean
onSearchClick?: () => void
} }
export function AppSidebar({ showCompactPlayer = false }: AppSidebarProps) { export function AppSidebar({ showCompactPlayer = false, onSearchClick }: AppSidebarProps) {
const { user, logout } = useAuth() const { user, logout } = useAuth()
if (!user) return null if (!user) return null
@@ -45,13 +49,28 @@ export function AppSidebar({ showCompactPlayer = false }: AppSidebarProps) {
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<div className="px-2 py-1">
<Button
variant="outline"
className="w-full justify-start gap-2 group-data-[collapsible=icon]:justify-center"
onClick={onSearchClick}
>
<Search className="h-4 w-4" />
<span className="group-data-[collapsible=icon]:hidden">Search</span>
<kbd className="ml-auto pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100 group-data-[collapsible=icon]:hidden">
<span className="text-xs"></span>F
</kbd>
</Button>
</div>
<Separator className="my-2" />
<NavGroup label="Application"> <NavGroup label="Application">
<NavItem href="/" icon={Home} title="Dashboard" /> <NavItem href="/" icon={Home} title="Dashboard" />
<NavItem href="/sounds" icon={Music} title="Sounds" /> <NavItem href="/sounds" icon={Music} title="Sounds" />
<NavItem href="/playlists" icon={PlayCircle} title="Playlists" /> <NavItem href="/playlists" icon={PlayCircle} title="Playlists" />
<NavItem href="/sequencer" icon={AudioLines} title="Sequencer" />
<NavItem href="/extractions" icon={Download} title="Extractions" /> <NavItem href="/extractions" icon={Download} title="Extractions" />
<NavItem href="/tts" icon={Mic} title="Text to Speech" />
<NavItem href="/schedulers" icon={CalendarClock} title="Schedulers" /> <NavItem href="/schedulers" icon={CalendarClock} title="Schedulers" />
<NavItem href="/sequencer" icon={AudioLines} title="Sequencer (WIP)" />
</NavGroup> </NavGroup>
{user.role === 'admin' && ( {user.role === 'admin' && (

View File

@@ -0,0 +1,402 @@
import { useEffect, useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { filesService } from '@/lib/api/services/files'
import { playerService } from '@/lib/api/services/player'
import { playlistsService } from '@/lib/api/services/playlists'
import { soundsService, type Sound } from '@/lib/api/services/sounds'
import { formatDuration } from '@/utils/format-duration'
import { Music, PlayCircle, Search, X } from 'lucide-react'
import { toast } from 'sonner'
interface GlobalSearchProps {
isOpen: boolean
onClose: () => void
}
type SearchResultType = 'sound' | 'playlist' | 'playlist-track'
interface SearchResult {
id: string
type: SearchResultType
title: string
subtitle?: string
duration?: number
thumbnail?: string
soundType?: 'SDB' | 'TTS' | 'EXT'
playlistId?: number
trackIndex?: number
}
export function GlobalSearch({ isOpen, onClose }: GlobalSearchProps) {
const [searchQuery, setSearchQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [loading, setLoading] = useState(false)
const [currentPlaylistTracks, setCurrentPlaylistTracks] = useState<Sound[]>([])
useEffect(() => {
if (!isOpen) {
setSearchQuery('')
setResults([])
setCurrentPlaylistTracks([])
} else {
// Load current playlist tracks when opening
loadCurrentPlaylistTracks()
}
}, [isOpen])
useEffect(() => {
if (searchQuery.trim()) {
performSearch()
} else {
setResults([])
}
}, [searchQuery])
const loadCurrentPlaylistTracks = async () => {
try {
const state = await playerService.getState()
if (state.playlist) {
setCurrentPlaylistTracks(state.playlist.sounds as unknown as Sound[])
}
} catch (error) {
console.error('Failed to load current playlist tracks:', error)
}
}
const performSearch = async () => {
setLoading(true)
try {
const query = searchQuery.trim().toLowerCase()
const newResults: SearchResult[] = []
// Search sounds (SDB and TTS)
const sounds = await soundsService.getSounds({
search: query,
types: ['SDB', 'TTS'],
limit: 20,
})
sounds.forEach((sound) => {
newResults.push({
id: `sound-${sound.id}`,
type: 'sound',
title: sound.name,
subtitle: sound.type,
duration: sound.duration,
thumbnail: sound.thumbnail,
soundType: sound.type,
})
})
// Search playlists
const playlistsResponse = await playlistsService.getPlaylists({
search: query,
limit: 10,
})
playlistsResponse.playlists.forEach((playlist) => {
newResults.push({
id: `playlist-${playlist.id}`,
type: 'playlist',
title: playlist.name,
subtitle: `${playlist.sound_count} tracks`,
duration: playlist.total_duration,
playlistId: playlist.id,
})
})
// Search current playlist tracks
currentPlaylistTracks.forEach((track, index) => {
if (track.name.toLowerCase().includes(query)) {
newResults.push({
id: `track-${track.id}-${index}`,
type: 'playlist-track',
title: track.name,
subtitle: 'Current playlist',
duration: track.duration,
thumbnail: track.thumbnail,
trackIndex: index,
})
}
})
setResults(newResults)
} catch (error) {
console.error('Search failed:', error)
toast.error('Search failed')
} finally {
setLoading(false)
}
}
const handleResultClick = async (result: SearchResult) => {
try {
if (result.type === 'sound') {
const soundId = parseInt(result.id.replace('sound-', ''))
await soundsService.playSound(soundId)
toast.success(`Playing ${result.title}`)
} else if (result.type === 'playlist' && result.playlistId) {
await playlistsService.setCurrentPlaylist(result.playlistId)
toast.success(`Set ${result.title} as current playlist`)
} else if (result.type === 'playlist-track' && result.trackIndex !== undefined) {
await playerService.playAtIndex(result.trackIndex)
toast.success(`Playing ${result.title}`)
}
onClose()
} catch (error) {
console.error('Action failed:', error)
toast.error('Action failed')
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
onClose()
}
}
// Handle global escape key
useEffect(() => {
if (!isOpen) return
const handleGlobalKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose()
}
}
window.addEventListener('keydown', handleGlobalKeyDown)
return () => window.removeEventListener('keydown', handleGlobalKeyDown)
}, [isOpen, onClose])
if (!isOpen) return null
return (
<div
className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50"
onClick={onClose}
>
<div
className="fixed top-[20%] left-1/2 -translate-x-1/2 w-full max-w-2xl bg-background border rounded-lg shadow-lg"
onClick={(e) => e.stopPropagation()}
>
{/* Search Input */}
<div className="relative p-4 border-b">
<Search className="absolute left-6 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search sounds, playlists, or tracks..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleKeyDown}
className="pl-10 pr-10"
autoFocus
/>
<Button
variant="ghost"
size="icon"
className="absolute right-4 top-1/2 -translate-y-1/2"
onClick={onClose}
>
<X className="h-4 w-4" />
</Button>
</div>
{/* Results */}
<ScrollArea className="h-[60vh]">
<div className="p-2">
{loading && (
<div className="p-4 text-center text-muted-foreground">
Searching...
</div>
)}
{!loading && searchQuery && results.length === 0 && (
<div className="p-4 text-center text-muted-foreground">
No results found
</div>
)}
{!loading && results.length > 0 && (
<div className="space-y-3">
{/* Sounds Section */}
{results.filter((r) => r.type === 'sound').length > 0 && (
<div>
<div className="px-2 py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Sounds ({results.filter((r) => r.type === 'sound').length})
</div>
<div className="space-y-1">
{results
.filter((r) => r.type === 'sound')
.map((result) => (
<button
key={result.id}
className="w-full flex items-center gap-3 p-2 rounded hover:bg-muted/50 text-left transition-colors"
onClick={() => handleResultClick(result)}
>
<div className="flex-shrink-0">
{result.thumbnail ? (
<div className="w-10 h-10 rounded bg-muted overflow-hidden">
<img
src={filesService.getThumbnailUrl(
parseInt(result.id.split('-')[1])
)}
alt=""
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="w-10 h-10 rounded bg-muted flex items-center justify-center">
<Music className="h-5 w-5 text-muted-foreground" />
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{result.title}</div>
{result.subtitle && (
<div className="text-sm text-muted-foreground truncate">
{result.subtitle}
</div>
)}
</div>
<div className="flex-shrink-0 flex items-center gap-2">
{result.soundType && (
<Badge
variant={
result.soundType === 'SDB' ? 'default' : 'secondary'
}
className="text-xs"
>
{result.soundType}
</Badge>
)}
{result.duration && (
<span className="text-sm text-muted-foreground">
{formatDuration(result.duration)}
</span>
)}
</div>
</button>
))}
</div>
</div>
)}
{/* Playlists Section */}
{results.filter((r) => r.type === 'playlist').length > 0 && (
<div>
<div className="px-2 py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Playlists ({results.filter((r) => r.type === 'playlist').length})
</div>
<div className="space-y-1">
{results
.filter((r) => r.type === 'playlist')
.map((result) => (
<button
key={result.id}
className="w-full flex items-center gap-3 p-2 rounded hover:bg-muted/50 text-left transition-colors"
onClick={() => handleResultClick(result)}
>
<div className="flex-shrink-0">
<div className="w-10 h-10 rounded bg-muted flex items-center justify-center">
<PlayCircle className="h-5 w-5 text-muted-foreground" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{result.title}</div>
{result.subtitle && (
<div className="text-sm text-muted-foreground truncate">
{result.subtitle}
</div>
)}
</div>
<div className="flex-shrink-0 flex items-center gap-2">
{result.duration && (
<span className="text-sm text-muted-foreground">
{formatDuration(result.duration)}
</span>
)}
</div>
</button>
))}
</div>
</div>
)}
{/* Playlist Tracks Section */}
{results.filter((r) => r.type === 'playlist-track').length > 0 && (
<div>
<div className="px-2 py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Current Playlist ({results.filter((r) => r.type === 'playlist-track').length})
</div>
<div className="space-y-1">
{results
.filter((r) => r.type === 'playlist-track')
.map((result) => (
<button
key={result.id}
className="w-full flex items-center gap-3 p-2 rounded hover:bg-muted/50 text-left transition-colors"
onClick={() => handleResultClick(result)}
>
<div className="flex-shrink-0">
{result.thumbnail ? (
<div className="w-10 h-10 rounded bg-muted overflow-hidden">
<img
src={filesService.getThumbnailUrl(
parseInt(result.id.split('-')[1])
)}
alt=""
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="w-10 h-10 rounded bg-muted flex items-center justify-center">
<Music className="h-5 w-5 text-muted-foreground" />
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{result.title}</div>
{result.subtitle && (
<div className="text-sm text-muted-foreground truncate">
{result.subtitle}
</div>
)}
</div>
<div className="flex-shrink-0 flex items-center gap-2">
{result.duration && (
<span className="text-sm text-muted-foreground">
{formatDuration(result.duration)}
</span>
)}
</div>
</button>
))}
</div>
</div>
)}
</div>
)}
{!searchQuery && (
<div className="p-4 text-center text-muted-foreground text-sm">
Type to search sounds, playlists, or tracks
</div>
)}
</div>
</ScrollArea>
{/* Footer */}
<div className="p-2 border-t bg-muted/30">
<div className="flex items-center justify-between text-xs text-muted-foreground px-2">
<span>Press ESC to close</span>
{results.length > 0 && <span>{results.length} results</span>}
</div>
</div>
</div>
</div>
)
}

View File

@@ -2,7 +2,7 @@ import { StatisticCard } from '@/components/dashboard/StatisticCard'
import { NumberFlowDuration } from '@/components/ui/number-flow-duration' import { NumberFlowDuration } from '@/components/ui/number-flow-duration'
import { NumberFlowSize } from '@/components/ui/number-flow-size' import { NumberFlowSize } from '@/components/ui/number-flow-size'
import NumberFlow from '@number-flow/react' import NumberFlow from '@number-flow/react'
import { Clock, HardDrive, Music, Play, Volume2 } from 'lucide-react' import { Clock, HardDrive, Music, Play, Volume2, MessageSquare } from 'lucide-react'
interface SoundboardStatistics { interface SoundboardStatistics {
sound_count: number sound_count: number
@@ -18,12 +18,20 @@ interface TrackStatistics {
total_size: number total_size: number
} }
interface TTSStatistics {
sound_count: number
total_play_count: number
total_duration: number
total_size: number
}
interface StatisticsGridProps { interface StatisticsGridProps {
soundboardStatistics: SoundboardStatistics soundboardStatistics: SoundboardStatistics
trackStatistics: TrackStatistics trackStatistics: TrackStatistics
ttsStatistics: TTSStatistics
} }
export function StatisticsGrid({ soundboardStatistics, trackStatistics }: StatisticsGridProps) { export function StatisticsGrid({ soundboardStatistics, trackStatistics, ttsStatistics }: StatisticsGridProps) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
@@ -109,6 +117,48 @@ export function StatisticsGrid({ soundboardStatistics, trackStatistics }: Statis
/> />
</div> </div>
</div> </div>
<div>
<h2 className="text-lg font-semibold mb-3 text-muted-foreground">
TTS Statistics
</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatisticCard
title="Total TTS"
icon={MessageSquare}
value={<NumberFlow value={ttsStatistics.sound_count} />}
description="Text-to-speech audio files"
/>
<StatisticCard
title="Total Plays"
icon={Play}
value={<NumberFlow value={ttsStatistics.total_play_count} />}
description="All-time play count"
/>
<StatisticCard
title="Total Duration"
icon={Clock}
value={
<NumberFlowDuration
duration={ttsStatistics.total_duration}
variant="wordy"
/>
}
description="Combined TTS duration"
/>
<StatisticCard
title="Total Size"
icon={HardDrive}
value={
<NumberFlowSize
size={ttsStatistics.total_size}
binary={true}
/>
}
description="Original + normalized files"
/>
</div>
</div>
</div> </div>
) )
} }

View File

@@ -0,0 +1,162 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import NumberFlow from '@number-flow/react'
import { Loader2, Trophy, User } from 'lucide-react'
interface TopUser {
id: number
name: string
count: number
}
interface TopUsersSectionProps {
topUsers: TopUser[]
loading: boolean
metricType: string
period: string
limit: number
onMetricTypeChange: (value: string) => void
onPeriodChange: (value: string) => void
onLimitChange: (value: number) => void
}
const metricTypeLabels = {
sounds_played: 'Sounds Played',
credits_used: 'Credits Used',
tracks_added: 'Tracks Added',
tts_added: 'TTS Added',
playlists_created: 'Playlists Created',
}
const metricTypeUnits = {
sounds_played: 'plays',
credits_used: 'credits',
tracks_added: 'tracks',
tts_added: 'TTS',
playlists_created: 'playlists',
}
export function TopUsersSection({
topUsers,
loading,
metricType,
period,
limit,
onMetricTypeChange,
onPeriodChange,
onLimitChange,
}: TopUsersSectionProps) {
return (
<div className="mt-8">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Trophy className="h-5 w-5" />
<CardTitle>Top Users</CardTitle>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Metric:</span>
<Select value={metricType} onValueChange={onMetricTypeChange}>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="credits_used">Credits Used</SelectItem>
<SelectItem value="playlists_created">Playlists Created</SelectItem>
<SelectItem value="sounds_played">Sounds Played</SelectItem>
<SelectItem value="tracks_added">Tracks Added</SelectItem>
<SelectItem value="tts_added">TTS Added</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Period:</span>
<Select value={period} onValueChange={onPeriodChange}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="today">Today</SelectItem>
<SelectItem value="1_day">1 Day</SelectItem>
<SelectItem value="1_week">1 Week</SelectItem>
<SelectItem value="1_month">1 Month</SelectItem>
<SelectItem value="1_year">1 Year</SelectItem>
<SelectItem value="all_time">All Time</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Count:</span>
<Select
value={limit.toString()}
onValueChange={value => onLimitChange(parseInt(value))}
>
<SelectTrigger className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="25">25</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
Loading top users...
</div>
) : topUsers.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<User className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>No users found for the selected criteria</p>
</div>
) : (
<div className="space-y-3">
{topUsers.map((user, index) => (
<div
key={user.id}
className="flex items-center gap-4 p-3 bg-muted/30 rounded-lg"
>
<div className="flex items-center justify-center w-8 h-8 bg-primary text-primary-foreground rounded-full font-bold text-sm">
{index + 1}
</div>
<User className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{user.name}</div>
<div className="text-xs text-muted-foreground mt-1">
<span className="px-1.5 py-0.5 bg-secondary rounded text-xs">
{metricTypeLabels[metricType as keyof typeof metricTypeLabels]}
</span>
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-primary">
<NumberFlow value={user.count} />
</div>
<div className="text-xs text-muted-foreground">
{metricTypeUnits[metricType as keyof typeof metricTypeUnits]}
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
)
}

View File

@@ -93,11 +93,28 @@ export function ExtractionsHeader({
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Select
value={statusFilter}
onValueChange={value => onStatusFilterChange(value as ExtractionStatus | 'all')}
>
<SelectTrigger className="w-[160px]">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="processing">Processing</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="failed">Failed</SelectItem>
</SelectContent>
</Select>
<Select <Select
value={sortBy} value={sortBy}
onValueChange={value => onSortByChange(value as ExtractionSortField)} onValueChange={value => onSortByChange(value as ExtractionSortField)}
> >
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[150px]">
<SelectValue placeholder="Sort by" /> <SelectValue placeholder="Sort by" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -122,23 +139,6 @@ export function ExtractionsHeader({
)} )}
</Button> </Button>
<Select
value={statusFilter}
onValueChange={value => onStatusFilterChange(value as ExtractionStatus | 'all')}
>
<SelectTrigger className="w-[120px]">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="processing">Processing</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="failed">Failed</SelectItem>
</SelectContent>
</Select>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"

View File

@@ -99,6 +99,7 @@ export function ExtractionsRow({ extraction, onExtractionDeleted }: ExtractionsR
tiktok: 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300', tiktok: 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300',
twitter: 'bg-sky-100 text-sky-800 dark:bg-sky-900 dark:text-sky-300', twitter: 'bg-sky-100 text-sky-800 dark:bg-sky-900 dark:text-sky-300',
instagram: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300', instagram: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300',
dailymotion: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300',
} }
const colorClass = const colorClass =
@@ -117,10 +118,10 @@ export function ExtractionsRow({ extraction, onExtractionDeleted }: ExtractionsR
<TableRow className="hover:bg-muted/50"> <TableRow className="hover:bg-muted/50">
<TableCell> <TableCell>
<div className="min-w-0"> <div className="min-w-0">
<div className="font-medium truncate"> <div className="font-medium truncate max-w-80">
{extraction.title || 'Extracting...'} {extraction.title || 'Processing...'}
</div> </div>
<div className="text-sm text-muted-foreground truncate max-w-64"> <div className="text-sm text-muted-foreground">
{extraction.url} {extraction.url}
</div> </div>
</div> </div>

View File

@@ -31,7 +31,7 @@ export function StopSoundsButton() {
onClick={handleStopSounds} onClick={handleStopSounds}
disabled={isLoading} disabled={isLoading}
tooltip={tooltipText} tooltip={tooltipText}
className="group-data-[collapsible=icon]:justify-center text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950/50" className="group-data-[collapsible=icon]:justify-center text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950/50 cursor-pointer disabled:cursor-not-allowed"
> >
<Square className="h-5 w-5 fill-current" /> <Square className="h-5 w-5 fill-current" />
<span className="font-semibold group-data-[collapsible=icon]:hidden"> <span className="font-semibold group-data-[collapsible=icon]:hidden">

View File

@@ -1,6 +1,3 @@
import { Button } from '@/components/ui/button'
import { Progress } from '@/components/ui/progress'
import { filesService } from '@/lib/api/services/files'
import { import {
type MessageResponse, type MessageResponse,
type PlayerState, type PlayerState,
@@ -8,19 +5,11 @@ import {
} from '@/lib/api/services/player' } from '@/lib/api/services/player'
import { PLAYER_EVENTS, playerEvents } from '@/lib/events' import { PLAYER_EVENTS, playerEvents } from '@/lib/events'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import {
Maximize2,
Music,
Pause,
Play,
SkipBack,
SkipForward,
Volume2,
VolumeX,
} from 'lucide-react'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { NumberFlowDuration } from '../ui/number-flow-duration' import { CompactPlayerControls } from './CompactPlayerControls'
import { CompactPlayerProgress } from './CompactPlayerProgress'
import { CompactPlayerTrackInfo } from './CompactPlayerTrackInfo'
interface CompactPlayerProps { interface CompactPlayerProps {
className?: string className?: string
@@ -33,6 +22,7 @@ export function CompactPlayer({ className }: CompactPlayerProps) {
volume: 80, volume: 80,
previous_volume: 80, previous_volume: 80,
position: 0, position: 0,
play_next_queue: [],
}) })
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
@@ -107,146 +97,50 @@ export function CompactPlayer({ className }: CompactPlayerProps) {
} }
}, [state.volume, executeAction]) }, [state.volume, executeAction])
// // Don't show if no current sound const handleExpand = useCallback(() => {
// if (!state.current_sound) { const expandFn = (
// return null window as unknown as { __expandPlayerFromSidebar?: () => void }
// } ).__expandPlayerFromSidebar
if (expandFn) expandFn()
}, [])
return ( return (
<div className={cn('w-full', className)}> <div className={cn('w-full', className)}>
{/* Collapsed state - only play/pause button */} {/* Collapsed state - only play/pause button */}
<div className="group-data-[collapsible=icon]:flex group-data-[collapsible=icon]:justify-center hidden"> <CompactPlayerControls
<Button status={state.status}
size="sm" volume={state.volume}
variant="ghost" isLoading={isLoading}
onClick={handlePlayPause} onPlayPause={handlePlayPause}
disabled={isLoading} onPrevious={handlePrevious}
className="h-8 w-8 p-0" onNext={handleNext}
title={state.status === 'playing' ? 'Pause' : 'Play'} onVolumeToggle={handleVolumeToggle}
> variant="collapsed"
{state.status === 'playing' ? ( />
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
</Button>
</div>
{/* Expanded state - full player */} {/* Expanded state - full player */}
<div className="group-data-[collapsible=icon]:hidden"> <div className="group-data-[collapsible=icon]:hidden">
{/* Track Info */} <CompactPlayerTrackInfo
<div className="flex items-center gap-2 mb-3 px-1"> currentSound={state.current_sound}
<div className="flex-shrink-0 w-8 h-8 bg-muted rounded flex items-center justify-center overflow-hidden"> playlistName={state.playlist?.name}
{state.current_sound?.thumbnail ? ( onExpand={handleExpand}
<img />
src={filesService.getThumbnailUrl(state.current_sound.id)}
alt={state.current_sound.name}
className="w-full h-full object-cover"
onError={e => {
// Hide image and show music icon if thumbnail fails to load
const target = e.target as HTMLImageElement
target.style.display = 'none'
const musicIcon = target.nextElementSibling as HTMLElement
if (musicIcon) musicIcon.style.display = 'block'
}}
/>
) : null}
<Music
className={cn(
'h-4 w-4 text-muted-foreground',
state.current_sound?.thumbnail ? 'hidden' : 'block',
)}
/>
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{state.current_sound?.name || 'No track selected'}
</div>
<div className="text-xs text-muted-foreground">
{state.playlist?.name}
</div>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => {
const expandFn = (
window as unknown as { __expandPlayerFromSidebar?: () => void }
).__expandPlayerFromSidebar
if (expandFn) expandFn()
}}
className="h-6 w-6 p-0 flex-shrink-0"
title="Expand Player"
>
<Maximize2 className="h-3 w-3" />
</Button>
</div>
{/* Progress Bar */} <CompactPlayerProgress
<div className="mb-3"> position={state.position}
<Progress duration={state.duration || 0}
value={(state.position / (state.duration || 1)) * 100} />
className="w-full h-1"
/>
<div className="flex justify-between text-xs text-muted-foreground mt-1">
<span><NumberFlowDuration duration={state.position} /></span>
<span><NumberFlowDuration duration={state.duration || 0} /></span>
</div>
</div>
{/* Controls */} <CompactPlayerControls
<div className="flex items-center justify-between gap-1"> status={state.status}
<Button volume={state.volume}
size="sm" isLoading={isLoading}
variant="ghost" onPlayPause={handlePlayPause}
onClick={handlePrevious} onPrevious={handlePrevious}
disabled={isLoading} onNext={handleNext}
className="h-7 w-7 p-0" onVolumeToggle={handleVolumeToggle}
title="Previous" variant="expanded"
> />
<SkipBack className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={handlePlayPause}
disabled={isLoading}
className="h-8 w-8 p-0"
title={state.status === 'playing' ? 'Pause' : 'Play'}
>
{state.status === 'playing' ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
</Button>
<Button
size="sm"
variant="ghost"
onClick={handleNext}
disabled={isLoading}
className="h-7 w-7 p-0"
title="Next"
>
<SkipForward className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={handleVolumeToggle}
className="h-7 w-7 p-0"
title={state.volume === 0 ? 'Unmute' : 'Mute'}
>
{state.volume === 0 ? (
<VolumeX className="h-3 w-3" />
) : (
<Volume2 className="h-3 w-3" />
)}
</Button>
</div>
</div> </div>
</div> </div>
) )

View File

@@ -0,0 +1,128 @@
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { useSidebar } from '@/components/ui/sidebar'
import type { PlayerState } from '@/lib/api/services/player'
import {
Pause,
Play,
SkipBack,
SkipForward,
Volume2,
VolumeX,
} from 'lucide-react'
import { memo } from 'react'
interface CompactPlayerControlsProps {
status: PlayerState['status']
volume: number
isLoading: boolean
onPlayPause: () => void
onPrevious: () => void
onNext: () => void
onVolumeToggle: () => void
variant?: 'collapsed' | 'expanded'
}
export const CompactPlayerControls = memo(function CompactPlayerControls({
status,
volume,
isLoading,
onPlayPause,
onPrevious,
onNext,
onVolumeToggle,
variant = 'expanded',
}: CompactPlayerControlsProps) {
const { isMobile, state: sidebarState } = useSidebar()
if (variant === 'collapsed') {
return (
<div className="group-data-[collapsible=icon]:flex group-data-[collapsible=icon]:justify-center hidden">
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="ghost"
onClick={onPlayPause}
disabled={isLoading}
className="h-8 w-8 p-0"
>
{status === 'playing' ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={sidebarState !== "collapsed" || isMobile}
>
{status === 'playing' ? 'Pause' : 'Play'}
</TooltipContent>
</Tooltip>
</div>
)
}
// Expanded variant
return (
<div className="flex items-center justify-between gap-1">
<Button
size="sm"
variant="ghost"
onClick={onPrevious}
disabled={isLoading}
className="h-7 w-7 p-0"
title="Previous"
>
<SkipBack className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={onPlayPause}
disabled={isLoading}
className="h-8 w-8 p-0"
title={status === 'playing' ? 'Pause' : 'Play'}
>
{status === 'playing' ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
</Button>
<Button
size="sm"
variant="ghost"
onClick={onNext}
disabled={isLoading}
className="h-7 w-7 p-0"
title="Next"
>
<SkipForward className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={onVolumeToggle}
className="h-7 w-7 p-0"
title={volume === 0 ? 'Unmute' : 'Mute'}
>
{volume === 0 ? (
<VolumeX className="h-3 w-3" />
) : (
<Volume2 className="h-3 w-3" />
)}
</Button>
</div>
)
})

View File

@@ -0,0 +1,31 @@
import { Progress } from '@/components/ui/progress'
import { memo, useMemo } from 'react'
import { NumberFlowDuration } from '../ui/number-flow-duration'
interface CompactPlayerProgressProps {
position: number
duration: number
}
export const CompactPlayerProgress = memo(function CompactPlayerProgress({
position,
duration,
}: CompactPlayerProgressProps) {
const progressPercentage = useMemo(() =>
(position / (duration || 1)) * 100,
[position, duration]
)
return (
<div className="mb-3">
<Progress
value={progressPercentage}
className="w-full h-1"
/>
<div className="flex justify-between text-xs text-muted-foreground mt-1">
<span><NumberFlowDuration duration={position} /></span>
<span><NumberFlowDuration duration={duration || 0} /></span>
</div>
</div>
)
})

View File

@@ -0,0 +1,62 @@
import { Button } from '@/components/ui/button'
import { filesService } from '@/lib/api/services/files'
import type { PlayerState } from '@/lib/api/services/player'
import { cn } from '@/lib/utils'
import { Maximize2, Music } from 'lucide-react'
import { memo } from 'react'
interface CompactPlayerTrackInfoProps {
currentSound: PlayerState['current_sound']
playlistName?: string
onExpand: () => void
}
export const CompactPlayerTrackInfo = memo(function CompactPlayerTrackInfo({
currentSound,
playlistName,
onExpand,
}: CompactPlayerTrackInfoProps) {
return (
<div className="flex items-center gap-2 mb-3 px-1">
<div className="flex-shrink-0 w-8 h-8 bg-muted rounded flex items-center justify-center overflow-hidden">
{currentSound?.thumbnail ? (
<img
src={filesService.getThumbnailUrl(currentSound.id)}
alt={currentSound.name}
className="w-full h-full object-cover"
onError={e => {
// Hide image and show music icon if thumbnail fails to load
const target = e.target as HTMLImageElement
target.style.display = 'none'
const musicIcon = target.nextElementSibling as HTMLElement
if (musicIcon) musicIcon.style.display = 'block'
}}
/>
) : null}
<Music
className={cn(
'h-4 w-4 text-muted-foreground',
currentSound?.thumbnail ? 'hidden' : 'block',
)}
/>
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{currentSound?.name || 'No track selected'}
</div>
<div className="text-xs text-muted-foreground">
{playlistName}
</div>
</div>
<Button
size="sm"
variant="ghost"
onClick={onExpand}
className="h-6 w-6 p-0 flex-shrink-0"
title="Expand Player"
>
<Maximize2 className="h-3 w-3" />
</Button>
</div>
)
})

View File

@@ -0,0 +1,105 @@
import { Badge } from '@/components/ui/badge'
import { ScrollArea } from '@/components/ui/scroll-area'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { filesService } from '@/lib/api/services/files'
import { type PlayerSound } from '@/lib/api/services/player'
import { formatDuration } from '@/utils/format-duration'
import { Music, ListPlus } from 'lucide-react'
interface PlayNextQueueProps {
queue: PlayerSound[]
}
export function PlayNextQueue({ queue }: PlayNextQueueProps) {
if (queue.length === 0) {
return null
}
// Calculate total duration
const totalDuration = queue.reduce((sum, sound) => sum + sound.duration, 0)
return (
<Popover>
<PopoverTrigger asChild>
<div className="flex items-center justify-between py-2 px-2 rounded hover:bg-muted/50 cursor-pointer transition-colors">
<div className="flex items-center gap-2">
<ListPlus className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">Play Next</span>
</div>
<Badge variant="secondary" className="text-xs">
{queue.length} {queue.length === 1 ? 'track' : 'tracks'}
</Badge>
</div>
</PopoverTrigger>
<PopoverContent side="left" align="start" className="w-80 p-0">
<div className="w-full flex flex-col">
{/* Header - Fixed */}
<div className="flex items-center justify-between p-3 border-b">
<h4 className="font-semibold text-sm">Play Next Queue</h4>
<Badge variant="secondary" className="text-xs">
{queue.length}
</Badge>
</div>
{/* Track List - Scrollable */}
<ScrollArea className="h-96">
<div className="w-full space-y-1 p-3 pt-2">
{queue.map((sound, index) => (
<div
key={`${sound.id}-${index}`}
className="grid grid-cols-10 gap-2 items-center py-1.5 px-2 rounded hover:bg-muted/50 text-xs"
>
{/* Queue position - 1 column */}
<div className="col-span-1 flex justify-center">
<span className="text-muted-foreground">{index + 1}</span>
</div>
{/* Thumbnail - 1 column */}
<div className="col-span-1">
<div className="bg-muted rounded flex items-center justify-center overflow-hidden w-5 h-5">
{sound.thumbnail ? (
<img
src={filesService.getThumbnailUrl(sound.id)}
alt=""
className="w-full h-full object-cover"
/>
) : (
<Music className="h-3 w-3 text-muted-foreground" />
)}
</div>
</div>
{/* Track name - 6 columns */}
<div className="col-span-6">
<span className="font-medium truncate block text-xs">
{sound.name}
</span>
</div>
{/* Duration - 2 columns */}
<div className="col-span-2 text-right">
<span className="text-muted-foreground text-xs whitespace-nowrap">
{formatDuration(sound.duration)}
</span>
</div>
</div>
))}
</div>
</ScrollArea>
{/* Footer - Fixed */}
<div className="p-3 pt-2 border-t bg-muted/30">
<div className="flex justify-between items-center text-xs">
<span className="text-muted-foreground">Total playtime</span>
<span className="font-medium">{formatDuration(totalDuration)}</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
)
}

View File

@@ -1,14 +1,5 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from '@/components/ui/card'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Progress } from '@/components/ui/progress'
import { Slider } from '@/components/ui/slider'
import { filesService } from '@/lib/api/services/files' import { filesService } from '@/lib/api/services/files'
import { import {
type MessageResponse, type MessageResponse,
@@ -16,36 +7,74 @@ import {
type PlayerState, type PlayerState,
playerService, playerService,
} from '@/lib/api/services/player' } from '@/lib/api/services/player'
import { soundsService } from '@/lib/api/services/sounds'
import { PLAYER_EVENTS, playerEvents } from '@/lib/events' import { PLAYER_EVENTS, playerEvents } from '@/lib/events'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { formatDuration } from '@/utils/format-duration'
import { import {
ArrowRight,
ArrowRightToLine,
Download,
ExternalLink,
List,
Maximize2, Maximize2,
Minimize2, Minimize2,
MoreVertical,
Music,
Pause,
Play,
Repeat,
Repeat1,
Shuffle,
SkipBack,
SkipForward,
Square, Square,
Volume2,
VolumeX,
} from 'lucide-react' } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Playlist } from './Playlist' import { Playlist } from './Playlist'
import { PlayNextQueue } from './PlayNextQueue'
import { PlayerControls } from './PlayerControls'
import { PlayerProgress } from './PlayerProgress'
import { PlayerTrackInfo } from './PlayerTrackInfo'
export type PlayerDisplayMode = 'normal' | 'minimized' | 'maximized' | 'sidebar' export type PlayerDisplayMode = 'normal' | 'minimized' | 'maximized' | 'sidebar'
// Helper function to deep compare player states to prevent unnecessary re-renders
function isPlayerStateEqual(state1: PlayerState, state2: PlayerState): boolean {
// Quick reference equality check first
if (state1 === state2) return true
// Compare primitive properties
if (
state1.status !== state2.status ||
state1.mode !== state2.mode ||
state1.volume !== state2.volume ||
state1.previous_volume !== state2.previous_volume ||
state1.position !== state2.position ||
state1.duration !== state2.duration ||
state1.index !== state2.index
) {
return false
}
// Compare current_sound object
if (state1.current_sound !== state2.current_sound) {
if (!state1.current_sound || !state2.current_sound) return false
if (
state1.current_sound.id !== state2.current_sound.id ||
state1.current_sound.name !== state2.current_sound.name ||
state1.current_sound.thumbnail !== state2.current_sound.thumbnail ||
state1.current_sound.extract_url !== state2.current_sound.extract_url
) {
return false
}
}
// Compare playlist object (only shallow comparison for performance)
if (state1.playlist !== state2.playlist) {
if (!state1.playlist || !state2.playlist) return false
if (
state1.playlist.id !== state2.playlist.id ||
state1.playlist.sounds.length !== state2.playlist.sounds.length
) {
return false
}
}
// Compare play_next_queue length
if (state1.play_next_queue.length !== state2.play_next_queue.length) {
return false
}
return true
}
interface PlayerProps { interface PlayerProps {
className?: string className?: string
onPlayerModeChange?: (mode: PlayerDisplayMode) => void onPlayerModeChange?: (mode: PlayerDisplayMode) => void
@@ -58,6 +87,7 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
volume: 80, volume: 80,
previous_volume: 80, previous_volume: 80,
position: 0, position: 0,
play_next_queue: [],
}) })
const [displayMode, setDisplayMode] = useState<PlayerDisplayMode>(() => { const [displayMode, setDisplayMode] = useState<PlayerDisplayMode>(() => {
// Initialize from localStorage or default to 'normal' // Initialize from localStorage or default to 'normal'
@@ -84,6 +114,7 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
const [showPlaylist, setShowPlaylist] = useState(false) const [showPlaylist, setShowPlaylist] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
// Load initial state // Load initial state
useEffect(() => { useEffect(() => {
const loadState = async () => { const loadState = async () => {
@@ -97,11 +128,18 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
loadState() loadState()
}, []) }, [])
// Listen for player state updates // Listen for player state updates with optimization
const stateRef = useRef(state)
stateRef.current = state
useEffect(() => { useEffect(() => {
const handlePlayerState = (...args: unknown[]) => { const handlePlayerState = (...args: unknown[]) => {
const newState = args[0] as PlayerState const newState = args[0] as PlayerState
setState(newState)
// Only update state if it actually changed to prevent unnecessary re-renders
if (!isPlayerStateEqual(stateRef.current, newState)) {
setState(newState)
}
} }
playerEvents.on(PLAYER_EVENTS.PLAYER_STATE, handlePlayerState) playerEvents.on(PLAYER_EVENTS.PLAYER_STATE, handlePlayerState)
@@ -216,20 +254,16 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
} }
}, [state.current_sound]) }, [state.current_sound])
const getModeIcon = () => { const handleStopAllSounds = useCallback(async () => {
switch (state.mode) { try {
case 'continuous': await soundsService.stopSounds()
return <ArrowRight className="h-4 w-4" /> toast.success('All sounds stopped')
case 'loop': } catch (error) {
return <Repeat className="h-4 w-4" /> console.error('Failed to stop all sounds:', error)
case 'loop_one': toast.error('Failed to stop all sounds')
return <Repeat1 className="h-4 w-4" />
case 'random':
return <Shuffle className="h-4 w-4" />
default:
return <ArrowRightToLine className="h-4 w-4" />
} }
} }, [])
const expandFromSidebar = useCallback(() => { const expandFromSidebar = useCallback(() => {
setDisplayMode('normal') setDisplayMode('normal')
@@ -252,46 +286,17 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
<Card className="w-48 bg-background/90 backdrop-blur-sm pt-0 pb-0"> <Card className="w-48 bg-background/90 backdrop-blur-sm pt-0 pb-0">
<CardContent className="p-2"> <CardContent className="p-2">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button <PlayerControls
size="sm" status={state.status}
variant="ghost" mode={state.mode}
onClick={handlePrevious} isLoading={isLoading}
disabled={isLoading} onPlayPause={handlePlayPause}
className="h-8 w-8 p-0" onStop={handleStop}
> onPrevious={handlePrevious}
<SkipBack className="h-4 w-4" /> onNext={handleNext}
</Button> onModeChange={handleModeChange}
<Button variant="minimized"
size="sm" />
variant="ghost"
onClick={handlePlayPause}
disabled={isLoading}
className="h-8 w-8 p-0"
>
{state.status === 'playing' ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
</Button>
<Button
size="sm"
variant="ghost"
onClick={handleStop}
disabled={isLoading}
className="h-8 w-8 p-0"
>
<Square className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={handleNext}
disabled={isLoading}
className="h-8 w-8 p-0"
>
<SkipForward className="h-4 w-4" />
</Button>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -330,180 +335,32 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
</Button> </Button>
</div> </div>
{/* Album Art / Thumbnail */} <PlayerTrackInfo
<div className="mb-4"> currentSound={state.current_sound}
{state.current_sound?.thumbnail ? ( onDownloadSound={handleDownloadSound}
<div className="w-full aspect-auto bg-muted rounded-lg flex items-center justify-center overflow-hidden"> />
<img
src={filesService.getThumbnailUrl(state.current_sound.id)}
alt={state.current_sound.name}
className="w-full h-full object-cover"
onError={e => {
// Hide image and show music icon if thumbnail fails to load
const target = e.target as HTMLImageElement
target.style.display = 'none'
const musicIcon = target.nextElementSibling as HTMLElement
if (musicIcon) musicIcon.style.display = 'block'
}}
/>
<Music
className={cn(
'h-8 w-8 text-muted-foreground',
state.current_sound?.thumbnail ? 'hidden' : 'block',
)}
/>
</div>
) : null}
</div>
{/* Track Info */} <PlayerProgress
<div className="mb-4 text-center"> position={state.position}
<div className="flex items-center justify-center gap-2"> duration={state.duration || 0}
<h3 className="font-medium text-sm truncate"> onSeek={handleSeek}
{state.current_sound?.name || 'No track selected'} />
</h3>
{state.current_sound &&
(state.current_sound.extract_url || state.current_sound.id) && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-4 w-4 p-0">
<MoreVertical className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{state.current_sound.extract_url && (
<DropdownMenuItem asChild>
<a
href={state.current_sound.extract_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<ExternalLink className="h-4 w-4" />
Source
</a>
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={handleDownloadSound}
className="flex items-center gap-2"
>
<Download className="h-4 w-4" />
File
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
{/* Progress Bar */} <PlayerControls
<div className="mb-4"> status={state.status}
<Progress mode={state.mode}
value={(state.position / (state.duration || 1)) * 100} isLoading={isLoading}
className="w-full h-2 cursor-pointer" showPlaylistButton={true}
onClick={e => { volume={state.volume}
const rect = e.currentTarget.getBoundingClientRect() onPlayPause={handlePlayPause}
const clickX = e.clientX - rect.left onStop={handleStop}
const percentage = clickX / rect.width onPrevious={handlePrevious}
const newPosition = Math.round(percentage * (state.duration || 0)) onNext={handleNext}
handleSeek([newPosition]) onModeChange={handleModeChange}
}} onTogglePlaylist={() => setShowPlaylist(!showPlaylist)}
/> onVolumeChange={handleVolumeChange}
<div className="flex justify-between text-xs text-muted-foreground mt-1"> onMute={handleMute}
<span>{formatDuration(state.position)}</span> />
<span>{formatDuration(state.duration || 0)}</span>
</div>
</div>
{/* Main Controls */}
<div className="flex items-center justify-center gap-2 mb-4">
<Button
size="sm"
variant="ghost"
onClick={handleModeChange}
className="h-8 w-8 p-0"
title={`Mode: ${state.mode.replace('_', ' ')}`}
>
{getModeIcon()}
</Button>
<Button
size="sm"
variant="ghost"
onClick={handlePrevious}
disabled={isLoading}
>
<SkipBack className="h-4 w-4" />
</Button>
<Button
size="sm"
onClick={handlePlayPause}
disabled={isLoading}
className="h-10 w-10 rounded-full"
>
{state.status === 'playing' ? (
<Pause className="h-5 w-5" />
) : (
<Play className="h-5 w-5" />
)}
</Button>
<Button
size="sm"
variant="ghost"
onClick={handleStop}
disabled={isLoading}
>
<Square className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={handleNext}
disabled={isLoading}
>
<SkipForward className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => setShowPlaylist(!showPlaylist)}
className="h-8 w-8 p-0"
title="Toggle Playlist"
>
<List className="h-4 w-4" />
</Button>
</div>
{/* Secondary Controls */}
<div className="flex items-center justify-between">
<Badge variant="secondary" className="text-xs">
{state.mode.replace('_', ' ')}
</Badge>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="ghost"
onClick={handleMute}
className="h-8 w-8 p-0"
>
{state.volume === 0 ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
<div className="w-16">
<Slider
value={[state.volume]}
max={100}
step={1}
onValueChange={handleVolumeChange}
className="w-full"
/>
</div>
</div>
</div>
{/* Playlist */} {/* Playlist */}
{showPlaylist && state.playlist && ( {showPlaylist && state.playlist && (
@@ -518,6 +375,13 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
) )
} }
/> />
{/* Play Next Queue */}
{state.play_next_queue.length > 0 && (
<div className="mt-2 border-t">
<PlayNextQueue queue={state.play_next_queue} />
</div>
)}
</div> </div>
)} )}
</CardContent> </CardContent>
@@ -529,188 +393,69 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
{/* Header */} {/* Header */}
<div className="p-4 border-b flex items-center justify-between"> <div className="p-4 border-b flex items-center justify-between">
<h2 className="text-lg font-semibold">Now Playing</h2> <h2 className="text-lg font-semibold">Now Playing</h2>
<Button <div className="flex items-center gap-2">
size="sm" <Button
variant="ghost" size="sm"
onClick={() => setDisplayMode('normal')} variant="ghost"
> onClick={handleStopAllSounds}
<Minimize2 className="h-4 w-4 mr-2" /> className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950/50"
Exit Fullscreen >
</Button> <Square className="h-4 w-4 mr-2 fill-current" />
Stop All Sounds
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => setDisplayMode('normal')}
>
<Minimize2 className="h-4 w-4 mr-2" />
Exit Fullscreen
</Button>
</div>
</div> </div>
<div className="flex-1 flex"> <div className="flex-1 flex">
{/* Main Player Area */} {/* Main Player Area */}
<div className="flex-1 flex flex-col items-center justify-center p-8"> <div className="flex-1 flex flex-col items-center justify-center p-8">
{/* Large Album Art */} <PlayerTrackInfo
<div className="max-w-300 max-h-200 aspect-auto bg-muted rounded-lg flex items-center justify-center overflow-hidden mb-8"> currentSound={state.current_sound}
{state.current_sound?.thumbnail ? ( onDownloadSound={handleDownloadSound}
<img variant="maximized"
src={filesService.getThumbnailUrl(state.current_sound.id)} />
alt={state.current_sound.name}
className="w-full h-full object-cover"
onError={e => {
// Hide image and show music icon if thumbnail fails to load
const target = e.target as HTMLImageElement
target.style.display = 'none'
const musicIcon = target.nextElementSibling as HTMLElement
if (musicIcon) musicIcon.style.display = 'block'
}}
/>
) : null}
<Music
className={cn(
'h-32 w-32 text-muted-foreground',
state.current_sound?.thumbnail ? 'hidden' : 'block',
)}
/>
</div>
{/* Track Info */} <PlayerProgress
<div className="text-center mb-8"> position={state.position}
<div className="flex items-center justify-center gap-3 mb-2"> duration={state.duration || 0}
<h1 className="text-2xl font-bold"> onSeek={handleSeek}
{state.current_sound?.name || 'No track selected'} variant="maximized"
</h1> />
{state.current_sound &&
(state.current_sound.extract_url || state.current_sound.id) && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{state.current_sound.extract_url && (
<DropdownMenuItem asChild>
<a
href={state.current_sound.extract_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<ExternalLink className="h-4 w-4" />
Source
</a>
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={handleDownloadSound}
className="flex items-center gap-2"
>
<Download className="h-4 w-4" />
File
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
{/* Progress Bar */} <PlayerControls
<div className="w-full max-w-md mb-8"> status={state.status}
<Progress mode={state.mode}
value={(state.position / (state.duration || 1)) * 100} isLoading={isLoading}
className="w-full h-3 cursor-pointer" volume={state.volume}
onClick={e => { onPlayPause={handlePlayPause}
const rect = e.currentTarget.getBoundingClientRect() onStop={handleStop}
const clickX = e.clientX - rect.left onPrevious={handlePrevious}
const percentage = clickX / rect.width onNext={handleNext}
const newPosition = Math.round( onModeChange={handleModeChange}
percentage * (state.duration || 0), onVolumeChange={handleVolumeChange}
) onMute={handleMute}
handleSeek([newPosition]) variant="maximized"
}} />
/>
<div className="flex justify-between text-sm text-muted-foreground mt-2">
<span>{formatDuration(state.position)}</span>
<span>{formatDuration(state.duration || 0)}</span>
</div>
</div>
{/* Large Controls */}
<div className="flex items-center gap-4 mb-8">
<Button
size="lg"
variant="ghost"
onClick={handlePrevious}
disabled={isLoading}
>
<SkipBack className="h-6 w-6" />
</Button>
<Button
size="lg"
onClick={handlePlayPause}
disabled={isLoading}
className="h-16 w-16 rounded-full"
>
{state.status === 'playing' ? (
<Pause className="h-8 w-8" />
) : (
<Play className="h-8 w-8" />
)}
</Button>
<Button
size="lg"
variant="ghost"
onClick={handleStop}
disabled={isLoading}
>
<Square className="h-6 w-6" />
</Button>
<Button
size="lg"
variant="ghost"
onClick={handleNext}
disabled={isLoading}
>
<SkipForward className="h-6 w-6" />
</Button>
</div>
{/* Secondary Controls */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<Button size="sm" variant="ghost" onClick={handleModeChange}>
{getModeIcon()}
</Button>
<Badge variant="secondary">{state.mode.replace('_', ' ')}</Badge>
</div>
<div className="flex items-center gap-2">
<Button size="sm" variant="ghost" onClick={handleMute}>
{state.volume === 0 ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
<div className="w-24">
<Slider
value={[state.volume]}
max={100}
step={1}
onValueChange={handleVolumeChange}
className="w-full"
/>
</div>
<span className="text-sm text-muted-foreground w-8">
{Math.round(state.volume)}%
</span>
</div>
</div>
</div> </div>
{/* Playlist Sidebar */} {/* Playlist Sidebar */}
{state.playlist && ( {state.playlist && (
<div className="w-96 border-l bg-muted/10 backdrop-blur-sm"> <div className="w-96 border-l bg-muted/10 backdrop-blur-sm flex flex-col">
<div className="p-4 border-b"> <div className="p-4 border-b flex-shrink-0">
<h3 className="font-semibold">Playlist</h3> <h3 className="font-semibold">Playlist</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{state.playlist.sounds.length} tracks {state.playlist.sounds.length} tracks
</p> </p>
</div> </div>
<div className="p-4"> <div className="p-4 overflow-y-auto flex-1">
<Playlist <Playlist
playlist={state.playlist} playlist={state.playlist}
currentIndex={state.index} currentIndex={state.index}
@@ -722,6 +467,13 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
} }
variant="maximized" variant="maximized"
/> />
{/* Play Next Queue */}
{state.play_next_queue.length > 0 && (
<div className="mt-2 border-t">
<PlayNextQueue queue={state.play_next_queue} />
</div>
)}
</div> </div>
</div> </div>
)} )}

View File

@@ -0,0 +1,302 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import type { PlayerMode, PlayerState } from '@/lib/api/services/player'
import {
ArrowRight,
ArrowRightToLine,
List,
Pause,
Play,
Repeat,
Repeat1,
Shuffle,
SkipBack,
SkipForward,
Square,
Volume2,
VolumeX,
} from 'lucide-react'
import { memo, useMemo } from 'react'
interface PlayerControlsProps {
status: PlayerState['status']
mode: PlayerMode
isLoading: boolean
showPlaylistButton?: boolean
volume?: number
onPlayPause: () => void
onStop: () => void
onPrevious: () => void
onNext: () => void
onModeChange: () => void
onTogglePlaylist?: () => void
onVolumeChange?: (volume: number[]) => void
onMute?: () => void
variant?: 'normal' | 'maximized' | 'minimized'
}
export const PlayerControls = memo(function PlayerControls({
status,
mode,
isLoading,
showPlaylistButton = false,
volume,
onPlayPause,
onStop,
onPrevious,
onNext,
onModeChange,
onTogglePlaylist,
onVolumeChange,
onMute,
variant = 'normal',
}: PlayerControlsProps) {
const isMinimized = variant === 'minimized'
const isMaximized = variant === 'maximized'
const modeIcon = useMemo(() => {
switch (mode) {
case 'continuous':
return <ArrowRight className="h-4 w-4" />
case 'loop':
return <Repeat className="h-4 w-4" />
case 'loop_one':
return <Repeat1 className="h-4 w-4" />
case 'random':
return <Shuffle className="h-4 w-4" />
default:
return <ArrowRightToLine className="h-4 w-4" />
}
}, [mode])
const modeLabel = useMemo(() =>
mode.replace('_', ' '),
[mode]
)
if (isMinimized) {
return (
<div className="flex items-center gap-1">
<Button
size="sm"
variant="ghost"
onClick={onPrevious}
disabled={isLoading}
className="h-8 w-8 p-0"
>
<SkipBack className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={onPlayPause}
disabled={isLoading}
className="h-8 w-8 p-0"
>
{status === 'playing' ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
</Button>
<Button
size="sm"
variant="ghost"
onClick={onStop}
disabled={isLoading}
className="h-8 w-8 p-0"
>
<Square className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={onNext}
disabled={isLoading}
className="h-8 w-8 p-0"
>
<SkipForward className="h-4 w-4" />
</Button>
</div>
)
}
if (isMaximized) {
return (
<div>
{/* Large Controls */}
<div className="flex items-center gap-4 mb-8">
<Button
size="lg"
variant="ghost"
onClick={onPrevious}
disabled={isLoading}
>
<SkipBack className="h-6 w-6" />
</Button>
<Button
size="lg"
onClick={onPlayPause}
disabled={isLoading}
className="h-16 w-16 rounded-full"
>
{status === 'playing' ? (
<Pause className="h-8 w-8" />
) : (
<Play className="h-8 w-8" />
)}
</Button>
<Button
size="lg"
variant="ghost"
onClick={onStop}
disabled={isLoading}
>
<Square className="h-6 w-6" />
</Button>
<Button
size="lg"
variant="ghost"
onClick={onNext}
disabled={isLoading}
>
<SkipForward className="h-6 w-6" />
</Button>
</div>
{/* Secondary Controls */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<Button size="sm" variant="ghost" onClick={onModeChange}>
{modeIcon}
</Button>
<Badge variant="secondary">{modeLabel}</Badge>
</div>
{volume !== undefined && onVolumeChange && onMute && (
<div className="flex items-center gap-2">
<Button size="sm" variant="ghost" onClick={onMute}>
{volume === 0 ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
<div className="w-24">
<Slider
value={[volume]}
max={100}
step={1}
onValueChange={onVolumeChange}
className="w-full"
/>
</div>
<span className="text-sm text-muted-foreground w-8">
{Math.round(volume)}%
</span>
</div>
)}
</div>
</div>
)
}
// Normal variant
return (
<>
{/* Main Controls */}
<div className="flex items-center justify-center gap-2 mb-4">
<Button
size="sm"
variant="ghost"
onClick={onModeChange}
className="h-8 w-8 p-0"
title={`Mode: ${modeLabel}`}
>
{modeIcon}
</Button>
<Button
size="sm"
variant="ghost"
onClick={onPrevious}
disabled={isLoading}
>
<SkipBack className="h-4 w-4" />
</Button>
<Button
size="sm"
onClick={onPlayPause}
disabled={isLoading}
className="h-10 w-10 rounded-full"
>
{status === 'playing' ? (
<Pause className="h-5 w-5" />
) : (
<Play className="h-5 w-5" />
)}
</Button>
<Button
size="sm"
variant="ghost"
onClick={onStop}
disabled={isLoading}
>
<Square className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={onNext}
disabled={isLoading}
>
<SkipForward className="h-4 w-4" />
</Button>
{showPlaylistButton && onTogglePlaylist && (
<Button
size="sm"
variant="ghost"
onClick={onTogglePlaylist}
className="h-8 w-8 p-0"
title="Toggle Playlist"
>
<List className="h-4 w-4" />
</Button>
)}
</div>
{/* Secondary Controls */}
<div className="flex items-center justify-between">
<Badge variant="secondary" className="text-xs">
{modeLabel}
</Badge>
{volume !== undefined && onVolumeChange && onMute && (
<div className="flex items-center gap-2">
<Button
size="sm"
variant="ghost"
onClick={onMute}
className="h-8 w-8 p-0"
>
{volume === 0 ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
<div className="w-16">
<Slider
value={[volume]}
max={100}
step={1}
onValueChange={onVolumeChange}
className="w-full"
/>
</div>
</div>
)}
</div>
</>
)
})

View File

@@ -0,0 +1,53 @@
import { Progress } from '@/components/ui/progress'
import { cn } from '@/lib/utils'
import { memo, useMemo } from 'react'
import { NumberFlowDuration } from '../ui/number-flow-duration'
interface PlayerProgressProps {
position: number
duration: number
onSeek: (position: number[]) => void
variant?: 'normal' | 'maximized'
}
export const PlayerProgress = memo(function PlayerProgress({
position,
duration,
onSeek,
variant = 'normal',
}: PlayerProgressProps) {
const isMaximized = variant === 'maximized'
const progressPercentage = useMemo(() =>
(position / (duration || 1)) * 100,
[position, duration]
)
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect()
const clickX = e.clientX - rect.left
const percentage = clickX / rect.width
const newPosition = Math.round(percentage * (duration || 0))
onSeek([newPosition])
}
return (
<div className={isMaximized ? 'w-full max-w-md mb-8' : 'mb-4'}>
<Progress
value={progressPercentage}
className={cn(
'w-full cursor-pointer',
isMaximized ? 'h-3' : 'h-2'
)}
onClick={handleProgressClick}
/>
<div className={cn(
'flex justify-between text-muted-foreground mt-1',
isMaximized ? 'text-sm mt-2' : 'text-xs'
)}>
<NumberFlowDuration duration={position} />
<NumberFlowDuration duration={duration || 0} />
</div>
</div>
)
})

View File

@@ -0,0 +1,103 @@
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { filesService } from '@/lib/api/services/files'
import type { PlayerState } from '@/lib/api/services/player'
import { cn } from '@/lib/utils'
import { Download, ExternalLink, MoreVertical, Music } from 'lucide-react'
import { memo } from 'react'
interface PlayerTrackInfoProps {
currentSound: PlayerState['current_sound']
onDownloadSound: () => void
variant?: 'normal' | 'maximized'
}
export const PlayerTrackInfo = memo(function PlayerTrackInfo({
currentSound,
onDownloadSound,
variant = 'normal',
}: PlayerTrackInfoProps) {
const isMaximized = variant === 'maximized'
return (
<>
{/* Album Art / Thumbnail */}
<div className={isMaximized ? 'max-w-300 max-h-200 aspect-auto bg-muted rounded-lg flex items-center justify-center overflow-hidden mb-8' : 'mb-4'}>
{currentSound?.thumbnail ? (
<div className={isMaximized ? 'w-full h-full' : 'w-full aspect-auto bg-muted rounded-lg flex items-center justify-center overflow-hidden'}>
<img
src={filesService.getThumbnailUrl(currentSound.id)}
alt={currentSound.name}
className="w-full h-full object-cover"
onError={e => {
// Hide image and show music icon if thumbnail fails to load
const target = e.target as HTMLImageElement
target.style.display = 'none'
const musicIcon = target.nextElementSibling as HTMLElement
if (musicIcon) musicIcon.style.display = 'block'
}}
/>
<Music
className={cn(
isMaximized ? 'h-32 w-32 text-muted-foreground' : 'h-8 w-8 text-muted-foreground',
currentSound?.thumbnail ? 'hidden' : 'block',
)}
/>
</div>
) : isMaximized ? (
<Music className="h-32 w-32 text-muted-foreground" />
) : null}
</div>
{/* Track Info */}
<div className={cn('text-center', isMaximized ? 'mb-8' : 'mb-4')}>
<div className={cn('flex items-center justify-center gap-2', isMaximized && 'gap-3 mb-2')}>
<h3 className={cn('font-medium truncate', isMaximized ? 'text-2xl font-bold' : 'text-sm')}>
{currentSound?.name || 'No track selected'}
</h3>
{currentSound &&
(currentSound.extract_url || currentSound.id) && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className={isMaximized ? 'h-6 w-6 p-0' : 'h-4 w-4 p-0'}
>
<MoreVertical className={isMaximized ? 'h-4 w-4' : 'h-3 w-3'} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{currentSound.extract_url && (
<DropdownMenuItem asChild>
<a
href={currentSound.extract_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<ExternalLink className="h-4 w-4" />
Source
</a>
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={onDownloadSound}
className="flex items-center gap-2"
>
<Download className="h-4 w-4" />
File
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
</>
)
})

View File

@@ -1,10 +1,19 @@
import { useState } from 'react'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from '@/components/ui/context-menu'
import { filesService } from '@/lib/api/services/files' import { filesService } from '@/lib/api/services/files'
import { type PlayerPlaylist } from '@/lib/api/services/player' import { type PlayerPlaylist, playerService } from '@/lib/api/services/player'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { formatDuration } from '@/utils/format-duration' import { formatDuration } from '@/utils/format-duration'
import { Music, Play } from 'lucide-react' import { Music, Play, Search, X, ListPlus } from 'lucide-react'
interface PlaylistProps { interface PlaylistProps {
playlist: PlayerPlaylist playlist: PlayerPlaylist
@@ -19,6 +28,20 @@ export function Playlist({
onTrackSelect, onTrackSelect,
variant = 'normal', variant = 'normal',
}: PlaylistProps) { }: PlaylistProps) {
const [searchQuery, setSearchQuery] = useState('')
const filteredSounds = playlist.sounds.filter((sound) =>
sound.name.toLowerCase().includes(searchQuery.toLowerCase())
)
const handleAddToPlayNext = async (soundId: number) => {
try {
await playerService.addToPlayNext(soundId)
} catch (error) {
console.error('Failed to add track to play next:', error)
}
}
return ( return (
<div className="w-full"> <div className="w-full">
{/* Header */} {/* Header */}
@@ -29,70 +52,103 @@ export function Playlist({
</Badge> </Badge>
</div> </div>
{/* Search */}
<div className="relative mb-2">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
type="text"
placeholder="Search tracks..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="h-8 pl-8 pr-8 text-xs"
/>
{searchQuery && (
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-1/2 -translate-y-1/2 h-8 w-8"
onClick={() => setSearchQuery('')}
>
<X className="h-3.5 w-3.5" />
</Button>
)}
</div>
{/* Track List */} {/* Track List */}
<ScrollArea <ScrollArea
className={variant === 'maximized' ? 'h-[calc(100vh-230px)]' : 'h-60'} className={variant === 'maximized' ? 'h-[calc(100vh-320px)]' : 'h-60'}
> >
<div className="w-full"> <div className="w-full">
{playlist.sounds.map((sound, index) => ( {filteredSounds.map((sound) => {
<div const originalIndex = playlist.sounds.findIndex((s) => s.id === sound.id)
key={sound.id} return (
className={cn( <ContextMenu key={sound.id}>
'grid grid-cols-10 gap-2 items-center py-1.5 px-2 rounded hover:bg-muted/50 cursor-pointer text-xs', <ContextMenuTrigger asChild>
currentIndex === index && 'bg-primary/10 text-primary', <div
)} className={cn(
onClick={() => onTrackSelect(index)} 'grid grid-cols-10 gap-2 items-center py-1.5 px-2 rounded hover:bg-muted/50 cursor-pointer text-xs',
> currentIndex === originalIndex && 'bg-primary/10 text-primary',
{/* Track number/play icon - 1 column */} )}
<div className="col-span-1 flex justify-center"> onClick={() => onTrackSelect(originalIndex)}
{currentIndex === index ? ( >
<Play className="h-3 w-3" /> {/* Track number/play icon - 1 column */}
) : ( <div className="col-span-1 flex justify-center">
<span className="text-muted-foreground">{index + 1}</span> {currentIndex === originalIndex ? (
)} <Play className="h-3 w-3" />
</div> ) : (
<span className="text-muted-foreground">{originalIndex + 1}</span>
)}
</div>
{/* Thumbnail - 1 column */} {/* Thumbnail - 1 column */}
<div className="col-span-1"> <div className="col-span-1">
<div <div
className={cn( className={cn(
'bg-muted rounded flex items-center justify-center overflow-hidden', 'bg-muted rounded flex items-center justify-center overflow-hidden',
variant === 'maximized' ? 'w-6 h-6' : 'w-5 h-5', variant === 'maximized' ? 'w-6 h-6' : 'w-5 h-5',
)} )}
> >
{sound.thumbnail ? ( {sound.thumbnail ? (
<img <img
src={filesService.getThumbnailUrl(sound.id)} src={filesService.getThumbnailUrl(sound.id)}
alt="" alt=""
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
<Music className="h-3 w-3 text-muted-foreground" /> <Music className="h-3 w-3 text-muted-foreground" />
)} )}
</div> </div>
</div> </div>
{/* Track name - 6 columns (takes most space) */} {/* Track name - 6 columns (takes most space) */}
<div className="col-span-6"> <div className="col-span-6">
<span <span
className={cn( className={cn(
'font-medium truncate block', 'font-medium truncate block',
variant === 'maximized' ? 'text-sm' : 'text-xs', variant === 'maximized' ? 'text-sm' : 'text-xs',
currentIndex === index ? 'text-primary' : 'text-foreground', currentIndex === originalIndex ? 'text-primary' : 'text-foreground',
)} )}
> >
{sound.name} {sound.name}
</span> </span>
</div> </div>
{/* Duration - 2 columns */} {/* Duration - 2 columns */}
<div className="col-span-2 text-right"> <div className="col-span-2 text-right">
<span className="text-muted-foreground text-xs whitespace-nowrap"> <span className="text-muted-foreground text-xs whitespace-nowrap">
{formatDuration(sound.duration)} {formatDuration(sound.duration)}
</span> </span>
</div> </div>
</div> </div>
))} </ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleAddToPlayNext(sound.id)}>
<ListPlus className="mr-2 h-4 w-4" />
Add to play next
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)})}
</div> </div>
</ScrollArea> </ScrollArea>

View File

@@ -5,16 +5,17 @@ import type { Playlist } from '@/lib/api/services/playlists'
import { formatDateDistanceToNow } from '@/utils/format-date' import { formatDateDistanceToNow } from '@/utils/format-date'
import { formatDuration } from '@/utils/format-duration' import { formatDuration } from '@/utils/format-duration'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Calendar, Clock, Edit, Heart, Music, Play, User } from 'lucide-react' import { Calendar, Clock, Edit, Heart, Music, Play, Trash2, User } from 'lucide-react'
interface PlaylistRowProps { interface PlaylistRowProps {
playlist: Playlist playlist: Playlist
onEdit: (playlist: Playlist) => void onEdit: (playlist: Playlist) => void
onSetCurrent: (playlist: Playlist) => void onSetCurrent: (playlist: Playlist) => void
onFavoriteToggle?: (playlistId: number, isFavorited: boolean) => void onFavoriteToggle?: (playlistId: number, isFavorited: boolean) => void
onDelete?: (playlist: Playlist) => void
} }
export function PlaylistRow({ playlist, onEdit, onSetCurrent, onFavoriteToggle }: PlaylistRowProps) { export function PlaylistRow({ playlist, onEdit, onSetCurrent, onFavoriteToggle, onDelete }: PlaylistRowProps) {
const handleFavoriteToggle = () => { const handleFavoriteToggle = () => {
if (onFavoriteToggle) { if (onFavoriteToggle) {
onFavoriteToggle(playlist.id, !playlist.is_favorited) onFavoriteToggle(playlist.id, !playlist.is_favorited)
@@ -121,6 +122,17 @@ export function PlaylistRow({ playlist, onEdit, onSetCurrent, onFavoriteToggle }
<Play className="h-4 w-4" /> <Play className="h-4 w-4" />
</Button> </Button>
)} )}
{onDelete && !playlist.is_main && playlist.is_deletable && (
<Button
size="sm"
variant="ghost"
onClick={() => onDelete(playlist)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
title={`Delete "${playlist.name}"`}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -13,9 +13,10 @@ interface PlaylistTableProps {
onEdit: (playlist: Playlist) => void onEdit: (playlist: Playlist) => void
onSetCurrent: (playlist: Playlist) => void onSetCurrent: (playlist: Playlist) => void
onFavoriteToggle?: (playlistId: number, isFavorited: boolean) => void onFavoriteToggle?: (playlistId: number, isFavorited: boolean) => void
onDelete?: (playlist: Playlist) => void
} }
export function PlaylistTable({ playlists, onEdit, onSetCurrent, onFavoriteToggle }: PlaylistTableProps) { export function PlaylistTable({ playlists, onEdit, onSetCurrent, onFavoriteToggle, onDelete }: PlaylistTableProps) {
return ( return (
<div className="rounded-md border"> <div className="rounded-md border">
<Table> <Table>
@@ -39,6 +40,7 @@ export function PlaylistTable({ playlists, onEdit, onSetCurrent, onFavoriteToggl
onEdit={onEdit} onEdit={onEdit}
onSetCurrent={onSetCurrent} onSetCurrent={onSetCurrent}
onFavoriteToggle={onFavoriteToggle} onFavoriteToggle={onFavoriteToggle}
onDelete={onDelete}
/> />
))} ))}
</TableBody> </TableBody>

View File

@@ -38,7 +38,7 @@ interface CreateTaskDialogProps {
onCancel: () => void onCancel: () => void
} }
const TASK_TYPES: TaskType[] = ['credit_recharge', 'play_sound', 'play_playlist'] const TASK_TYPES: TaskType[] = [/*'credit_recharge',*/ 'play_sound', 'play_playlist']
const RECURRENCE_TYPES: RecurrenceType[] = ['none', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'cron'] const RECURRENCE_TYPES: RecurrenceType[] = ['none', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'cron']
export function CreateTaskDialog({ export function CreateTaskDialog({

View File

@@ -14,7 +14,8 @@ import {
getTaskTypeLabel, getTaskTypeLabel,
} from '@/lib/api/services/schedulers' } from '@/lib/api/services/schedulers'
import { import {
CalendarPlus, Filter,
Plus,
RefreshCw, RefreshCw,
Search, Search,
} from 'lucide-react' } from 'lucide-react'
@@ -34,7 +35,7 @@ interface SchedulersHeaderProps {
} }
const TASK_STATUSES: TaskStatus[] = ['pending', 'running', 'completed', 'failed', 'cancelled'] const TASK_STATUSES: TaskStatus[] = ['pending', 'running', 'completed', 'failed', 'cancelled']
const TASK_TYPES: TaskType[] = ['credit_recharge', 'play_sound', 'play_playlist'] const TASK_TYPES: TaskType[] = [/*'credit_recharge',*/ 'play_sound', 'play_playlist']
export function SchedulersHeader({ export function SchedulersHeader({
searchQuery, searchQuery,
@@ -64,18 +65,9 @@ export function SchedulersHeader({
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button onClick={onCreateClick}>
variant="outline" <Plus className="h-4 w-4 mr-2" />
size="sm" Add Extraction
onClick={onRefresh}
disabled={loading}
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
<Button onClick={onCreateClick} size="sm">
<CalendarPlus className="h-4 w-4" />
Create Task
</Button> </Button>
</div> </div>
</div> </div>
@@ -96,7 +88,8 @@ export function SchedulersHeader({
value={statusFilter} value={statusFilter}
onValueChange={onStatusFilterChange} onValueChange={onStatusFilterChange}
> >
<SelectTrigger className="w-[140px]"> <SelectTrigger className="w-[160px]">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="All Status" /> <SelectValue placeholder="All Status" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -113,7 +106,8 @@ export function SchedulersHeader({
value={taskTypeFilter} value={taskTypeFilter}
onValueChange={onTaskTypeFilterChange} onValueChange={onTaskTypeFilterChange}
> >
<SelectTrigger className="w-[140px]"> <SelectTrigger className="w-[200px]">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="All Types" /> <SelectValue placeholder="All Types" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -125,6 +119,18 @@ export function SchedulersHeader({
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<Button
variant="outline"
size="icon"
onClick={onRefresh}
disabled={loading}
title="Refresh extractions"
>
<RefreshCw
className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`}
/>
</Button>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,6 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { formatDate } from '@/utils/format-date' import { formatDate } from '@/utils/format-date'
@@ -20,8 +19,6 @@ import {
import { import {
CalendarClock, CalendarClock,
MoreHorizontal, MoreHorizontal,
Pause,
Play,
Square, Square,
} from 'lucide-react' } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
@@ -29,36 +26,26 @@ import { toast } from 'sonner'
interface SchedulersTableProps { interface SchedulersTableProps {
tasks: ScheduledTask[] tasks: ScheduledTask[]
onTaskUpdated?: (task: ScheduledTask) => void
onTaskDeleted?: (taskId: number) => void onTaskDeleted?: (taskId: number) => void
} }
export function SchedulersTable({ tasks, onTaskUpdated, onTaskDeleted }: SchedulersTableProps) { export function SchedulersTable({ tasks, onTaskDeleted }: SchedulersTableProps) {
const [loadingActions, setLoadingActions] = useState<Record<number, boolean>>({}) const [loadingActions, setLoadingActions] = useState<Record<number, boolean>>({})
const handleToggleActive = async (task: ScheduledTask) => {
if (loadingActions[task.id]) return
try {
setLoadingActions(prev => ({ ...prev, [task.id]: true }))
const updatedTask = await schedulersService.updateTask(task.id, {
is_active: !task.is_active,
})
onTaskUpdated?.(updatedTask)
toast.success(`Task ${task.is_active ? 'paused' : 'resumed'} successfully`)
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to update task'
toast.error(message)
} finally {
setLoadingActions(prev => ({ ...prev, [task.id]: false }))
}
}
const handleCancelTask = async (task: ScheduledTask) => { const handleCancelTask = async (task: ScheduledTask) => {
if (loadingActions[task.id]) return if (loadingActions[task.id]) return
// Confirm deletion
const confirmMessage = `Are you sure you want to delete the task "${task.name}"?${
task.status === 'pending' || task.status === 'running'
? '\n\nThis task is currently active and will be stopped immediately.'
: ''
}\n\nThis action cannot be undone.`
if (!confirm(confirmMessage)) {
return
}
try { try {
setLoadingActions(prev => ({ ...prev, [task.id]: true })) setLoadingActions(prev => ({ ...prev, [task.id]: true }))
@@ -85,7 +72,7 @@ export function SchedulersTable({ tasks, onTaskUpdated, onTaskDeleted }: Schedul
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{tasks.map((task) => ( {tasks.map((task) => (
<Card key={task.id} className={task.is_active ? '' : 'opacity-60'}> <Card key={task.id}>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="space-y-1"> <div className="space-y-1">
@@ -94,11 +81,6 @@ export function SchedulersTable({ tasks, onTaskUpdated, onTaskDeleted }: Schedul
<Badge variant={getTaskStatusVariant(task.status)}> <Badge variant={getTaskStatusVariant(task.status)}>
{getTaskStatusLabel(task.status)} {getTaskStatusLabel(task.status)}
</Badge> </Badge>
{!task.is_active && (
<Badge variant="outline" className="text-muted-foreground">
Paused
</Badge>
)}
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{getTaskTypeLabel(task.task_type)} {getTaskTypeLabel(task.task_type)}
@@ -121,26 +103,8 @@ export function SchedulersTable({ tasks, onTaskUpdated, onTaskDeleted }: Schedul
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => handleToggleActive(task)}
disabled={task.status === 'completed' || task.status === 'cancelled'}
>
{task.is_active ? (
<>
<Pause className="h-4 w-4 mr-2" />
Pause Task
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
Resume Task
</>
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleCancelTask(task)} onClick={() => handleCancelTask(task)}
disabled={task.status === 'completed' || task.status === 'cancelled'}
className="text-destructive focus:text-destructive" className="text-destructive focus:text-destructive"
> >
<Square className="h-4 w-4 mr-2" /> <Square className="h-4 w-4 mr-2" />

View File

@@ -1,4 +1,5 @@
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { type Sound } from '@/lib/api/services/sounds' import { type Sound } from '@/lib/api/services/sounds'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { formatDuration } from '@/utils/format-duration' import { formatDuration } from '@/utils/format-duration'
@@ -27,6 +28,19 @@ export function SoundCard({ sound, playSound, onFavoriteToggle, colorClasses }:
onFavoriteToggle(sound.id, !sound.is_favorited) onFavoriteToggle(sound.id, !sound.is_favorited)
} }
const getBadgeVariant = (type: Sound['type']) => {
switch (type) {
case 'SDB':
return 'default'
case 'TTS':
return 'secondary'
case 'EXT':
return 'outline'
default:
return 'default'
}
}
return ( return (
<Card <Card
onClick={handlePlaySound} onClick={handlePlaySound}
@@ -36,6 +50,14 @@ export function SoundCard({ sound, playSound, onFavoriteToggle, colorClasses }:
)} )}
> >
<CardContent className="grid grid-cols-1 pl-3 pr-3 gap-1"> <CardContent className="grid grid-cols-1 pl-3 pr-3 gap-1">
{/* Type badge */}
<Badge
variant={getBadgeVariant(sound.type)}
className="absolute top-2 left-2 text-xs px-1 py-0 h-4 text-[10px]"
>
{sound.type}
</Badge>
{/* Favorite button */} {/* Favorite button */}
<button <button
data-favorite-button data-favorite-button
@@ -57,7 +79,7 @@ export function SoundCard({ sound, playSound, onFavoriteToggle, colorClasses }:
/> />
</button> </button>
<h3 className="font-medium text-s truncate pr-8">{sound.name}</h3> <h3 className="font-medium text-s truncate pl-8 pr-6">{sound.name}</h3>
<div className="grid grid-cols-2 gap-1 text-xs text-muted-foreground"> <div className="grid grid-cols-2 gap-1 text-xs text-muted-foreground">
<div className="flex"> <div className="flex">
<Clock className="h-3.5 w-3.5 mr-0.5" /> <Clock className="h-3.5 w-3.5 mr-0.5" />

View File

@@ -0,0 +1,279 @@
import { useState, useEffect } from 'react'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Combobox, type ComboboxOption } from '@/components/ui/combobox'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { Switch } from '@/components/ui/switch'
import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label'
import { Loader2, Mic } from 'lucide-react'
import { ttsService, type TTSProvider } from '@/lib/api/services/tts'
import { TTS_EVENTS, ttsEvents } from '@/lib/events'
import { toast } from 'sonner'
import { getSortedLanguages, getLanguageDisplayName } from '@/lib/constants/gtts-languages'
interface FormData {
text: string
provider: string
language?: string
slow?: boolean
}
interface CreateTTSDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function CreateTTSDialog({ open, onOpenChange }: CreateTTSDialogProps) {
const [selectedProvider, setSelectedProvider] = useState<TTSProvider | null>(null)
const [providers, setProviders] = useState<Record<string, TTSProvider> | null>(null)
const [isLoadingProviders, setIsLoadingProviders] = useState(false)
const [isGenerating, setIsGenerating] = useState(false)
const [formErrors, setFormErrors] = useState<Record<string, string>>({})
const [formData, setFormData] = useState<FormData>({
text: '',
provider: 'gtts',
language: 'en',
slow: false,
})
// Prepare language options for combobox
const languageOptions: ComboboxOption[] = getSortedLanguages().map((lang) => ({
value: lang.code,
label: getLanguageDisplayName(lang),
searchValue: `${lang.name} ${lang.region || ''} ${lang.code}`.toLowerCase()
}))
// Load providers when dialog opens
useEffect(() => {
if (open && !providers) {
const loadProviders = async () => {
try {
setIsLoadingProviders(true)
const data = await ttsService.getProviders()
setProviders(data)
} catch (error) {
toast.error('Failed to load TTS providers')
} finally {
setIsLoadingProviders(false)
}
}
loadProviders()
}
}, [open, providers])
const generateTTS = async (data: FormData) => {
try {
setIsGenerating(true)
const { text, provider, language, slow } = data
const response = await ttsService.generateTTS({
text,
provider,
options: {
...(language && { lang: language }),
tld: 'com', // Always use .com TLD
...(slow !== undefined && { slow }),
},
})
toast.success(response.message)
onOpenChange(false)
handleReset()
// Emit event for new TTS created
ttsEvents.emit(TTS_EVENTS.TTS_CREATED, response.tts)
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to generate TTS')
} finally {
setIsGenerating(false)
}
}
// Update selected provider when form provider changes
useEffect(() => {
if (providers && formData.provider) {
setSelectedProvider(providers[formData.provider] || null)
}
}, [formData.provider, providers])
const validateForm = (): boolean => {
const errors: Record<string, string> = {}
if (!formData.text.trim()) {
errors.text = 'Text is required'
} else if (formData.text.length > 1000) {
errors.text = 'Text must be less than 1000 characters'
}
if (!formData.provider) {
errors.provider = 'Provider is required'
}
setFormErrors(errors)
return Object.keys(errors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (validateForm()) {
generateTTS(formData)
}
}
const handleReset = () => {
setFormData({
text: '',
provider: 'gtts',
language: 'en',
slow: false,
})
setFormErrors({})
}
const handleClose = () => {
if (!isGenerating) {
handleReset()
onOpenChange(false)
}
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Mic className="h-5 w-5" />
Generate Text to Speech
</DialogTitle>
<DialogDescription>
Convert text to speech using various TTS providers. The audio will be processed and added to your soundboard.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="text">Text</Label>
<Textarea
id="text"
placeholder="Enter the text you want to convert to speech..."
className="min-h-[100px] resize-none"
value={formData.text}
onChange={(e) => setFormData(prev => ({ ...prev, text: e.target.value }))}
/>
<p className="text-sm text-muted-foreground">
Maximum 1000 characters ({formData.text?.length || 0}/1000)
</p>
{formErrors.text && (
<p className="text-sm text-destructive">{formErrors.text}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="provider">Provider</Label>
<Select
value={formData.provider}
onValueChange={(value) => setFormData(prev => ({ ...prev, provider: value }))}
>
<SelectTrigger>
<SelectValue placeholder="Select a TTS provider" />
</SelectTrigger>
<SelectContent>
{isLoadingProviders ? (
<SelectItem value="loading" disabled>
Loading providers...
</SelectItem>
) : (
providers &&
Object.entries(providers).map(([key, provider]) => (
<SelectItem key={key} value={key}>
<div className="flex items-center gap-2">
<span>{provider.name.toUpperCase()}</span>
<Badge variant="outline" className="text-xs">
{provider.file_extension}
</Badge>
</div>
</SelectItem>
))
)}
</SelectContent>
</Select>
{formErrors.provider && (
<p className="text-sm text-destructive">{formErrors.provider}</p>
)}
</div>
{selectedProvider && (
<div className="space-y-4 p-4 border rounded-lg bg-muted/50">
<Label className="text-sm font-medium">Provider Options</Label>
{selectedProvider.name === 'gtts' && (
<>
<div className="space-y-2">
<Label htmlFor="language">Language</Label>
<Combobox
value={formData.language}
onValueChange={(value) => setFormData(prev => ({ ...prev, language: value }))}
options={languageOptions}
placeholder="Select language..."
searchPlaceholder="Search languages..."
emptyMessage="No language found."
/>
<p className="text-sm text-muted-foreground">
Choose from {languageOptions.length} supported languages including regional variants
</p>
</div>
<div className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<Label className="text-base">Slow Speech</Label>
<p className="text-sm text-muted-foreground">
Generate speech at a slower pace
</p>
</div>
<Switch
checked={formData.slow}
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, slow: checked }))}
/>
</div>
</>
)}
</div>
)}
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={handleClose}
disabled={isGenerating}
>
Cancel
</Button>
<Button type="submit" disabled={isGenerating}>
{isGenerating && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Generate TTS
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,131 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import type { TTSSortField, TTSSortOrder } from '@/lib/api/services/tts'
import { Plus, RefreshCw, Search, SortAsc, SortDesc, X } from 'lucide-react'
interface TTSHeaderProps {
searchQuery: string
onSearchChange: (query: string) => void
sortBy: TTSSortField
onSortByChange: (sortBy: TTSSortField) => void
sortOrder: TTSSortOrder
onSortOrderChange: (order: TTSSortOrder) => void
onRefresh: () => void
onCreateClick: () => void
loading: boolean
error: string | null
ttsCount: number
}
export function TTSHeader({
searchQuery,
onSearchChange,
sortBy,
onSortByChange,
sortOrder,
onSortOrderChange,
onRefresh,
onCreateClick,
loading,
error,
ttsCount,
}: TTSHeaderProps) {
return (
<>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold">Text to Speech</h1>
<p className="text-muted-foreground">
Generate speech from text using various TTS providers
</p>
</div>
<div className="flex items-center gap-4">
{!loading && !error && (
<div className="text-sm text-muted-foreground">
{ttsCount} generation{ttsCount !== 1 ? 's' : ''}
</div>
)}
<Button onClick={onCreateClick}>
<Plus className="h-4 w-4 mr-2" />
Generate TTS
</Button>
</div>
</div>
{/* Search and Sort Controls */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by text or provider..."
value={searchQuery}
onChange={e => onSearchChange(e.target.value)}
className="pl-9 pr-9"
/>
{searchQuery && (
<Button
variant="ghost"
size="sm"
onClick={() => onSearchChange('')}
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0 hover:bg-muted"
title="Clear search"
>
<X className="h-3 w-3" />
</Button>
)}
</div>
</div>
<div className="flex gap-2">
<Select
value={sortBy}
onValueChange={value => onSortByChange(value as TTSSortField)}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="created_at">Created Date</SelectItem>
<SelectItem value="text">Text</SelectItem>
<SelectItem value="provider">Provider</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
onClick={() => onSortOrderChange(sortOrder === 'asc' ? 'desc' : 'asc')}
title={sortOrder === 'asc' ? 'Sort ascending' : 'Sort descending'}
>
{sortOrder === 'asc' ? (
<SortAsc className="h-4 w-4" />
) : (
<SortDesc className="h-4 w-4" />
)}
</Button>
<Button
variant="outline"
size="icon"
onClick={onRefresh}
disabled={loading}
title="Refresh TTS history"
>
<RefreshCw
className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`}
/>
</Button>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,239 @@
import { useState, useEffect, useCallback } from 'react'
import { ttsService, type TTSResponse } from '@/lib/api/services/tts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Skeleton } from '@/components/ui/skeleton'
import { TTSRow } from './TTSRow'
import { RefreshCw, Search } from 'lucide-react'
export function TTSList() {
const [ttsHistory, setTTSHistory] = useState<TTSResponse[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [sortBy, setSortBy] = useState('created_at')
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
const [limit, setLimit] = useState(50)
const fetchTTSHistory = useCallback(async () => {
try {
setIsLoading(true)
setError(null)
const data = await ttsService.getTTSHistory({ limit })
setTTSHistory(data.tts)
} catch (err) {
setError('Failed to load TTS history')
console.error('Failed to fetch TTS history:', err)
} finally {
setIsLoading(false)
}
}, [limit])
useEffect(() => {
fetchTTSHistory()
}, [fetchTTSHistory])
// Listen for TTS generation events to refresh the list
useEffect(() => {
const handleTTSGenerated = () => {
fetchTTSHistory()
}
window.addEventListener('tts-generated', handleTTSGenerated)
return () => {
window.removeEventListener('tts-generated', handleTTSGenerated)
}
}, [fetchTTSHistory])
const filteredHistory = ttsHistory.filter((tts) =>
tts.text.toLowerCase().includes(searchQuery.toLowerCase()) ||
tts.provider.toLowerCase().includes(searchQuery.toLowerCase())
)
const sortedHistory = [...filteredHistory].sort((a, b) => {
let aValue: any = a[sortBy as keyof TTSResponse]
let bValue: any = b[sortBy as keyof TTSResponse]
// Convert dates to timestamps for comparison
if (sortBy === 'created_at') {
aValue = new Date(aValue).getTime()
bValue = new Date(bValue).getTime()
}
if (sortOrder === 'asc') {
return aValue > bValue ? 1 : -1
} else {
return aValue < bValue ? 1 : -1
}
})
if (error) {
return (
<Card>
<CardContent className="pt-6">
<div className="text-center text-muted-foreground">
Failed to load TTS history. Please try again.
</div>
</CardContent>
</Card>
)
}
return (
<div className="space-y-4">
{/* Search and filters */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
TTS History
<Button
variant="ghost"
size="sm"
onClick={fetchTTSHistory}
disabled={isLoading}
>
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
</Button>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-4">
<div className="flex-1">
<Label htmlFor="search">Search</Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="search"
placeholder="Search by text or provider..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
</div>
<div>
<Label htmlFor="sortBy">Sort by</Label>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger id="sortBy" className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="created_at">Created</SelectItem>
<SelectItem value="text">Text</SelectItem>
<SelectItem value="provider">Provider</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="sortOrder">Order</Label>
<Select
value={sortOrder}
onValueChange={(value: 'asc' | 'desc') => setSortOrder(value)}
>
<SelectTrigger id="sortOrder" className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="desc">Newest</SelectItem>
<SelectItem value="asc">Oldest</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="limit">Limit</Label>
<Select
value={limit.toString()}
onValueChange={(value) => setLimit(parseInt(value))}
>
<SelectTrigger id="limit" className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="25">25</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
{/* Results */}
<div className="space-y-2">
{isLoading ? (
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Card key={i}>
<CardContent className="pt-6">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
{/* Badges row */}
<div className="flex items-center gap-2">
<Skeleton className="h-6 w-16" />
<Skeleton className="h-6 w-20" />
</div>
{/* Text content */}
<div className="space-y-1">
<Skeleton className="h-4 w-3/4" />
<div className="flex gap-1">
<Skeleton className="h-5 w-12" />
<Skeleton className="h-5 w-16" />
</div>
</div>
{/* Date and metadata */}
<div className="flex items-center gap-4">
<Skeleton className="h-3 w-32" />
<Skeleton className="h-3 w-20" />
</div>
</div>
{/* Play button */}
<div className="flex items-center gap-2">
<Skeleton className="h-8 w-8 rounded" />
</div>
</div>
</CardContent>
</Card>
))}
</div>
) : sortedHistory.length === 0 ? (
<Card>
<CardContent className="pt-6">
<div className="text-center text-muted-foreground">
{searchQuery
? 'No TTS generations match your search.'
: 'No TTS generations yet. Create your first one!'}
</div>
</CardContent>
</Card>
) : (
<>
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
{sortedHistory.length} generation{sortedHistory.length !== 1 ? 's' : ''}
</div>
</div>
<div className="space-y-2">
{sortedHistory.map((tts) => (
<TTSRow key={tts.id} tts={tts} />
))}
</div>
</>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,112 @@
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { AlertCircle, Mic, RefreshCw } from 'lucide-react'
export function TTSLoading() {
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Text</TableHead>
<TableHead>Provider</TableHead>
<TableHead>Options</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead className="w-[120px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: 5 }).map((_, i) => (
<TableRow key={i}>
<TableCell className="max-w-md">
<Skeleton className="h-4 w-3/4" />
</TableCell>
<TableCell>
<Skeleton className="h-6 w-16" />
</TableCell>
<TableCell>
<div className="flex gap-1">
<Skeleton className="h-5 w-12" />
<Skeleton className="h-5 w-16" />
</div>
</TableCell>
<TableCell>
<Skeleton className="h-6 w-20" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-32" />
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Skeleton className="h-8 w-8" />
<Skeleton className="h-8 w-8" />
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}
interface TTSErrorProps {
error: string
onRetry: () => void
}
export function TTSError({ error, onRetry }: TTSErrorProps) {
return (
<Card>
<CardContent className="pt-6">
<div className="flex flex-col items-center justify-center space-y-4 text-center">
<AlertCircle className="h-12 w-12 text-destructive" />
<div className="space-y-2">
<h3 className="text-lg font-semibold">Failed to load TTS generations</h3>
<p className="text-sm text-muted-foreground">{error}</p>
</div>
<Button onClick={onRetry} variant="outline">
<RefreshCw className="mr-2 h-4 w-4" />
Try again
</Button>
</div>
</CardContent>
</Card>
)
}
interface TTSEmptyProps {
searchQuery: string
}
export function TTSEmpty({ searchQuery }: TTSEmptyProps) {
return (
<Card>
<CardContent className="pt-6">
<div className="flex flex-col items-center justify-center space-y-4 text-center">
<Mic className="h-12 w-12 text-muted-foreground" />
<div className="space-y-2">
<h3 className="text-lg font-semibold">
{searchQuery ? 'No TTS generations found' : 'No TTS generations yet'}
</h3>
<p className="text-sm text-muted-foreground">
{searchQuery
? 'Try adjusting your search or create a new TTS generation.'
: 'Create your first text-to-speech generation to get started.'}
</p>
</div>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,121 @@
import { format } from 'date-fns'
import { Clock, Mic, Volume2, CheckCircle, Loader } from 'lucide-react'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { type TTSResponse } from '@/lib/api/services/tts'
import { soundsService } from '@/lib/api/services/sounds'
import { toast } from 'sonner'
interface TTSRowProps {
tts: TTSResponse
}
export function TTSRow({ tts }: TTSRowProps) {
const isCompleted = tts.sound_id !== null
const isProcessing = tts.sound_id === null
const handlePlaySound = async () => {
if (!tts.sound_id) {
toast.error('This TTS is still being processed.')
return
}
try {
await soundsService.playSound(tts.sound_id)
} catch (error) {
toast.error('Failed to play the sound.')
}
}
const getProviderColor = (provider: string) => {
const colors = {
gtts: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
}
return colors[provider as keyof typeof colors] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300'
}
const getStatusBadge = () => {
if (isCompleted) {
return (
<Badge variant="secondary" className="gap-1">
<CheckCircle className="h-3 w-3" />
Complete
</Badge>
)
} else {
return (
<Badge variant="outline" className="gap-1">
<Loader className="h-3 w-3 animate-spin" />
Processing
</Badge>
)
}
}
return (
<Card className="transition-colors hover:bg-muted/50">
<CardContent className="pt-6">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<Badge className={getProviderColor(tts.provider)}>
<Mic className="mr-1 h-3 w-3" />
{tts.provider.toUpperCase()}
</Badge>
{getStatusBadge()}
</div>
<div className="space-y-1">
<p className="text-sm font-medium leading-relaxed">
&quot;{tts.text}&quot;
</p>
{Object.keys(tts.options).length > 0 && (
<div className="flex gap-1 flex-wrap">
{Object.entries(tts.options).map(([key, value]) => (
<Badge key={key} variant="outline" className="text-xs">
{key}: {String(value)}
</Badge>
))}
</div>
)}
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{format(new Date(tts.created_at), 'MMM dd, yyyy HH:mm')}
</div>
{tts.sound_id && (
<div className="flex items-center gap-1">
Sound ID: {tts.sound_id}
</div>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={handlePlaySound}
disabled={isProcessing}
>
<Volume2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
{isProcessing ? 'Sound is being processed' : 'Play sound'}
</TooltipContent>
</Tooltip>
</div>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,193 @@
import { Calendar, CheckCircle, Clock, Loader, Mic, Trash2, Volume2, XCircle } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { type TTSResponse, ttsService } from '@/lib/api/services/tts'
import { soundsService } from '@/lib/api/services/sounds'
import { toast } from 'sonner'
import { formatDateDistanceToNow } from '@/utils/format-date'
interface TTSTableProps {
ttsHistory: TTSResponse[]
onTTSDeleted?: (ttsId: number) => void
}
export function TTSTable({ ttsHistory, onTTSDeleted }: TTSTableProps) {
const handlePlaySound = async (tts: TTSResponse) => {
if (!tts.sound_id) {
toast.error('This TTS is still being processed.')
return
}
try {
await soundsService.playSound(tts.sound_id)
} catch (error) {
toast.error('Failed to play the sound.')
}
}
const handleDeleteTTS = async (tts: TTSResponse) => {
if (!confirm(`Are you sure you want to delete this TTS generation?\n\n"${tts.text}"\n\nThis will also delete the associated sound file and cannot be undone.`)) {
return
}
try {
await ttsService.deleteTTS(tts.id)
toast.success('TTS generation deleted successfully')
onTTSDeleted?.(tts.id)
} catch (error) {
toast.error('Failed to delete TTS generation')
}
}
const getProviderColor = (provider: string) => {
const colors = {
gtts: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
}
return colors[provider as keyof typeof colors] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300'
}
const getStatusBadge = (tts: TTSResponse) => {
switch (tts.status) {
case 'completed':
return (
<Badge variant="secondary" className="gap-1">
<CheckCircle className="h-3 w-3" />
Completed
</Badge>
)
case 'processing':
return (
<Badge variant="outline" className="gap-1">
<Loader className="h-3 w-3 animate-spin" />
Processing
</Badge>
)
case 'failed':
return (
<Badge variant="destructive" className="gap-1">
<XCircle className="h-3 w-3" />
Failed
</Badge>
)
case 'pending':
default:
return (
<Badge variant="outline" className="gap-1">
<Clock className="h-3 w-3" />
Pending
</Badge>
)
}
}
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Text</TableHead>
<TableHead>Provider</TableHead>
<TableHead>Options</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead className="w-[120px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{ttsHistory.map((tts) => (
<TableRow key={tts.id}>
<TableCell className="max-w-md">
<div className="truncate font-medium">
"{tts.text}"
</div>
</TableCell>
<TableCell>
<Badge className={getProviderColor(tts.provider)}>
<Mic className="mr-1 h-3 w-3" />
{tts.provider.toUpperCase()}
</Badge>
</TableCell>
<TableCell>
{Object.keys(tts.options).length > 0 ? (
<div className="flex gap-1 flex-wrap">
{Object.entries(tts.options).map(([key, value]) => (
<Badge key={key} variant="outline" className="text-xs">
{key}: {String(value)}
</Badge>
))}
</div>
) : (
<span className="text-muted-foreground text-sm">None</span>
)}
</TableCell>
<TableCell>
<div className="space-y-1">
{getStatusBadge(tts)}
{tts.error && (
<div
className="text-xs text-destructive max-w-48 truncate"
title={tts.error}
>
{tts.error}
</div>
)}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Calendar className="h-3 w-3" />
{formatDateDistanceToNow(tts.created_at)}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => handlePlaySound(tts)}
disabled={!tts.sound_id}
>
<Volume2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
{!tts.sound_id ? 'Sound is being processed' : 'Play sound'}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteTTS(tts)}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
Delete TTS generation
</TooltipContent>
</Tooltip>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}

View File

@@ -5,18 +5,17 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: default: "bg-primary text-primary-foreground hover:bg-primary/90",
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive: destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
@@ -26,6 +25,8 @@ const buttonVariants = cva(
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9", icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
}, },
}, },
defaultVariants: { defaultVariants: {

View File

@@ -1,5 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import { import {
ChevronDownIcon, ChevronDownIcon,

View File

@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="card-header" data-slot="card-header"
className={cn( className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className className
)} )}
{...props} {...props}

View File

@@ -179,70 +179,72 @@ function ChartTooltipContent({
> >
{!nestLabel ? tooltipLabel : null} {!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5"> <div className="grid gap-1.5">
{payload.map((item, index) => { {payload
const key = `${nameKey || item.name || item.dataKey || "value"}` .filter((item) => item.type !== "none")
const itemConfig = getPayloadConfigFromPayload(config, item, key) .map((item, index) => {
const indicatorColor = color || item.payload.fill || item.color const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return ( return (
<div <div
key={item.dataKey} key={item.dataKey}
className={cn( className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5", "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center" indicator === "dot" && "items-center"
)} )}
> >
{formatter && item?.value !== undefined && item.name ? ( {formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload) formatter(item.value, item.name, item, index, item.payload)
) : ( ) : (
<> <>
{itemConfig?.icon ? ( {itemConfig?.icon ? (
<itemConfig.icon /> <itemConfig.icon />
) : ( ) : (
!hideIndicator && ( !hideIndicator && (
<div <div
className={cn( className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{ {
"h-2.5 w-2.5": indicator === "dot", "h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line", "w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": "w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed", indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed", "my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
} }
)} />
style={ )
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)} )}
> <div
<div className="grid gap-1.5"> className={cn(
{nestLabel ? tooltipLabel : null} "flex flex-1 justify-between leading-none",
<span className="text-muted-foreground"> nestLabel ? "items-end" : "items-center"
{itemConfig?.label || item.name} )}
</span> >
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div> </div>
{item.value && ( </>
<span className="text-foreground font-mono font-medium tabular-nums"> )}
{item.value.toLocaleString()} </div>
</span> )
)} })}
</div>
</>
)}
</div>
)
})}
</div> </div>
</div> </div>
) )
@@ -275,31 +277,33 @@ function ChartLegendContent({
className className
)} )}
> >
{payload.map((item) => { {payload
const key = `${nameKey || item.dataKey || "value"}` .filter((item) => item.type !== "none")
const itemConfig = getPayloadConfigFromPayload(config, item, key) .map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return ( return (
<div <div
key={item.value} key={item.value}
className={cn( className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3" "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)} )}
> >
{itemConfig?.icon && !hideIcon ? ( {itemConfig?.icon && !hideIcon ? (
<itemConfig.icon /> <itemConfig.icon />
) : ( ) : (
<div <div
className="h-2 w-2 shrink-0 rounded-[2px]" className="h-2 w-2 shrink-0 rounded-[2px]"
style={{ style={{
backgroundColor: item.color, backgroundColor: item.color,
}} }}
/> />
)} )}
{itemConfig?.label} {itemConfig?.label}
</div> </div>
) )
})} })}
</div> </div>
) )
} }

View File

@@ -1,3 +1,5 @@
"use client"
import * as React from "react" import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react" import { CheckIcon } from "lucide-react"

View File

@@ -1,5 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import { Command as CommandPrimitive } from "cmdk" import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react" import { SearchIcon } from "lucide-react"

View File

@@ -1,5 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
@@ -66,7 +64,7 @@ function ContextMenuSubTrigger({
data-slot="context-menu-sub-trigger" data-slot="context-menu-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}

View File

@@ -1,3 +1,5 @@
"use client"
import * as React from "react" import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react" import { XIcon } from "lucide-react"

View File

@@ -209,7 +209,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger" data-slot="dropdown-menu-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8", "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}

View File

@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className className

View File

@@ -1,5 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"

View File

@@ -52,6 +52,7 @@ function SelectContent({
className, className,
children, children,
position = "popper", position = "popper",
align = "center",
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) { }: React.ComponentProps<typeof SelectPrimitive.Content>) {
return ( return (
@@ -65,6 +66,7 @@ function SelectContent({
className className
)} )}
position={position} position={position}
align={align}
{...props} {...props}
> >
<SelectScrollUpButton /> <SelectScrollUpButton />

View File

@@ -53,7 +53,7 @@ function Slider({
<SliderPrimitive.Thumb <SliderPrimitive.Thumb
data-slot="slider-thumb" data-slot="slider-thumb"
key={index} key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/> />
))} ))}
</SliderPrimitive.Root> </SliderPrimitive.Root>

View File

@@ -1,3 +1,5 @@
"use client"
import * as React from "react" import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View File

@@ -1,3 +1,5 @@
"use client"
import * as React from "react" import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as TooltipPrimitive from "@radix-ui/react-tooltip"
@@ -44,13 +46,13 @@ function TooltipContent({
data-slot="tooltip-content" data-slot="tooltip-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
) )

View File

@@ -12,11 +12,13 @@ import {
EXTRACTION_EVENTS, EXTRACTION_EVENTS,
PLAYER_EVENTS, PLAYER_EVENTS,
SOUND_EVENTS, SOUND_EVENTS,
TTS_EVENTS,
USER_EVENTS, USER_EVENTS,
authEvents, authEvents,
extractionEvents, extractionEvents,
playerEvents, playerEvents,
soundEvents, soundEvents,
ttsEvents,
userEvents, userEvents,
} from '../lib/events' } from '../lib/events'
import { extractionsService } from '../lib/api/services/extractions' import { extractionsService } from '../lib/api/services/extractions'
@@ -158,6 +160,28 @@ export function SocketProvider({ children }: SocketProviderProps) {
} }
}) })
// Listen for TTS status updates
newSocket.on('tts_completed', data => {
// Emit local event for other components to listen to
ttsEvents.emit(TTS_EVENTS.TTS_COMPLETED, data)
toast.success('TTS generation completed', {
duration: 3000,
})
})
newSocket.on('tts_failed', data => {
const { error } = data
// Emit local event for other components to listen to
ttsEvents.emit(TTS_EVENTS.TTS_FAILED, data)
toast.error('TTS generation failed', {
description: error,
duration: 5000,
})
})
return newSocket return newSocket
}, [user, fetchAndShowOngoingExtractions]) }, [user, fetchAndShowOngoingExtractions])

View File

@@ -1,4 +1,4 @@
import * as React from 'react' import * as React from "react"
const MOBILE_BREAKPOINT = 768 const MOBILE_BREAKPOINT = 768
@@ -10,9 +10,9 @@ export function useIsMobile() {
const onChange = () => { const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
} }
mql.addEventListener('change', onChange) mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener('change', onChange) return () => mql.removeEventListener("change", onChange)
}, []) }, [])
return !!isMobile return !!isMobile

View File

@@ -0,0 +1,88 @@
import { PLAYER_EVENTS, playerEvents } from '@/lib/events'
import type { PlayerState } from '@/lib/api/services/player'
import { useCallback, useEffect, useRef, useState } from 'react'
// Type for selecting specific parts of the player state
type PlayerStateSelector<T> = (state: PlayerState) => T
// Custom hook for subscribing to specific parts of player state
export function usePlayerState<T>(
selector: PlayerStateSelector<T>,
equalityFn?: (a: T, b: T) => boolean
): T | null {
const [selectedState, setSelectedState] = useState<T | null>(null)
const selectorRef = useRef(selector)
const equalityRef = useRef(equalityFn)
// Keep refs updated
selectorRef.current = selector
equalityRef.current = equalityFn
useEffect(() => {
const handlePlayerState = (...args: unknown[]) => {
const newState = args[0] as PlayerState
const newSelectedState = selectorRef.current(newState)
setSelectedState(prevSelected => {
// Use custom equality function if provided, otherwise use shallow comparison
if (equalityRef.current) {
if (prevSelected !== null && equalityRef.current(prevSelected, newSelectedState)) {
return prevSelected
}
} else if (prevSelected === newSelectedState) {
return prevSelected
}
return newSelectedState
})
}
playerEvents.on(PLAYER_EVENTS.PLAYER_STATE, handlePlayerState)
return () => {
playerEvents.off(PLAYER_EVENTS.PLAYER_STATE, handlePlayerState)
}
}, [])
return selectedState
}
// Specific hooks for common state selections
export function usePlayerProgress() {
return usePlayerState(
useCallback((state: PlayerState) => ({
position: state.position,
duration: state.duration || 0,
}), [])
)
}
export function usePlayerTrackInfo() {
return usePlayerState(
useCallback((state: PlayerState) => state.current_sound, [])
)
}
export function usePlayerControls() {
return usePlayerState(
useCallback((state: PlayerState) => ({
status: state.status,
mode: state.mode,
}), [])
)
}
export function usePlayerVolume() {
return usePlayerState(
useCallback((state: PlayerState) => state.volume, [])
)
}
export function usePlayerPlaylist() {
return usePlayerState(
useCallback((state: PlayerState) => ({
playlist: state.playlist,
index: state.index,
}), [])
)
}

View File

@@ -38,6 +38,7 @@ export interface PlayerState {
index?: number index?: number
current_sound?: PlayerSound current_sound?: PlayerSound
playlist?: PlayerPlaylist playlist?: PlayerPlaylist
play_next_queue: PlayerSound[]
} }
export interface PlayerSeekRequest { export interface PlayerSeekRequest {
@@ -147,6 +148,13 @@ export class PlayerService {
async getState(): Promise<PlayerState> { async getState(): Promise<PlayerState> {
return apiClient.get<PlayerState>('/api/v1/player/state') return apiClient.get<PlayerState>('/api/v1/player/state')
} }
/**
* Add a sound to the play next queue
*/
async addToPlayNext(soundId: number): Promise<MessageResponse> {
return apiClient.post<MessageResponse>(`/api/v1/player/play-next/${soundId}`)
}
} }
export const playerService = new PlayerService() export const playerService = new PlayerService()

136
src/lib/api/services/tts.ts Normal file
View File

@@ -0,0 +1,136 @@
import { apiClient } from '../client'
export interface TTSRequest {
text: string
provider?: string
options?: Record<string, any>
}
export interface TTSResponse {
id: number
text: string
provider: string
options: Record<string, any>
status: string
error: string | null
sound_id: number | null
user_id: number
created_at: string
}
export interface TTSGenerateResponse {
message: string
tts: TTSResponse
}
export interface TTSProvider {
name: string
file_extension: string
supported_languages: string[]
option_schema: Record<string, any>
}
export interface TTSProvidersResponse {
[key: string]: TTSProvider
}
export type TTSSortField = 'created_at' | 'text' | 'provider'
export type TTSSortOrder = 'asc' | 'desc'
export interface GetTTSHistoryParams {
search?: string
sort_by?: TTSSortField
sort_order?: TTSSortOrder
page?: number
limit?: number
}
export interface GetTTSHistoryResponse {
tts: TTSResponse[]
total: number
total_pages: number
current_page: number
}
export const ttsService = {
async generateTTS(request: TTSRequest): Promise<TTSGenerateResponse> {
return await apiClient.post('/api/v1/tts', request)
},
async getTTSHistory(params?: GetTTSHistoryParams): Promise<GetTTSHistoryResponse> {
const searchParams = new URLSearchParams()
// Backend currently only supports limit and offset, not page-based pagination
if (params?.limit) {
searchParams.append('limit', params.limit.toString())
}
if (params?.page && params?.limit) {
// Convert page to offset
const offset = (params.page - 1) * params.limit
searchParams.append('offset', offset.toString())
}
const url = searchParams.toString()
? `/api/v1/tts?${searchParams.toString()}`
: '/api/v1/tts'
const ttsArray: TTSResponse[] = await apiClient.get(url)
// Apply client-side filtering and sorting since backend doesn't support them yet
let filteredTTS = ttsArray
if (params?.search) {
const search = params.search.toLowerCase()
filteredTTS = filteredTTS.filter(tts =>
tts.text.toLowerCase().includes(search) ||
tts.provider.toLowerCase().includes(search)
)
}
if (params?.sort_by && params?.sort_order) {
filteredTTS.sort((a, b) => {
let aValue = a[params.sort_by as keyof TTSResponse]
let bValue = b[params.sort_by as keyof TTSResponse]
// Convert dates to timestamps for comparison
if (params.sort_by === 'created_at') {
aValue = new Date(aValue as string).getTime()
bValue = new Date(bValue as string).getTime()
}
// Handle null values
if (aValue === null && bValue === null) return 0
if (aValue === null) return 1
if (bValue === null) return -1
const comparison = aValue > bValue ? 1 : -1
return params.sort_order === 'asc' ? comparison : -comparison
})
}
// Calculate pagination info
const limit = params?.limit || 50
const currentPage = params?.page || 1
const total = filteredTTS.length
const totalPages = Math.ceil(total / limit)
return {
tts: filteredTTS,
total,
total_pages: totalPages,
current_page: currentPage,
}
},
async getProviders(): Promise<TTSProvidersResponse> {
return await apiClient.get('/api/v1/tts/providers')
},
async getProvider(name: string): Promise<TTSProvider> {
return await apiClient.get(`/api/v1/tts/providers/${name}`)
},
async deleteTTS(ttsId: number): Promise<{ message: string }> {
return await apiClient.delete(`/api/v1/tts/${ttsId}`)
},
}

View File

@@ -0,0 +1,115 @@
export interface LanguageOption {
code: string
name: string
region?: string
}
export const GTTS_LANGUAGES: LanguageOption[] = [
{ code: 'af', name: 'Afrikaans' },
{ code: 'ar', name: 'Arabic' },
{ code: 'bg', name: 'Bulgarian' },
{ code: 'bn', name: 'Bengali' },
{ code: 'bs', name: 'Bosnian' },
{ code: 'ca', name: 'Catalan' },
{ code: 'cs', name: 'Czech' },
{ code: 'cy', name: 'Welsh' },
{ code: 'da', name: 'Danish' },
{ code: 'de', name: 'German' },
{ code: 'el', name: 'Greek' },
{ code: 'en', name: 'English', region: 'United States' },
{ code: 'en-au', name: 'English', region: 'Australia' },
{ code: 'en-ca', name: 'English', region: 'Canada' },
{ code: 'en-gb', name: 'English', region: 'UK' },
{ code: 'en-ie', name: 'English', region: 'Ireland' },
{ code: 'en-in', name: 'English', region: 'India' },
{ code: 'en-ng', name: 'English', region: 'Nigeria' },
{ code: 'en-nz', name: 'English', region: 'New Zealand' },
{ code: 'en-ph', name: 'English', region: 'Philippines' },
{ code: 'en-za', name: 'English', region: 'South Africa' },
{ code: 'en-tz', name: 'English', region: 'Tanzania' },
{ code: 'en-uk', name: 'English', region: 'United Kingdom' },
{ code: 'en-us', name: 'English', region: 'United States' },
{ code: 'eo', name: 'Esperanto' },
{ code: 'es', name: 'Spanish', region: 'Spain' },
{ code: 'es-es', name: 'Spanish', region: 'Spain' },
{ code: 'es-mx', name: 'Spanish', region: 'Mexico' },
{ code: 'es-us', name: 'Spanish', region: 'United States' },
{ code: 'et', name: 'Estonian' },
{ code: 'eu', name: 'Basque' },
{ code: 'fa', name: 'Persian' },
{ code: 'fi', name: 'Finnish' },
{ code: 'fr', name: 'French', region: 'France' },
{ code: 'fr-ca', name: 'French', region: 'Canada' },
{ code: 'fr-fr', name: 'French', region: 'France' },
{ code: 'ga', name: 'Irish' },
{ code: 'gu', name: 'Gujarati' },
{ code: 'he', name: 'Hebrew' },
{ code: 'hi', name: 'Hindi' },
{ code: 'hr', name: 'Croatian' },
{ code: 'hu', name: 'Hungarian' },
{ code: 'hy', name: 'Armenian' },
{ code: 'id', name: 'Indonesian' },
{ code: 'is', name: 'Icelandic' },
{ code: 'it', name: 'Italian' },
{ code: 'ja', name: 'Japanese' },
{ code: 'jw', name: 'Javanese' },
{ code: 'ka', name: 'Georgian' },
{ code: 'kk', name: 'Kazakh' },
{ code: 'km', name: 'Khmer' },
{ code: 'kn', name: 'Kannada' },
{ code: 'ko', name: 'Korean' },
{ code: 'la', name: 'Latin' },
{ code: 'lv', name: 'Latvian' },
{ code: 'mk', name: 'Macedonian' },
{ code: 'ml', name: 'Malayalam' },
{ code: 'mr', name: 'Marathi' },
{ code: 'ms', name: 'Malay' },
{ code: 'mt', name: 'Maltese' },
{ code: 'my', name: 'Myanmar (Burmese)' },
{ code: 'ne', name: 'Nepali' },
{ code: 'nl', name: 'Dutch' },
{ code: 'no', name: 'Norwegian' },
{ code: 'pa', name: 'Punjabi' },
{ code: 'pl', name: 'Polish' },
{ code: 'pt', name: 'Portuguese', region: 'Brazil' },
{ code: 'pt-br', name: 'Portuguese', region: 'Brazil' },
{ code: 'pt-pt', name: 'Portuguese', region: 'Portugal' },
{ code: 'ro', name: 'Romanian' },
{ code: 'ru', name: 'Russian' },
{ code: 'si', name: 'Sinhala' },
{ code: 'sk', name: 'Slovak' },
{ code: 'sl', name: 'Slovenian' },
{ code: 'sq', name: 'Albanian' },
{ code: 'sr', name: 'Serbian' },
{ code: 'su', name: 'Sundanese' },
{ code: 'sv', name: 'Swedish' },
{ code: 'sw', name: 'Swahili' },
{ code: 'ta', name: 'Tamil' },
{ code: 'te', name: 'Telugu' },
{ code: 'th', name: 'Thai' },
{ code: 'tl', name: 'Filipino' },
{ code: 'tr', name: 'Turkish' },
{ code: 'uk', name: 'Ukrainian' },
{ code: 'ur', name: 'Urdu' },
{ code: 'vi', name: 'Vietnamese' },
{ code: 'yo', name: 'Yoruba' },
{ code: 'zh', name: 'Chinese (Mandarin)' },
{ code: 'zh-cn', name: 'Chinese', region: 'China' },
{ code: 'zh-tw', name: 'Chinese', region: 'Taiwan' },
{ code: 'zu', name: 'Zulu' }
]
export function getLanguageDisplayName(lang: LanguageOption): string {
if (lang.region) {
return `${lang.name} (${lang.region}) - ${lang.code}`
}
return `${lang.name} - ${lang.code}`
}
export function getSortedLanguages(): LanguageOption[] {
return [...GTTS_LANGUAGES].sort((a, b) => {
const aDisplay = getLanguageDisplayName(a)
const bDisplay = getLanguageDisplayName(b)
return aDisplay.localeCompare(bDisplay)
})
}

View File

@@ -37,6 +37,7 @@ export const playerEvents = new EventEmitter()
export const soundEvents = new EventEmitter() export const soundEvents = new EventEmitter()
export const userEvents = new EventEmitter() export const userEvents = new EventEmitter()
export const extractionEvents = new EventEmitter() export const extractionEvents = new EventEmitter()
export const ttsEvents = new EventEmitter()
// Auth event types // Auth event types
export const AUTH_EVENTS = { export const AUTH_EVENTS = {
@@ -69,3 +70,11 @@ export const EXTRACTION_EVENTS = {
EXTRACTION_COMPLETED: 'extraction_completed', EXTRACTION_COMPLETED: 'extraction_completed',
EXTRACTION_FAILED: 'extraction_failed', EXTRACTION_FAILED: 'extraction_failed',
} as const } as const
// TTS event types
export const TTS_EVENTS = {
TTS_STATUS_UPDATED: 'tts_status_updated',
TTS_CREATED: 'tts_created',
TTS_COMPLETED: 'tts_completed',
TTS_FAILED: 'tts_failed',
} as const

View File

@@ -3,6 +3,7 @@ import { DashboardHeader } from '@/components/dashboard/DashboardHeader'
import { ErrorState, LoadingSkeleton } from '@/components/dashboard/DashboardLoadingStates' import { ErrorState, LoadingSkeleton } from '@/components/dashboard/DashboardLoadingStates'
import { StatisticsGrid } from '@/components/dashboard/StatisticsGrid' import { StatisticsGrid } from '@/components/dashboard/StatisticsGrid'
import { TopSoundsSection } from '@/components/dashboard/TopSoundsSection' import { TopSoundsSection } from '@/components/dashboard/TopSoundsSection'
import { TopUsersSection } from '@/components/dashboard/TopUsersSection'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
interface SoundboardStatistics { interface SoundboardStatistics {
@@ -19,6 +20,13 @@ interface TrackStatistics {
total_size: number total_size: number
} }
interface TTSStatistics {
sound_count: number
total_play_count: number
total_duration: number
total_size: number
}
interface TopSound { interface TopSound {
id: number id: number
name: string name: string
@@ -28,11 +36,19 @@ interface TopSound {
created_at: string | null created_at: string | null
} }
interface TopUser {
id: number
name: string
count: number
}
export function DashboardPage() { export function DashboardPage() {
const [soundboardStatistics, setSoundboardStatistics] = const [soundboardStatistics, setSoundboardStatistics] =
useState<SoundboardStatistics | null>(null) useState<SoundboardStatistics | null>(null)
const [trackStatistics, setTrackStatistics] = const [trackStatistics, setTrackStatistics] =
useState<TrackStatistics | null>(null) useState<TrackStatistics | null>(null)
const [ttsStatistics, setTtsStatistics] =
useState<TTSStatistics | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -44,6 +60,13 @@ export function DashboardPage() {
const [limit, setLimit] = useState(5) const [limit, setLimit] = useState(5)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
// Top users state
const [topUsers, setTopUsers] = useState<TopUser[]>([])
const [topUsersLoading, setTopUsersLoading] = useState(false)
const [metricType, setMetricType] = useState('sounds_played')
const [userPeriod, setUserPeriod] = useState('all_time')
const [userLimit, setUserLimit] = useState(5)
const fetchStatistics = useCallback(async () => { const fetchStatistics = useCallback(async () => {
try { try {
setError(null) // Clear previous errors setError(null) // Clear previous errors
@@ -74,6 +97,19 @@ export function DashboardPage() {
const trackData = await trackResponse.json() const trackData = await trackResponse.json()
setTrackStatistics(trackData) setTrackStatistics(trackData)
// Fetch TTS statistics separately to avoid Promise.all failures
const ttsResponse = await fetch('/api/v1/dashboard/tts-statistics', {
credentials: 'include'
})
if (!ttsResponse.ok) {
const errorText = await ttsResponse.text()
throw new Error(`Failed to fetch TTS statistics: ${errorText}`)
}
const ttsData = await ttsResponse.json()
setTtsStatistics(ttsData)
} catch (err) { } catch (err) {
console.error('Dashboard statistics error:', err) console.error('Dashboard statistics error:', err)
setError(err instanceof Error ? err.message : 'An error occurred') setError(err instanceof Error ? err.message : 'An error occurred')
@@ -134,18 +170,70 @@ export function DashboardPage() {
[soundType, period, limit], [soundType, period, limit],
) )
const fetchTopUsers = useCallback(
async (showLoading = false) => {
try {
if (showLoading) {
setTopUsersLoading(true)
}
const response = await fetch(
`/api/v1/dashboard/top-users?metric_type=${metricType}&period=${userPeriod}&limit=${userLimit}`,
{ credentials: 'include' },
)
if (!response.ok) {
throw new Error('Failed to fetch top users')
}
const data = await response.json()
// Graceful update: merge new data while preserving animations
setTopUsers(prevTopUsers => {
// Create a map of existing users for efficient lookup
const existingUsersMap = new Map(
prevTopUsers.map(user => [user.id, user]),
)
// Update existing users and add new ones
return data.map((newUser: TopUser) => {
const existingUser = existingUsersMap.get(newUser.id)
if (existingUser) {
// Preserve object reference if data hasn't changed to avoid re-renders
if (
existingUser.name === newUser.name &&
existingUser.count === newUser.count
) {
return existingUser
}
}
return newUser
})
})
} catch (err) {
console.error('Failed to fetch top users:', err)
} finally {
if (showLoading) {
setTopUsersLoading(false)
}
}
},
[metricType, userPeriod, userLimit],
)
const refreshAll = useCallback(async () => { const refreshAll = useCallback(async () => {
setRefreshing(true) setRefreshing(true)
try { try {
// Fetch statistics and top sounds sequentially to avoid Promise.all issues // Fetch statistics and top sounds sequentially to avoid Promise.all issues
await fetchStatistics() await fetchStatistics()
await fetchTopSounds() await fetchTopSounds()
await fetchTopUsers()
} catch (err) { } catch (err) {
console.error('Error during refresh:', err) console.error('Error during refresh:', err)
} finally { } finally {
setRefreshing(false) setRefreshing(false)
} }
}, [fetchStatistics, fetchTopSounds]) }, [fetchStatistics, fetchTopSounds, fetchTopUsers])
const retryFromError = useCallback(async () => { const retryFromError = useCallback(async () => {
setLoading(true) setLoading(true)
@@ -174,23 +262,27 @@ export function DashboardPage() {
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
// Only auto-refresh if not currently loading or in error state // Only auto-refresh if not currently loading or in error state
if (!loading && !refreshing && (!error || (soundboardStatistics && trackStatistics))) { if (!loading && !refreshing && (!error || (soundboardStatistics && trackStatistics && ttsStatistics))) {
refreshAll() refreshAll()
} }
}, 30000) // Increased to 30 seconds }, 30000) // Increased to 30 seconds
return () => clearInterval(interval) return () => clearInterval(interval)
}, [refreshAll, loading, refreshing, error, soundboardStatistics, trackStatistics]) }, [refreshAll, loading, refreshing, error, soundboardStatistics, trackStatistics, ttsStatistics])
useEffect(() => { useEffect(() => {
fetchTopSounds(true) // Show loading on initial load and filter changes fetchTopSounds(true) // Show loading on initial load and filter changes
}, [fetchTopSounds]) }, [fetchTopSounds])
useEffect(() => {
fetchTopUsers(true) // Show loading on initial load and filter changes
}, [fetchTopUsers])
if (loading) { if (loading) {
return <LoadingSkeleton /> return <LoadingSkeleton />
} }
if (error && (!soundboardStatistics || !trackStatistics)) { if (error && (!soundboardStatistics || !trackStatistics || !ttsStatistics)) {
return <ErrorState error={error} onRetry={retryFromError} /> return <ErrorState error={error} onRetry={retryFromError} />
} }
@@ -204,10 +296,11 @@ export function DashboardPage() {
<DashboardHeader onRefresh={refreshAll} isRefreshing={refreshing} /> <DashboardHeader onRefresh={refreshAll} isRefreshing={refreshing} />
<div className="space-y-6"> <div className="space-y-6">
{soundboardStatistics && trackStatistics && ( {soundboardStatistics && trackStatistics && ttsStatistics && (
<StatisticsGrid <StatisticsGrid
soundboardStatistics={soundboardStatistics} soundboardStatistics={soundboardStatistics}
trackStatistics={trackStatistics} trackStatistics={trackStatistics}
ttsStatistics={ttsStatistics}
/> />
)} )}
@@ -221,6 +314,17 @@ export function DashboardPage() {
onPeriodChange={setPeriod} onPeriodChange={setPeriod}
onLimitChange={setLimit} onLimitChange={setLimit}
/> />
<TopUsersSection
topUsers={topUsers}
loading={topUsersLoading}
metricType={metricType}
period={userPeriod}
limit={userLimit}
onMetricTypeChange={setMetricType}
onPeriodChange={setUserPeriod}
onLimitChange={setUserLimit}
/>
</div> </div>
</div> </div>
</AppLayout> </AppLayout>

View File

@@ -16,6 +16,7 @@ import { SimpleSortableRow } from '@/components/playlists/playlist-edit/SimpleSo
import { SortableTableRow } from '@/components/playlists/playlist-edit/SortableTableRow' import { SortableTableRow } from '@/components/playlists/playlist-edit/SortableTableRow'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { import {
Table, Table,
@@ -46,7 +47,7 @@ import {
SortableContext, SortableContext,
verticalListSortingStrategy, verticalListSortingStrategy,
} from '@dnd-kit/sortable' } from '@dnd-kit/sortable'
import { Minus, Music, Plus, RefreshCw } from 'lucide-react' import { Minus, Music, Plus, RefreshCw, Search, X } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router' import { useNavigate, useParams } from 'react-router'
import { toast } from 'sonner' import { toast } from 'sonner'
@@ -74,6 +75,7 @@ export function PlaylistEditPage() {
Sound | PlaylistSound | null Sound | PlaylistSound | null
>(null) >(null)
const [dropPosition, setDropPosition] = useState<number | null>(null) const [dropPosition, setDropPosition] = useState<number | null>(null)
const [searchQuery, setSearchQuery] = useState('')
// dnd-kit sensors // dnd-kit sensors
const sensors = useSensors( const sensors = useSensors(
@@ -152,10 +154,18 @@ export function PlaylistEditPage() {
if (!isAddMode) { if (!isAddMode) {
// Entering add mode - fetch available sounds // Entering add mode - fetch available sounds
await fetchAvailableSounds() await fetchAvailableSounds()
} else {
// Exiting add mode - clear search
setSearchQuery('')
} }
setIsAddMode(!isAddMode) setIsAddMode(!isAddMode)
} }
// Filter available sounds based on search query
const filteredAvailableSounds = availableSounds.filter(sound =>
sound.name.toLowerCase().includes(searchQuery.toLowerCase())
)
useEffect(() => { useEffect(() => {
if (!isNaN(playlistId)) { if (!isNaN(playlistId)) {
fetchPlaylist() fetchPlaylist()
@@ -807,9 +817,30 @@ export function PlaylistEditPage() {
{/* Available Sounds */} {/* Available Sounds */}
<div className="space-y-2"> <div className="space-y-2">
<h4 className="font-medium text-sm text-muted-foreground mb-3"> <div className="flex items-center justify-between mb-3">
Available EXT Sounds ({availableSounds.length}) <h4 className="font-medium text-sm text-muted-foreground">
</h4> Available EXT Sounds ({filteredAvailableSounds.length}/{availableSounds.length})
</h4>
</div>
<div className="relative mb-3">
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search sounds..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8 pr-8"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-2 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
title="Clear search"
>
<X className="h-4 w-4" />
</button>
)}
</div>
{availableSoundsLoading ? ( {availableSoundsLoading ? (
<div className="space-y-2"> <div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => ( {Array.from({ length: 3 }).map((_, i) => (
@@ -824,15 +855,23 @@ export function PlaylistEditPage() {
All EXT sounds are already in this playlist All EXT sounds are already in this playlist
</p> </p>
</div> </div>
) : filteredAvailableSounds.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<Search className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No sounds match your search</p>
<p className="text-xs mt-1">
Try a different search term
</p>
</div>
) : ( ) : (
<SortableContext <SortableContext
items={availableSounds.map( items={filteredAvailableSounds.map(
sound => `available-sound-${sound.id}`, sound => `available-sound-${sound.id}`,
)} )}
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
<div className="space-y-2 max-h-96 overflow-y-auto"> <div className="space-y-2 max-h-96 overflow-y-auto">
{availableSounds.map(sound => ( {filteredAvailableSounds.map(sound => (
<AvailableSound <AvailableSound
key={sound.id} key={sound.id}
sound={sound} sound={sound}

View File

@@ -189,6 +189,53 @@ export function PlaylistsPage() {
} }
} }
const handleDeletePlaylist = async (playlist: Playlist) => {
// Protect main playlist from deletion
if (playlist.is_main) {
toast.error('The main playlist cannot be deleted')
return
}
// Check if playlist is deletable
if (!playlist.is_deletable) {
toast.error('This playlist cannot be deleted')
return
}
// Confirm deletion
const confirmMessage = `Are you sure you want to delete the playlist "${playlist.name}"?${
playlist.sound_count > 0
? `\n\nThis playlist contains ${playlist.sound_count} sound${playlist.sound_count !== 1 ? 's' : ''}. The sounds will not be deleted, only removed from this playlist.`
: ''
}\n\nThis action cannot be undone.`
if (!confirm(confirmMessage)) {
return
}
try {
await playlistsService.deletePlaylist(playlist.id)
toast.success(`Playlist "${playlist.name}" deleted successfully`)
// Remove the deleted playlist from the local state
setPlaylists(prevPlaylists =>
prevPlaylists.filter(p => p.id !== playlist.id)
)
// Update total count
setTotalCount(prev => prev - 1)
// If current page is now empty and not the first page, go to previous page
const remainingOnCurrentPage = playlists.length - 1
if (remainingOnCurrentPage === 0 && currentPage > 1) {
setCurrentPage(currentPage - 1)
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to delete playlist'
toast.error(errorMessage)
}
}
const renderContent = () => { const renderContent = () => {
if (loading) { if (loading) {
return <PlaylistsLoading /> return <PlaylistsLoading />
@@ -209,6 +256,7 @@ export function PlaylistsPage() {
onEdit={handleEditPlaylist} onEdit={handleEditPlaylist}
onSetCurrent={handleSetCurrent} onSetCurrent={handleSetCurrent}
onFavoriteToggle={handleFavoriteToggle} onFavoriteToggle={handleFavoriteToggle}
onDelete={handleDeletePlaylist}
/> />
<AppPagination <AppPagination
currentPage={currentPage} currentPage={currentPage}

View File

@@ -100,11 +100,6 @@ export function SchedulersPage() {
setShowCreateDialog(false) setShowCreateDialog(false)
} }
const handleTaskUpdated = (updatedTask: ScheduledTask) => {
setTasks(prev => prev.map(task =>
task.id === updatedTask.id ? updatedTask : task
))
}
const handleTaskDeleted = (taskId: number) => { const handleTaskDeleted = (taskId: number) => {
setTasks(prev => prev.filter(task => task.id !== taskId)) setTasks(prev => prev.filter(task => task.id !== taskId))
@@ -132,7 +127,6 @@ export function SchedulersPage() {
return ( return (
<SchedulersTable <SchedulersTable
tasks={tasks} tasks={tasks}
onTaskUpdated={handleTaskUpdated}
onTaskDeleted={handleTaskDeleted} onTaskDeleted={handleTaskDeleted}
/> />
) )

View File

@@ -22,6 +22,7 @@ import { SOUND_EVENTS, soundEvents } from '@/lib/events'
import { useSocket } from '@/contexts/SocketContext' import { useSocket } from '@/contexts/SocketContext'
import { import {
AlertCircle, AlertCircle,
Filter,
Heart, Heart,
RefreshCw, RefreshCw,
Search, Search,
@@ -90,6 +91,7 @@ export function SoundsPage() {
const [sortBy, setSortBy] = useState<SoundSortField>('name') const [sortBy, setSortBy] = useState<SoundSortField>('name')
const [sortOrder, setSortOrder] = useState<SortOrder>('asc') const [sortOrder, setSortOrder] = useState<SortOrder>('asc')
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false) const [showFavoritesOnly, setShowFavoritesOnly] = useState(false)
const [typeFilter, setTypeFilter] = useState<'all' | 'SDB' | 'TTS'>('all')
const handlePlaySound = async (sound: Sound) => { const handlePlaySound = async (sound: Sound) => {
// If WebSocket is connected, use WebSocket for immediate response // If WebSocket is connected, use WebSocket for immediate response
@@ -159,13 +161,18 @@ export function SoundsPage() {
try { try {
setLoading(true) setLoading(true)
setError(null) setError(null)
const sdbSounds = await soundsService.getSDBSounds({
// Determine types to filter by
const types = typeFilter === 'all' ? ['SDB', 'TTS'] : [typeFilter]
const sounds = await soundsService.getSounds({
types,
search: debouncedSearchQuery.trim() || undefined, search: debouncedSearchQuery.trim() || undefined,
sort_by: sortBy, sort_by: sortBy,
sort_order: sortOrder, sort_order: sortOrder,
favorites_only: showFavoritesOnly, favorites_only: showFavoritesOnly,
}) })
setSounds(sdbSounds) setSounds(sounds)
} catch (err) { } catch (err) {
const errorMessage = const errorMessage =
err instanceof Error ? err.message : 'Failed to fetch sounds' err instanceof Error ? err.message : 'Failed to fetch sounds'
@@ -189,7 +196,7 @@ export function SoundsPage() {
useEffect(() => { useEffect(() => {
fetchSounds() fetchSounds()
}, [debouncedSearchQuery, sortBy, sortOrder, showFavoritesOnly]) }, [debouncedSearchQuery, sortBy, sortOrder, showFavoritesOnly, typeFilter])
// Listen for sound_played events and update play_count // Listen for sound_played events and update play_count
useEffect(() => { useEffect(() => {
@@ -290,7 +297,9 @@ export function SoundsPage() {
<p className="text-muted-foreground"> <p className="text-muted-foreground">
{showFavoritesOnly {showFavoritesOnly
? 'You haven\'t favorited any sounds yet. Click the heart icon on sounds to add them to your favorites.' ? 'You haven\'t favorited any sounds yet. Click the heart icon on sounds to add them to your favorites.'
: 'No SDB type sounds are available in your library.' : typeFilter === 'all'
? 'No sounds are available in your library.'
: `No ${typeFilter} type sounds are available in your library.`
} }
</p> </p>
</div> </div>
@@ -298,7 +307,7 @@ export function SoundsPage() {
} }
return ( return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
{sounds.map((sound, idx) => ( {sounds.map((sound, idx) => (
<SoundCard <SoundCard
key={sound.id} key={sound.id}
@@ -362,6 +371,21 @@ export function SoundsPage() {
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Select
value={typeFilter}
onValueChange={value => setTypeFilter(value as 'all' | 'SDB' | 'TTS')}
>
<SelectTrigger className="w-[160px]">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="SDB">Soundboard</SelectItem>
<SelectItem value="TTS">TTS</SelectItem>
</SelectContent>
</Select>
<Select <Select
value={sortBy} value={sortBy}
onValueChange={value => setSortBy(value as SoundSortField)} onValueChange={value => setSortBy(value as SoundSortField)}

201
src/pages/TTSPage.tsx Normal file
View File

@@ -0,0 +1,201 @@
import { AppLayout } from '@/components/AppLayout'
import { AppPagination } from '@/components/AppPagination'
import { CreateTTSDialog } from '@/components/tts/CreateTTSDialog'
import { TTSHeader } from '@/components/tts/TTSHeader'
import {
TTSEmpty,
TTSError,
TTSLoading,
} from '@/components/tts/TTSLoadingStates'
import { TTSTable } from '@/components/tts/TTSTable'
import {
type TTSResponse,
type TTSSortField,
type TTSSortOrder,
ttsService,
} from '@/lib/api/services/tts'
import { TTS_EVENTS, ttsEvents } from '@/lib/events'
import { useCallback, useEffect, useState } from 'react'
import { toast } from 'sonner'
export function TTSPage() {
const [ttsHistory, setTTSHistory] = useState<TTSResponse[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Search and sorting state
const [searchQuery, setSearchQuery] = useState('')
const [sortBy, setSortBy] = useState<TTSSortField>('created_at')
const [sortOrder, setSortOrder] = useState<TTSSortOrder>('desc')
// Pagination state
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [totalCount, setTotalCount] = useState(0)
const [pageSize, setPageSize] = useState(10)
// Create TTS dialog state
const [showCreateDialog, setShowCreateDialog] = useState(false)
// Debounce search query
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearchQuery(searchQuery)
}, 300)
return () => clearTimeout(handler)
}, [searchQuery])
const fetchTTSHistory = useCallback(async () => {
try {
setLoading(true)
setError(null)
const response = await ttsService.getTTSHistory({
search: debouncedSearchQuery.trim() || undefined,
sort_by: sortBy,
sort_order: sortOrder,
page: currentPage,
limit: pageSize,
})
setTTSHistory(response.tts)
setTotalPages(response.total_pages)
setTotalCount(response.total)
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Failed to fetch TTS history'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
}, [debouncedSearchQuery, sortBy, sortOrder, currentPage, pageSize])
useEffect(() => {
fetchTTSHistory()
}, [fetchTTSHistory])
// Reset to page 1 when filters change
useEffect(() => {
if (currentPage !== 1) {
setCurrentPage(1)
}
}, [debouncedSearchQuery, sortBy, sortOrder, pageSize])
// Listen for TTS events to refresh the list
useEffect(() => {
const handleTTSCompleted = () => {
fetchTTSHistory()
}
const handleTTSFailed = () => {
fetchTTSHistory()
}
const handleTTSCreated = () => {
fetchTTSHistory()
}
// Subscribe to TTS events
ttsEvents.on(TTS_EVENTS.TTS_COMPLETED, handleTTSCompleted)
ttsEvents.on(TTS_EVENTS.TTS_FAILED, handleTTSFailed)
ttsEvents.on(TTS_EVENTS.TTS_CREATED, handleTTSCreated)
return () => {
// Cleanup event listeners
ttsEvents.off(TTS_EVENTS.TTS_COMPLETED, handleTTSCompleted)
ttsEvents.off(TTS_EVENTS.TTS_FAILED, handleTTSFailed)
ttsEvents.off(TTS_EVENTS.TTS_CREATED, handleTTSCreated)
}
}, [fetchTTSHistory])
const handlePageChange = (page: number) => {
setCurrentPage(page)
}
const handlePageSizeChange = (size: number) => {
setPageSize(size)
setCurrentPage(1) // Reset to first page when changing page size
}
const handleTTSDeleted = (ttsId: number) => {
// Remove the deleted TTS from the current list
setTTSHistory(prev => prev.filter(tts => tts.id !== ttsId))
// Update total count
setTotalCount(prev => prev - 1)
// If current page is now empty and not the first page, go to previous page
const remainingOnCurrentPage = ttsHistory.length - 1
if (remainingOnCurrentPage === 0 && currentPage > 1) {
setCurrentPage(currentPage - 1)
}
// Refresh the full list to ensure accuracy
fetchTTSHistory()
}
const renderContent = () => {
if (loading) {
return <TTSLoading />
}
if (error) {
return <TTSError error={error} onRetry={fetchTTSHistory} />
}
if (!ttsHistory || ttsHistory.length === 0) {
return <TTSEmpty searchQuery={searchQuery} />
}
return (
<div className="space-y-4">
<TTSTable
ttsHistory={ttsHistory}
onTTSDeleted={handleTTSDeleted}
/>
<AppPagination
currentPage={currentPage}
totalPages={totalPages}
totalCount={totalCount}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
itemName="TTS generations"
/>
</div>
)
}
return (
<AppLayout
breadcrumb={{
items: [{ label: 'Dashboard', href: '/' }, { label: 'Text to Speech' }],
}}
>
<div className="flex-1 rounded-xl bg-muted/50 p-4">
<TTSHeader
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
sortBy={sortBy}
onSortByChange={setSortBy}
sortOrder={sortOrder}
onSortOrderChange={setSortOrder}
onRefresh={fetchTTSHistory}
onCreateClick={() => setShowCreateDialog(true)}
loading={loading}
error={error}
ttsCount={totalCount}
/>
<CreateTTSDialog
open={showCreateDialog}
onOpenChange={setShowCreateDialog}
/>
{renderContent()}
</div>
</AppLayout>
)
}

View File

@@ -25,6 +25,61 @@ export default defineConfig({
build: { build: {
outDir: 'dist', outDir: 'dist',
sourcemap: false, // Disable source maps in production for security sourcemap: false, // Disable source maps in production for security
rollupOptions: {
output: {
manualChunks: {
// React core
'react': ['react', 'react-dom', 'react-router'],
// UI library - Radix UI components
'radix-ui': [
'@radix-ui/react-avatar',
'@radix-ui/react-checkbox',
'@radix-ui/react-context-menu',
'@radix-ui/react-dialog',
'@radix-ui/react-dropdown-menu',
'@radix-ui/react-label',
'@radix-ui/react-popover',
'@radix-ui/react-progress',
'@radix-ui/react-scroll-area',
'@radix-ui/react-select',
'@radix-ui/react-separator',
'@radix-ui/react-slider',
'@radix-ui/react-slot',
'@radix-ui/react-switch',
'@radix-ui/react-tabs',
'@radix-ui/react-tooltip',
],
// Drag and drop
'dnd-kit': [
'@dnd-kit/core',
'@dnd-kit/sortable',
'@dnd-kit/utilities',
],
// Utilities
'utils': [
'clsx',
'tailwind-merge',
'class-variance-authority',
'date-fns',
],
// Other libraries
'misc': [
'recharts',
'socket.io-client',
'sonner',
'next-themes',
'cmdk',
'react-day-picker',
'@number-flow/react',
'lucide-react',
],
},
},
},
}, },
// Preview server configuration (for testing built version) // Preview server configuration (for testing built version)
preview: { preview: {