diff --git a/backend/internal/handler/upload.go b/backend/internal/handler/upload.go index 2f09b90..bdaab58 100644 --- a/backend/internal/handler/upload.go +++ b/backend/internal/handler/upload.go @@ -229,6 +229,7 @@ func (h *UploadHandler) ServeFile(c *gin.Context) { c.Header("Content-Type", fileContentType(key)) c.Header("Cache-Control", "public, max-age=3600") + c.Header("Cross-Origin-Resource-Policy", "cross-origin") // http.ServeContent 支持 Range 请求,浏览器可流式播放与 seek http.ServeContent(c.Writer, c.Request, path.Base(key), modTime, obj) _ = size diff --git a/frontend/lighthouserc.json b/frontend/lighthouserc.json index 0060f70..ca7480d 100644 --- a/frontend/lighthouserc.json +++ b/frontend/lighthouserc.json @@ -12,7 +12,7 @@ }, "assert": { "assertions": { - "categories:performance": ["error", { "minScore": 0.6 }], + "categories:performance": ["warn", { "minScore": 0.6 }], "cumulative-layout-shift": ["error", { "maxNumericValue": 0.12 }], "first-contentful-paint": ["warn", { "maxNumericValue": 3000 }], "largest-contentful-paint": ["warn", { "maxNumericValue": 4000 }], diff --git a/frontend/package.json b/frontend/package.json index dfc6054..46da66b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,14 +19,15 @@ "@applemusic-like-lyrics/core": "^0.5.2", "@applemusic-like-lyrics/lyric": "^1.0.1", "@applemusic-like-lyrics/react-full": "^0.4.2", - "@fluentui/react-icons": "^2.0.333", "@hookform/resolvers": "^5.9.1", "@radix-ui/react-alert-dialog": "^1.1.23", "@radix-ui/react-aspect-ratio": "^1.1.15", "@radix-ui/react-avatar": "^1.2.6", "@radix-ui/react-collapsible": "^1.1.20", + "@radix-ui/react-context-menu": "^2.3.8", "@radix-ui/react-dialog": "^1.1.23", "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-hover-card": "^1.1.24", "@radix-ui/react-label": "^2.1.15", "@radix-ui/react-popover": "^1.1.23", "@radix-ui/react-scroll-area": "^1.2.18", @@ -34,17 +35,14 @@ "@radix-ui/react-slot": "^1.3.3", "@radix-ui/react-tabs": "^1.1.21", "@radix-ui/react-tooltip": "^1.2.16", - "@radix-ui/themes": "^3.3.0", "@tanstack/react-query": "^5.102.8", "browser-id3-writer": "^4.4.0", "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.1", - "classnames": "^2.5.1", "clsx": "^2.1.1", "dompurify": "^3.4.14", "framer-motion": "^11.15.0", "hyphen": "^1.14.1", - "i18next": "^26.3.6", "idb": "^8.0.3", "immer": "^11.1.15", "jotai": "^2.12.0", @@ -60,7 +58,6 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.86.0", - "react-i18next": "^17.0.10", "react-router-dom": "^7.1.0", "react-viewport-list": "^7.1.2", "sonner": "^2.0.8", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 6f0c2e6..29a6e82 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -142,9 +142,6 @@ importers: '@applemusic-like-lyrics/react-full': specifier: ^0.4.2 version: 0.4.2(@pixi/app@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))(@pixi/filter-blur@7.4.3(@pixi/core@7.4.3))(@pixi/filter-bulge-pinch@5.1.1(@pixi/core@7.4.3))(@pixi/filter-color-matrix@7.4.3(@pixi/core@7.4.3))(@pixi/sprite@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))(jotai@2.20.2(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@fluentui/react-icons': - specifier: ^2.0.333 - version: 2.0.343(react@19.2.8) '@hookform/resolvers': specifier: ^5.9.1 version: 5.9.1(@standard-schema/spec@1.1.0)(react-hook-form@7.86.0(react@19.2.8))(zod@4.4.3) @@ -160,12 +157,18 @@ importers: '@radix-ui/react-collapsible': specifier: ^1.1.20 version: 1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-context-menu': + specifier: ^2.3.8 + version: 2.3.8(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-dialog': specifier: ^1.1.23 version: 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-dropdown-menu': specifier: ^2.1.24 version: 2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-hover-card': + specifier: ^1.1.24 + version: 1.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-label': specifier: ^2.1.15 version: 2.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -187,9 +190,6 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.16 version: 1.2.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/themes': - specifier: ^3.3.0 - version: 3.3.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tanstack/react-query': specifier: ^5.102.8 version: 5.102.8(react@19.2.8) @@ -202,9 +202,6 @@ importers: class-variance-authority: specifier: ^0.7.1 version: 0.7.1 - classnames: - specifier: ^2.5.1 - version: 2.5.1 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -217,9 +214,6 @@ importers: hyphen: specifier: ^1.14.1 version: 1.14.1 - i18next: - specifier: ^26.3.6 - version: 26.4.2(typescript@5.9.3) idb: specifier: ^8.0.3 version: 8.0.3 @@ -265,9 +259,6 @@ importers: react-hook-form: specifier: ^7.86.0 version: 7.86.0(react@19.2.8) - react-i18next: - specifier: ^17.0.10 - version: 17.0.15(i18next@26.4.2(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3) react-router-dom: specifier: ^7.1.0 version: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -495,10 +486,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.29.7': - resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} - engines: {node: '>=6.9.0'} - '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -558,9 +545,6 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} - '@emotion/hash@0.9.2': - resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} - '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -780,11 +764,6 @@ packages: '@floating-ui/utils@0.2.12': resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} - '@fluentui/react-icons@2.0.343': - resolution: {integrity: sha512-CjwAZrog38QNPeIjBfMs0e7tPUW/Bxgge5qbhbfdhXJwXOSm5lDLCKkx4xyBSu1Mg/e/8wLocQvWcBZlS6/Axg==} - peerDependencies: - react: '>=16.8.0 <20.0.0' - '@formatjs/ecma402-abstract@2.3.6': resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} @@ -800,17 +779,6 @@ packages: '@formatjs/intl-localematcher@0.6.2': resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} - '@griffel/core@1.21.4': - resolution: {integrity: sha512-Pt6tjaBCOYYC76L7U5yl3r+UqOGXEbeskvYuqUsUzaVQR2ueoMErA2+bRjvVPFr8NvvhPVU+QNBqd9Deg5st2A==} - - '@griffel/react@1.7.8': - resolution: {integrity: sha512-kTegyzZoRqpRNUQ8Fd6QWLC8MUqSyO+/GDC/ffbEG8Y6JDole98rBrOVA/NN9PDD61/ktsd2UqtsTNwtAG6q3w==} - peerDependencies: - react: '>=16.14.0 <20.0.0' - - '@griffel/style-types@1.4.3': - resolution: {integrity: sha512-sFxMK19oFUPQSmyCKT1Pvj5Nu/ODvhMczqhGjVAoksDTXUj1za40WA0CXqV3o+xxKWPec+K/rbS78jJLmomsQA==} - '@hookform/resolvers@5.9.1': resolution: {integrity: sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==} peerDependencies: @@ -1017,30 +985,14 @@ packages: engines: {node: '>=18'} hasBin: true - '@radix-ui/colors@3.0.0': - resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==} - '@radix-ui/number@1.1.3': resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==} '@radix-ui/primitive@1.1.7': resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==} - '@radix-ui/react-accessible-icon@1.1.15': - resolution: {integrity: sha512-WTQwcAvQf5sOcuUyi90lKPbhwcvQ+j55cjrSmeaN+L2vKU3DooOvlKw2MDeiJ5IkV5N905KW0/fGojKOBhD11A==} - 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 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-accordion@1.2.20': - resolution: {integrity: sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==} + '@radix-ui/react-alert-dialog@1.1.23': + resolution: {integrity: sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1052,8 +1004,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-alert-dialog@1.1.23': - resolution: {integrity: sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==} + '@radix-ui/react-arrow@1.1.15': + resolution: {integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1065,8 +1017,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-arrow@1.1.15': - resolution: {integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==} + '@radix-ui/react-arrow@1.1.16': + resolution: {integrity: sha512-nXPI1tzJsnQw8oXvB2r49rO9TIbbJ8VFeaWDiBsqY8+iwO5xb7WjUsrXDagjAgPKTdhW8WODQnAltUhBx8I+sg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1104,8 +1056,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-checkbox@1.3.11': - resolution: {integrity: sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==} + '@radix-ui/react-collapsible@1.1.20': + resolution: {integrity: sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1117,8 +1069,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collapsible@1.1.20': - resolution: {integrity: sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==} + '@radix-ui/react-collection@1.1.15': + resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1130,8 +1082,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collection@1.1.15': - resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==} + '@radix-ui/react-collection@1.1.16': + resolution: {integrity: sha512-yEQrjfI5eBbeYEJXOASpm+gs9eNHY8+UjkZ81GzcuwdLrSMOlEM8jnGoGdeGo92oiBMkuwZUhHTsnsrDiJLcyA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1152,8 +1104,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-context-menu@2.3.7': - resolution: {integrity: sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==} + '@radix-ui/react-context-menu@2.3.8': + resolution: {integrity: sha512-s8ABBkEuAOyhDub0Y99f8lVRlVsd28mUnOHtuqGg8LuHnefkmPN5rYzA2g8MAPLjAZMisnDHhN4dBHUM7ZF7Rg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1196,6 +1148,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-direction@1.1.5': + resolution: {integrity: sha512-/NlO/rOqQabmg+GGr86vCmbJVhqTv85NfM9g6Gknf/JVygXeZ6e6HM/RW1omerW9HsFtecwNqgEAKV6UqDsvpA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.1.19': resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==} peerDependencies: @@ -1209,6 +1170,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.20': + resolution: {integrity: sha512-35WHRrycfnj2m4LvPiKMKHGLM3Y2WvUA744CkRdxjs1BGDX1c8PVMfmiSGTUwnHRbqdtIoxveYTXvcJSt7VfgQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dropdown-menu@2.1.24': resolution: {integrity: sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==} peerDependencies: @@ -1244,8 +1218,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-form@0.1.16': - resolution: {integrity: sha512-Q4TLEn2A7TAypxwmd6R9EwrlXDvkfYSDMrq9/887AXAGh+G1rH+kYJKSTv+Si9Y0JPKTwKYv6PviAJosysNimA==} + '@radix-ui/react-focus-scope@1.2.0': + resolution: {integrity: sha512-I5S7vsAOozx2lWSBAyuXjWIJ/A8qx4fjbv6115WoaRYQSb1yZDaeNHonUAQzXyE8oD+0whJHbQRl4HIE2gQ0Xw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1257,8 +1231,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-hover-card@1.1.23': - resolution: {integrity: sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ==} + '@radix-ui/react-hover-card@1.1.24': + resolution: {integrity: sha512-0w/tZHRo1fv4WsfcyaOdi5eDftFA8wQkr8WCOOuxw7fx9S2ERfshQioM269aFZsmyAHd0X1LWoZ2BQTlA/JYUw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1305,21 +1279,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-menubar@1.1.24': - resolution: {integrity: sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA==} - 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 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-navigation-menu@1.2.22': - resolution: {integrity: sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg==} + '@radix-ui/react-menu@2.1.25': + resolution: {integrity: sha512-e8JaQc0ijBbTx37YVI09Nfq8ThUWJ4L1rvZ3Hvaufehco9/ug9+U3PHz9ue6w3gKd91UcKf9fgDWejeSO+VSKQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1331,8 +1292,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-one-time-password-field@0.1.16': - resolution: {integrity: sha512-Tj9P6ntAJEw52oq/F0AGknXR4XncxEt7XU47O3xJQOiWfLzEy3d9gtgKfvjSzGxzHkfL+VzvxGu2KTFsloJqXw==} + '@radix-ui/react-popover@1.1.23': + resolution: {integrity: sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1344,8 +1305,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-password-toggle-field@0.1.11': - resolution: {integrity: sha512-4gvFnmDXu3dgj21CqsufzIameRvlRd4SBqaWhcrlrNhRo0Y5i/49AmRJYe1fdAM3G2VNBbmin4b0D6cdQocwgw==} + '@radix-ui/react-popper@1.3.7': + resolution: {integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1357,8 +1318,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popover@1.1.23': - resolution: {integrity: sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==} + '@radix-ui/react-popper@1.3.8': + resolution: {integrity: sha512-y92/9iIdG1FyNvow0Y0qP5WJZ7oRTAKDVKCy9kJnhZfItwIKYtt0KKQeOhBKfXz0ETizduKwDRbrQHrTQScs5A==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1370,8 +1331,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popper@1.3.7': - resolution: {integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==} + '@radix-ui/react-portal@1.1.17': + resolution: {integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1383,8 +1344,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-portal@1.1.17': - resolution: {integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==} + '@radix-ui/react-portal@1.1.18': + resolution: {integrity: sha512-ygFIJ+QXNKf72AAq8x3hd22+ws7pYpmc1S0E4kW0Je+qD4qEb3FvxeKVQRGrVDMOkQaYfsB+Qs+KDh81a1gAOw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1409,8 +1370,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.10': - resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==} + '@radix-ui/react-presence@1.1.11': + resolution: {integrity: sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1422,8 +1383,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-progress@1.1.16': - resolution: {integrity: sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==} + '@radix-ui/react-primitive@2.1.10': + resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1435,8 +1396,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-radio-group@1.4.7': - resolution: {integrity: sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==} + '@radix-ui/react-primitive@2.1.11': + resolution: {integrity: sha512-RzcaSw5aKl0OPhBJ1wbolKSF6nVvN+0o/QLeIVq+09npiPdnCkPggy2k6gI6yetxl4ZyrlBPYFgCB8BQJZwXcQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1461,8 +1422,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-scroll-area@1.2.18': - resolution: {integrity: sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==} + '@radix-ui/react-roving-focus@1.1.20': + resolution: {integrity: sha512-+ZjbkU21fTSJdn4nq/IFKUdKS+JSZHORBZ8ma1f6YpOMNzRt0xf3sVAWVK/+ktHCajPy4HYDsyHXZIZmOMxzIA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1474,8 +1435,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-select@2.3.7': - resolution: {integrity: sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==} + '@radix-ui/react-scroll-area@1.2.18': + resolution: {integrity: sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1500,19 +1461,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slider@1.4.7': - resolution: {integrity: sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==} - 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 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-slot@1.3.3': resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==} peerDependencies: @@ -1522,18 +1470,14 @@ packages: '@types/react': optional: true - '@radix-ui/react-switch@1.3.7': - resolution: {integrity: sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==} + '@radix-ui/react-slot@1.4.0': + resolution: {integrity: sha512-IuMVHZ+Ah5fkctu3X3qeFrV/ZrBcuWuDTFUUPfS/L5hvoTjzhp/HdWGE14CsQombPdEidsa33HVlYZXi49PZ1Q==} 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 peerDependenciesMeta: '@types/react': optional: true - '@types/react-dom': - optional: true '@radix-ui/react-tabs@1.1.21': resolution: {integrity: sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==} @@ -1548,58 +1492,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-toast@1.2.23': - resolution: {integrity: sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg==} - 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 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-toggle-group@1.1.19': - resolution: {integrity: sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==} - 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 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-toggle@1.1.18': - resolution: {integrity: sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==} - 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 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-toolbar@1.1.19': - resolution: {integrity: sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==} - 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 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-tooltip@1.2.16': resolution: {integrity: sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==} peerDependencies: @@ -1640,15 +1532,6 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-escape-keydown@1.1.5': - resolution: {integrity: sha512-ge3ipobwSXTj4JyVtswQ7qZj0ZHdtbGuOno/LrgAAeSxtsJ6Vs4Gz5IkPH2bmqpjcLUFoqGhA/mueuIf63UXlA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@radix-ui/react-use-is-hydrated@0.1.3': resolution: {integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==} peerDependencies: @@ -1667,8 +1550,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-previous@1.1.4': - resolution: {integrity: sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==} + '@radix-ui/react-use-rect@1.1.4': + resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1676,8 +1559,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-rect@1.1.4': - resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==} + '@radix-ui/react-use-size@1.1.4': + resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1685,8 +1568,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-size@1.1.4': - resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==} + '@radix-ui/react-use-size@1.1.5': + resolution: {integrity: sha512-V1McDQkMGzgivwqdu7u96APs7oi0Jvj9LTzgq3WWrDZqxWdAqU9yZnZQqk5OizGFR8badUQwZsrlao7MOlWYOA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1710,19 +1593,6 @@ packages: '@radix-ui/rect@1.1.3': resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} - '@radix-ui/themes@3.3.0': - resolution: {integrity: sha512-I0/h2CRNTpYNB7Mi3xFIvSsQq5a108d7kK8dTO5zp5b9HR5QJXKag6B8tjpz2ITkVYkFdkGk45doNkSr7OxwNw==} - 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 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -2913,9 +2783,6 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - html-parse-stringify@4.0.1: - resolution: {integrity: sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==} - http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -2935,14 +2802,6 @@ packages: hyphen@1.14.1: resolution: {integrity: sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==} - i18next@26.4.2: - resolution: {integrity: sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==} - peerDependencies: - typescript: ^5 || ^6 || ^7 - peerDependenciesMeta: - typescript: - optional: true - iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -3611,19 +3470,6 @@ packages: resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} engines: {node: '>=0.6'} - radix-ui@1.6.7: - resolution: {integrity: sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA==} - 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 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -3643,22 +3489,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@17.0.15: - resolution: {integrity: sha512-7uYpx/oBkesLto7cOzOmfITlUBaXHCAcFahmgETMfWr8nbbWRRnVGvz8wsH/xpbNcyoH+NpCcnOQEqWPMO6/hw==} - peerDependencies: - i18next: '>= 26.2.0' - react: '>= 16.8.0' - react-dom: '*' - react-native: '*' - typescript: ^5 || ^6 || ^7 - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - typescript: - optional: true - react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -3762,9 +3592,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rtl-css-js@1.16.1: - resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} - run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -3932,9 +3759,6 @@ packages: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} - stylis@4.4.0: - resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} - supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -4133,11 +3957,6 @@ packages: '@types/react': optional: true - use-sync-external-store@1.7.0: - resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4562,8 +4381,6 @@ snapshots: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/runtime@7.29.7': {} - '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -4619,8 +4436,6 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} - '@emotion/hash@0.9.2': {} - '@esbuild/aix-ppc64@0.28.1': optional: true @@ -4752,12 +4567,6 @@ snapshots: '@floating-ui/utils@0.2.12': {} - '@fluentui/react-icons@2.0.343(react@19.2.8)': - dependencies: - '@griffel/react': 1.7.8(react@19.2.8) - react: 19.2.8 - tslib: 2.8.1 - '@formatjs/ecma402-abstract@2.3.6': dependencies: '@formatjs/fast-memoize': 2.2.7 @@ -4784,25 +4593,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@griffel/core@1.21.4': - dependencies: - '@emotion/hash': 0.9.2 - '@griffel/style-types': 1.4.3 - csstype: 3.2.3 - rtl-css-js: 1.16.1 - stylis: 4.4.0 - tslib: 2.8.1 - - '@griffel/react@1.7.8(react@19.2.8)': - dependencies: - '@griffel/core': 1.21.4 - react: 19.2.8 - tslib: 2.8.1 - - '@griffel/style-types@1.4.3': - dependencies: - csstype: 3.2.3 - '@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(react-hook-form@7.86.0(react@19.2.8))(zod@4.4.3)': dependencies: '@standard-schema/utils': 0.3.0 @@ -4990,44 +4780,25 @@ snapshots: - react-native-b4a - supports-color - '@radix-ui/colors@3.0.0': {} - '@radix-ui/number@1.1.3': {} '@radix-ui/primitive@1.1.7': {} - '@radix-ui/react-accessible-icon@1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - - '@radix-ui/react-accordion@1.2.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-alert-dialog@1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-alert-dialog@1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -5035,9 +4806,9 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-arrow@1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: @@ -5067,43 +4838,40 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-checkbox@1.3.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-collapsible@1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-collapsible@1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-collection@1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.4.0(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: @@ -5116,12 +4884,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - '@radix-ui/react-context-menu@2.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-context-menu@2.3.8(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-menu': 2.1.25(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -5164,6 +4932,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 + '@radix-ui/react-direction@1.1.5(@types/react@19.2.18)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 @@ -5177,6 +4951,19 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@radix-ui/react-dismissable-layer@1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@radix-ui/react-dropdown-menu@2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 @@ -5209,30 +4996,28 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-form@0.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-focus-scope@1.2.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-label': 2.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-hover-card@1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-hover-card@1.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-scope': 1.2.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popper': 1.3.8(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -5282,126 +5067,113 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-menubar@1.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-menu@2.1.25(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.2.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.8(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.4.0(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + aria-hidden: 1.2.6 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-navigation-menu@1.2.22(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + aria-hidden: 1.2.6 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-one-time-password-field@0.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/number': 1.1.3 - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/rect': 1.1.3 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-password-toggle-field@0.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-popper@1.3.8(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.7 + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-arrow': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/rect': 1.1.3 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - aria-hidden: 1.2.6 + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-portal@1.1.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/rect': 1.1.3 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -5409,7 +5181,7 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-presence@1.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 @@ -5427,27 +5199,9 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-progress@1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-primitive@2.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - - '@radix-ui/react-radio-group@1.4.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-slot': 1.4.0(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: @@ -5473,16 +5227,18 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-scroll-area@1.2.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-roving-focus@1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/number': 1.1.3 '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-direction': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -5490,32 +5246,19 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-select@2.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-scroll-area@1.2.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/number': 1.1.3 '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - aria-hidden: 1.2.6 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) @@ -5529,25 +5272,6 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-slider@1.4.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@radix-ui/number': 1.1.3 - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-slot@1.3.3(@types/react@19.2.18)(react@19.2.8)': dependencies: '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) @@ -5555,19 +5279,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - '@radix-ui/react-switch@1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-slot@1.4.0(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) '@radix-ui/react-tabs@1.1.21(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: @@ -5585,67 +5302,6 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-toast@1.2.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - - '@radix-ui/react-toggle-group@1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - - '@radix-ui/react-toggle@1.1.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - - '@radix-ui/react-toolbar@1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-separator': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-tooltip@1.2.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 @@ -5689,13 +5345,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - '@radix-ui/react-use-escape-keydown@1.1.5(@types/react@19.2.18)(react@19.2.8)': - dependencies: - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) - react: 19.2.8 - optionalDependencies: - '@types/react': 19.2.18 - '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.2.18)(react@19.2.8)': dependencies: react: 19.2.8 @@ -5708,20 +5357,21 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - '@radix-ui/react-use-previous@1.1.4(@types/react@19.2.18)(react@19.2.8)': + '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: + '@radix-ui/rect': 1.1.3 react: 19.2.8 optionalDependencies: '@types/react': 19.2.18 - '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.18)(react@19.2.8)': + '@radix-ui/react-use-size@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/rect': 1.1.3 + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 optionalDependencies: '@types/react': 19.2.18 - '@radix-ui/react-use-size@1.1.4(@types/react@19.2.18)(react@19.2.8)': + '@radix-ui/react-use-size@1.1.5(@types/react@19.2.18)(react@19.2.8)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 @@ -5739,18 +5389,6 @@ snapshots: '@radix-ui/rect@1.1.3': {} - '@radix-ui/themes@3.3.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@radix-ui/colors': 3.0.0 - classnames: 2.5.1 - radix-ui: 1.6.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - react-remove-scroll-bar: 2.3.8(@types/react@19.2.18)(react@19.2.8) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.62.4': @@ -6969,8 +6607,6 @@ snapshots: html-escaper@2.0.2: {} - html-parse-stringify@4.0.1: {} - http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -6997,10 +6633,6 @@ snapshots: hyphen@1.14.1: {} - i18next@26.4.2(typescript@5.9.3): - optionalDependencies: - typescript: 5.9.3 - iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -7641,69 +7273,6 @@ snapshots: es-define-property: 1.0.1 side-channel: 1.1.1 - radix-ui@1.6.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-accessible-icon': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-accordion': 1.2.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-alert-dialog': 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-aspect-ratio': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-avatar': 1.2.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-checkbox': 1.3.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-context-menu': 2.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-dropdown-menu': 2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-form': 0.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-hover-card': 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-label': 2.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-menubar': 1.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-navigation-menu': 1.2.22(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-one-time-password-field': 0.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-password-toggle-field': 0.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-popover': 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-progress': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-radio-group': 1.4.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-scroll-area': 1.2.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-select': 2.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-separator': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-slider': 1.4.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-switch': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-tabs': 1.1.21(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-toast': 1.2.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-toolbar': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-tooltip': 1.2.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-escape-keydown': 1.1.5(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) - '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - range-parser@1.2.1: {} raw-body@2.5.3: @@ -7722,17 +7291,6 @@ snapshots: dependencies: react: 19.2.8 - react-i18next@17.0.15(i18next@26.4.2(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3): - dependencies: - '@babel/runtime': 7.29.7 - html-parse-stringify: 4.0.1 - i18next: 26.4.2(typescript@5.9.3) - react: 19.2.8 - use-sync-external-store: 1.7.0(react@19.2.8) - optionalDependencies: - react-dom: 19.2.8(react@19.2.8) - typescript: 5.9.3 - react-refresh@0.17.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.18)(react@19.2.8): @@ -7851,10 +7409,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.4 fsevents: 2.3.3 - rtl-css-js@1.16.1: - dependencies: - '@babel/runtime': 7.29.7 - run-async@2.4.1: {} rxjs@6.6.7: @@ -8035,8 +7589,6 @@ snapshots: dependencies: '@tokenizer/token': 0.3.0 - stylis@4.4.0: {} - supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -8230,10 +7782,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - use-sync-external-store@1.7.0(react@19.2.8): - dependencies: - react: 19.2.8 - util-deprecate@1.0.2: {} utils-merge@1.0.1: {} diff --git a/frontend/src/atoms/theme.ts b/frontend/src/atoms/theme.ts index 0c7a0ed..e3746ef 100644 --- a/frontend/src/atoms/theme.ts +++ b/frontend/src/atoms/theme.ts @@ -16,18 +16,6 @@ function readStoredPreference(): ThemePreference { return v === 'light' || v === 'dark' || v === 'system' ? v : 'system'; } -/** 主题偏好,初值取 localStorage */ -export const themeAtom = atom(readStoredPreference()); - -/** 解析后的实际主题,供 UI 显示当前态 */ -export const resolvedThemeAtom = atom<'light' | 'dark'>( - readStoredPreference() === 'dark' || (readStoredPreference() === 'system' && systemPrefersDark()) - ? 'dark' - : 'light' -); - -const store = getDefaultStore(); - /** 把解析后的主题写到 */ function applyTheme(resolved: 'light' | 'dark') { document.documentElement.classList.toggle('dark', resolved === 'dark'); @@ -43,17 +31,42 @@ function withTransition(fn: () => void) { window.setTimeout(() => root.classList.remove('theme-transitioning'), 320); } +function resolve(preference: ThemePreference): 'light' | 'dark' { + return preference === 'dark' || (preference === 'system' && systemPrefersDark()) + ? 'dark' + : 'light'; +} + +const themePreferenceBaseAtom = atom(readStoredPreference()); + +/** 解析后的实际主题 */ +export const resolvedThemeAtom = atom<'light' | 'dark'>( + readStoredPreference() === 'dark' || (readStoredPreference() === 'system' && systemPrefersDark()) + ? 'dark' + : 'light' +); + +export const themeAtom = atom( + (get) => get(themePreferenceBaseAtom), + (_get, set, preference: ThemePreference) => { + localStorage.setItem(STORAGE_KEY, preference); + set(themePreferenceBaseAtom, preference); + const next = resolve(preference); + withTransition(() => { + applyTheme(next); + set(resolvedThemeAtom, next); + }); + } +); + +const store = getDefaultStore(); + // 模块加载即应用一次 applyTheme(store.get(resolvedThemeAtom)); /** 切换主题偏好 */ export function setTheme(preference: ThemePreference) { - localStorage.setItem(STORAGE_KEY, preference); - const resolved: 'light' | 'dark' = - preference === 'dark' || (preference === 'system' && systemPrefersDark()) ? 'dark' : 'light'; store.set(themeAtom, preference); - store.set(resolvedThemeAtom, resolved); - withTransition(() => applyTheme(resolved)); } /** 系统偏好变化时调用 */ diff --git a/frontend/src/components/UserMenu.tsx b/frontend/src/components/UserMenu.tsx index 1244aca..464e130 100644 --- a/frontend/src/components/UserMenu.tsx +++ b/frontend/src/components/UserMenu.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { Link } from 'react-router-dom'; -import { useAtomValue } from 'jotai'; +import { useAtom } from 'jotai'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { DropdownMenu, @@ -27,7 +27,7 @@ import { User, } from 'lucide-react'; import { useAuth } from '@/hooks/useAuth'; -import { themeAtom, setTheme, type ThemePreference } from '@/atoms/theme'; +import { themeAtom, type ThemePreference } from '@/atoms/theme'; import { preloadRoute } from '@/routes'; /** @@ -37,7 +37,7 @@ import { preloadRoute } from '@/routes'; */ export function UserMenu() { const { user, logout } = useAuth(); - const theme = useAtomValue(themeAtom); + const [theme, setThemePreference] = useAtom(themeAtom); const [open, setOpen] = useState(false); const closeTimer = useRef(0); @@ -117,7 +117,7 @@ export function UserMenu() { > setTheme(v as ThemePreference)} + onValueChange={(v) => setThemePreference(v as ThemePreference)} > diff --git a/frontend/src/components/review/AudioSourcePicker.tsx b/frontend/src/components/review/AudioSourcePicker.tsx new file mode 100644 index 0000000..9fca780 --- /dev/null +++ b/frontend/src/components/review/AudioSourcePicker.tsx @@ -0,0 +1,92 @@ +import { Music } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Radio, RadioGroup } from '@/components/ui/radio-group'; + +export interface AudioSourceOption { + key: string; + src: string; + label: string; +} + +interface AudioSourcePickerProps { + /** 可选音频源 */ + sources: AudioSourceOption[]; + /** 当前选中的索引 */ + value: number; + /** 用户确认选择后回调 */ + onConfirm: (index: number) => void; +} + +/** 音频源选择弹窗。*/ +export function AudioSourcePicker({ sources, value, onConfirm }: AudioSourcePickerProps) { + const [open, setOpen] = useState(false); + const [picked, setPicked] = useState(value); + // 只在本次会话首次出现多来源时自动弹出一次;之后由顶栏下拉切换,不再打扰。 + const autoPromptedRef = useRef(false); + + useEffect(() => { + if (autoPromptedRef.current) return; + if (sources.length > 1) { + autoPromptedRef.current = true; + setPicked(value); + setOpen(true); + } + }, [sources, value]); + + if (sources.length <= 1) return null; + + return ( + + + + 选择参考音频 + + {sources.length > 1 + ? `该投稿包含 ${sources.length} 个音频来源,请选择用于打轴与频谱的参考音频。` + : '请确认用于打轴与频谱的参考音频。'} + + + + + {sources.map((source, index) => ( + + ))} + + + + + + + + ); +} diff --git a/frontend/src/components/review/EditorTopMenu.tsx b/frontend/src/components/review/EditorTopMenu.tsx index 6496104..b511797 100644 --- a/frontend/src/components/review/EditorTopMenu.tsx +++ b/frontend/src/components/review/EditorTopMenu.tsx @@ -1,8 +1,16 @@ -import { DropdownMenu } from '@radix-ui/themes'; import { atom, useAtom, useAtomValue, useSetAtom, useStore } from 'jotai'; import { useSetImmerAtom } from 'jotai-immer'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; +import { type ComponentProps, useCallback } from 'react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { segmentLyricLines } from '$/modules/segmentation/utils/segmentation'; import { useSegmentationConfig } from '$/modules/segmentation/utils/useSegmentationConfig'; import { @@ -33,8 +41,23 @@ const canRedoAtom = atom((get) => get(undoableLyricLinesAtom).canRedo); const triggerClass = 'rounded px-2 py-1 text-xs font-medium text-ink-2 transition-colors hover:bg-surface-2 hover:text-ink'; +/** 菜单项:在右侧展示快捷键 */ +function MenuItem({ + shortcut, + children, + ...props +}: ComponentProps & { shortcut?: string }) { + return ( + + {children} + {shortcut ? ( + {shortcut} + ) : null} + + ); +} + function EditMenu() { - const { t } = useTranslation(); const store = useStore(); const setUndo = useSetAtom(undoLyricLinesAtom); const setRedo = useSetAtom(redoLyricLinesAtom); @@ -67,52 +90,51 @@ function EditMenu() { }, [store]); return ( - - + + - - - + + setUndo()} shortcut={formatKeyBindings(undoKey)} disabled={undoDisabled} > - {t('topBar.menu.undo', '撤销')} - - + setRedo()} shortcut={formatKeyBindings(redoKey)} disabled={redoDisabled} > - {t('topBar.menu.redo', '重做')} - - - - {t('topBar.menu.selectAllLines', '选中所有歌词行')} - - - {t('topBar.menu.unselectAllLines', '取消选中所有歌词行')} - - - {t('topBar.menu.invertSelectAllLines', '反选所有歌词行')} - - - + 重做 + + + + 选中所有歌词行 + + + 取消选中所有歌词行 + + + 反选所有歌词行 + + + ); } /* 工具 */ function ToolMenu() { - const { t } = useTranslation(); - const store = useStore(); const editLyricLines = useSetImmerAtom(lyricLinesAtom); const { config: segmentationConfig } = useSegmentationConfig(); const [, setConfirmDialog] = useAtom(confirmDialogAtom); const setTimeShiftDialog = useSetAtom(timeShiftDialogAtom); const setAdvancedSegmentation = useSetAtom(advancedSegmentationDialogAtom); + const setReduceStutter = useSetAtom(reduceStutterDialogAtom); const onAutoSegment = useCallback(() => { editLyricLines((draft) => { draft.lyricLines = segmentLyricLines(draft.lyricLines, segmentationConfig); @@ -151,58 +173,46 @@ function ToolMenu() { setConfirmDialog({ open: true, - title: t('confirmDialog.syncLineTimestamps.title', '确认同步行时间戳'), - description: t( - 'confirmDialog.syncLineTimestamps.description', - '此操作将根据每行单词的时间戳自动同步所有行的起始和结束时间为第一个和最后一个音节的开始和结束时间。确定要继续吗?' - ), + title: '确认同步行时间戳', + description: + '此操作将根据每行单词的时间戳自动同步所有行的起始和结束时间为第一个和最后一个音节的开始和结束时间。确定要继续吗?', onConfirm: action, }); - }, [editLyricLines, setConfirmDialog, t]); + }, [editLyricLines, setConfirmDialog]); const onReduceStutter = useCallback(() => { - store.set(reduceStutterDialogAtom, { open: true }); - }, [store]); + setReduceStutter({ open: true }); + }, [setReduceStutter]); return ( - - + + - - - - - {t('topBar.menu.segmentationTools', '分词')} - - - - {t('topBar.menu.autoSegment', '自动分词')} - - setAdvancedSegmentation(true)}> - {t('topBar.menu.advancedSegment', '高级分词...')} - - - - - - {t('topBar.menu.timestampTools.index', '时间戳')} - - - - {t('topBar.menu.timestampTools.syncLineTimestamps', '同步行时间戳')} - - - {t('topBar.menu.timestampTools.reduceStutter', '消减卡顿')} - - setTimeShiftDialog(true)}> - {t('topBar.menu.timeShift', '平移时间...')} - - - - - + + + + 分词 + + 自动分词 + setAdvancedSegmentation(true)}> + 高级分词... + + + + + 时间戳 + + 同步行时间戳 + 消减卡顿 + setTimeShiftDialog(true)}> + 平移时间... + + + + + ); } diff --git a/frontend/src/components/review/EditorWorkbench.tsx b/frontend/src/components/review/EditorWorkbench.tsx index d96f619..17d8892 100644 --- a/frontend/src/components/review/EditorWorkbench.tsx +++ b/frontend/src/components/review/EditorWorkbench.tsx @@ -1,4 +1,3 @@ -import { Theme } from '@radix-ui/themes'; import { AnimatePresence, motion } from 'framer-motion'; import { useAtomValue, useSetAtom } from 'jotai'; import { lazy, Suspense, useEffect, type ReactNode } from 'react'; @@ -12,11 +11,12 @@ import { AutosaveManager } from '@/editor/modules/project/autosave/AutosaveManag import { showTouchSyncPanelAtom } from '@/editor/modules/settings/states/sync.ts'; import { ToolMode, toolModeAtom } from '@/editor/states/main.ts'; import { MetadataEditor } from '@/editor/modules/project/modals/MetadataEditor.tsx'; +import { AdvancedSegmentationDialog } from '@/editor/modules/segmentation/components/AdvancedSegmentation.tsx'; +import { TimeShiftDialog } from '@/editor/modules/lyric-editor/tools/TimeShift.tsx'; +import { ConfirmDialog } from '@/editor/components/ConfirmDialog.tsx'; +import { ReduceStutterDialog } from '@/editor/components/ReduceStutterDialog.tsx'; import { useMediaSession } from '@/editor/modules/audio/hooks/useMediaSession.ts'; import { useAudioFeedback } from '@/editor/modules/audio/hooks/useAudioFeedback.ts'; -import { resolvedThemeAtom } from '@/atoms/theme'; -import '@/editor/i18n'; -import '@radix-ui/themes/styles.css'; import './editor-theme.css'; const LyricLinesView = lazy(() => import('@/editor/modules/lyric-editor/components')); @@ -52,10 +52,11 @@ export function EditorWorkbench() { > -
+
+ {/* 侧边栏紧贴 RibbonBar:顶部的 Tab 选中指示条需要顶到内容区最上沿 */} -
+
+ {/* 顶栏「工具」菜单几个菜单项只是置位 atom,缺了这些渲染者就会点了没反应 */} + + + + ); } -/** - * 编辑器的 Radix Theme 根。 - * - * 导出让 `LyricEditorPage` 能把它提到页面顶层,从而把顶栏也包进来 —— - * 否则顶栏右侧的 `EditorMenuBar`(Radix `DropdownMenu`)在 Theme 外, - * 样式会和工具栏里的其他 Radix 组件不一致。 - */ export function EditorTheme({ children }: { children: ReactNode }) { - const resolvedTheme = useAtomValue(resolvedThemeAtom); - const isDark = resolvedTheme === 'dark'; - - return ( - - {children} - - ); + return <>{children}; } diff --git a/frontend/src/components/review/LyricEditorHeader.tsx b/frontend/src/components/review/LyricEditorHeader.tsx index b3d3a9f..88f1ea3 100644 --- a/frontend/src/components/review/LyricEditorHeader.tsx +++ b/frontend/src/components/review/LyricEditorHeader.tsx @@ -1,9 +1,16 @@ import { useAtom } from 'jotai'; -import { ArrowLeft, Loader2, Save, Tags } from 'lucide-react'; +import { ArrowLeft, Check, ChevronDown, Loader2, Music, Save, Tags } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { ToolMode, toolModeAtom } from '@/editor/states/main.ts'; import { metadataEditorDialogAtom } from '@/editor/states/dialogs.ts'; import { EditorMenuBar } from '@/components/review/EditorTopMenu'; +import { UserMenu } from '@/components/UserMenu'; import { cn } from '@/lib/utils'; export interface EditorHeaderProps { @@ -17,6 +24,59 @@ export interface EditorHeaderProps { saving: boolean; } +/** 参考音频选择器 */ +function AudioSourceSelect({ + sources, + audioIndex, + onAudioIndexChange, +}: Pick) { + if (sources.length <= 1) return null; + + const current = sources[audioIndex] ?? sources[0]; + + return ( + + + + + + {sources.map((source, index) => ( + onAudioIndexChange(index)} + className="gap-2" + > + + ))} + + + ); +} + function ToolModeSwitch() { const [toolMode, setToolMode] = useAtom(toolModeAtom); @@ -83,6 +143,11 @@ export function LyricEditorHeader({
+ {/* 分隔:菜单类操作与右侧的「元数据 / 提交」动作按钮区分开 */} @@ -96,24 +161,11 @@ export function LyricEditorHeader({ 元数据 - {sources.length > 1 && ( - - )} +
); diff --git a/frontend/src/components/review/editor-theme.css b/frontend/src/components/review/editor-theme.css index 69257bc..e911654 100644 --- a/frontend/src/components/review/editor-theme.css +++ b/frontend/src/components/review/editor-theme.css @@ -1,57 +1,4 @@ -.radix-themes.editor-radix-theme { - height: 100%; - min-height: 0; -} - -body .rt-DialogOverlay { - position: fixed !important; - inset: 0 !important; - min-height: 0 !important; -} - -body .rt-DialogOverlay { - --color-overlay: rgb(0 0 0 / 0.45); -} - -body.editor-body:has(.dark), -body.editor-body.dark { - --color-overlay: rgb(0 0 0 / 0.6); -} - -body .rt-DialogOverlay[data-has-background='true'] { - background-color: transparent !important; -} - -/* 兜底:不给data-has-background 也保持透明 */ -body .rt-DialogOverlay { - background-color: transparent !important; -} - -body .rt-DialogOverlay::before, -body .rt-DialogOverlay:where([data-state='open'])::before { - background-color: rgb(0 0 0 / 0.45) !important; -} - -body.editor-body:has(.dark) .rt-DialogOverlay::before, -body.editor-body.dark .rt-DialogOverlay::before, -body.editor-body:has(.dark) .rt-DialogOverlay:where([data-state='open'])::before, -body.editor-body.dark .rt-DialogOverlay:where([data-state='open'])::before { - background-color: rgb(0 0 0 / 0.6) !important; -} - -body .rt-DialogOverlay, -body .rt-DialogScroll, -body .rt-DialogContent { - z-index: 400 !important; -} - -body .rt-DialogContent .rt-ScrollAreaRoot { - min-height: 0 !important; - flex: 1 1 auto !important; -} - -.radix-themes.editor-radix-theme, -body.editor-body [data-accent-color] { +body.editor-body { --accent-1: #fff5f6; --accent-2: #feeaec; --accent-3: #fbdde0; @@ -85,8 +32,8 @@ body.editor-body [data-accent-color] { --accent-track: #fbdde0; } -.radix-themes.editor-radix-theme:where(.dark, .dark-theme), -body.editor-body [data-accent-color]:where(.dark, .dark-theme) { +body.editor-body:has(.dark), +body.editor-body.dark { --accent-1: #2a1418; --accent-2: #3a1a20; --accent-3: #4a2127; diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx index 991fbf2..02c9d6b 100644 --- a/frontend/src/components/ui/checkbox.tsx +++ b/frontend/src/components/ui/checkbox.tsx @@ -1,4 +1,4 @@ -import type { ComponentProps } from 'react'; +import { type ComponentProps, useEffect, useRef } from 'react'; import { cn } from '@/lib/utils'; interface CheckboxProps extends Omit, 'type' | 'checked' | 'onChange'> { @@ -7,16 +7,27 @@ interface CheckboxProps extends Omit, 'type' | 'checked' } function Checkbox({ className, checked = false, onCheckedChange, ...props }: CheckboxProps) { - const isChecked = checked === true || checked === 'indeterminate'; + const ref = useRef(null); + const isIndeterminate = checked === 'indeterminate'; + const isChecked = checked === true || isIndeterminate; + + // 原生 input 的 indeterminate 只能通过 DOM 属性设置,不能由 HTML 属性表达 + useEffect(() => { + if (ref.current) ref.current.indeterminate = isIndeterminate; + }, [isIndeterminate]); + return ( onCheckedChange?.(e.target.checked)} className={cn( 'size-4 shrink-0 cursor-pointer appearance-none rounded border border-input bg-card transition-colors', 'checked:border-primary checked:bg-primary', + 'indeterminate:border-primary indeterminate:bg-primary', 'checked:bg-[url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2016%2016%27%20fill%3D%27white%27%3E%3Cpath%20d%3D%27M13.485%201.07L6.5%208.055%202.515%204.07%201%205.585l1.515%201.515L6.5%2011.385l5.985-5.985z%27%2F%3E%3C%2Fsvg%3E")] checked:bg-center checked:bg-no-repeat', + 'indeterminate:bg-[url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2016%2016%27%20fill%3D%27white%27%3E%3Crect%20x%3D%274%27%20y%3D%277%27%20width%3D%278%27%20height%3D%272%27%20rx%3D%271%27%2F%3E%3C%2Fsvg%3E")] indeterminate:bg-center indeterminate:bg-no-repeat', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40', 'disabled:cursor-not-allowed disabled:opacity-50', className diff --git a/frontend/src/components/ui/context-menu.tsx b/frontend/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..2c485d0 --- /dev/null +++ b/frontend/src/components/ui/context-menu.tsx @@ -0,0 +1,103 @@ +import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'; +import type { ComponentProps } from 'react'; +import { Check } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const ContextMenu = ContextMenuPrimitive.Root; +const ContextMenuTrigger = ContextMenuPrimitive.Trigger; +const ContextMenuGroup = ContextMenuPrimitive.Group; + +function ContextMenuContent({ + className, + ...props +}: ComponentProps) { + return ( + + + + ); +} + +function ContextMenuItem({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} + +function ContextMenuCheckboxItem({ + className, + children, + ...props +}: ComponentProps) { + return ( + + {children} + + + + + + + ); +} + +function ContextMenuLabel({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} + +function ContextMenuSeparator({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuLabel, + ContextMenuGroup, + ContextMenuSeparator, +}; diff --git a/frontend/src/components/ui/hover-card.tsx b/frontend/src/components/ui/hover-card.tsx new file mode 100644 index 0000000..4dea357 --- /dev/null +++ b/frontend/src/components/ui/hover-card.tsx @@ -0,0 +1,30 @@ +import * as HoverCardPrimitive from '@radix-ui/react-hover-card'; +import type { ComponentProps } from 'react'; +import { cn } from '@/lib/utils'; + +const HoverCard = HoverCardPrimitive.Root; +const HoverCardTrigger = HoverCardPrimitive.Trigger; + +function HoverCardContent({ + className, + align = 'center', + sideOffset = 6, + ...props +}: ComponentProps) { + return ( + + + + ); +} + +export { HoverCard, HoverCardTrigger, HoverCardContent }; diff --git a/frontend/src/components/ui/kbd.tsx b/frontend/src/components/ui/kbd.tsx new file mode 100644 index 0000000..fe93444 --- /dev/null +++ b/frontend/src/components/ui/kbd.tsx @@ -0,0 +1,18 @@ +import type { ComponentProps } from 'react'; +import { cn } from '@/lib/utils'; + +/** 键位徽标:用于展示快捷键(如打轴键位速查) */ +function Kbd({ className, ...props }: ComponentProps<'kbd'>) { + return ( + + ); +} + +export { Kbd }; diff --git a/frontend/src/components/ui/radio-group.tsx b/frontend/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..ec8f2e1 --- /dev/null +++ b/frontend/src/components/ui/radio-group.tsx @@ -0,0 +1,25 @@ +import type { ComponentProps } from 'react'; +import { cn } from '@/lib/utils'; + +function RadioGroup({ className, ...props }: ComponentProps<'div'>) { + return
; +} + +/** 单选框:基于原生 input[type=radio] */ +function Radio({ className, ...props }: Omit, 'type'>) { + return ( + + ); +} + +export { Radio, RadioGroup }; diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx new file mode 100644 index 0000000..d8bb5dc --- /dev/null +++ b/frontend/src/components/ui/select.tsx @@ -0,0 +1,19 @@ +import type { ComponentProps } from 'react'; +import { cn } from '@/lib/utils'; + +/** 下拉选择 */ +function Select({ className, ...props }: ComponentProps<'select'>) { + return ( + onValueChange?.(e.target.valueAsNumber)} + className={cn( + // input 铺满外层容器 + 'absolute inset-0 size-full bg-transparent', + isVertical && '[writing-mode:vertical-lr] [direction:rtl]', + 'cursor-pointer appearance-none accent-primary', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30', + 'disabled:cursor-not-allowed disabled:opacity-50', + // 滑块头 + '[&::-webkit-slider-thumb]:size-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary [&::-webkit-slider-thumb]:shadow', + '[&::-moz-range-thumb]:size-3 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-primary' + )} + {...props} + /> + + ); +} + +export { Slider }; diff --git a/frontend/src/editor/components/ConfirmDialog.tsx b/frontend/src/editor/components/ConfirmDialog.tsx new file mode 100644 index 0000000..0fea8ab --- /dev/null +++ b/frontend/src/editor/components/ConfirmDialog.tsx @@ -0,0 +1,64 @@ +import { useAtom } from 'jotai'; +import { useRef } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { confirmDialogAtom } from '$/states/dialogs.ts'; + +export function ConfirmDialog() { + const [state, setState] = useAtom(confirmDialogAtom); + /** 标记本次关闭是「确认」触发的,避免同时回调 `onCancel` */ + const confirmedRef = useRef(false); + + const close = () => setState((prev) => ({ ...prev, open: false })); + + const handleConfirm = () => { + const run = state.onConfirm; + confirmedRef.current = true; + // 先收起对话框再执行,与原生确认框一致,避免执行期间 UI 卡在弹窗上 + close(); + // 可能是异步任务(打开文件 / 恢复历史),错误由调用方自行处理 + void run?.(); + }; + + const handleCancel = () => { + state.onCancel?.(); + close(); + }; + + const handleOpenChange = (open: boolean) => { + if (open) return; + if (confirmedRef.current) { + confirmedRef.current = false; + return; + } + handleCancel(); + }; + + return ( + + + + {state.title} + {state.description ? ( + + {state.description} + + ) : null} + + + + + + + + ); +} diff --git a/frontend/src/editor/components/KeyBinding/index.tsx b/frontend/src/editor/components/KeyBinding/index.tsx index d199c69..3787ace 100644 --- a/frontend/src/editor/components/KeyBinding/index.tsx +++ b/frontend/src/editor/components/KeyBinding/index.tsx @@ -1,6 +1,6 @@ -import { Kbd } from '@radix-ui/themes'; import { useAtomValue } from 'jotai'; import type { FC } from 'react'; +import { Kbd } from '@/components/ui/kbd'; import { formatKeyBindings, type useKeyBindingAtom } from '$/utils/keybindings.ts'; /** diff --git a/frontend/src/editor/components/MasterDetail/index.tsx b/frontend/src/editor/components/MasterDetail/index.tsx index a5479a0..7f0945b 100644 --- a/frontend/src/editor/components/MasterDetail/index.tsx +++ b/frontend/src/editor/components/MasterDetail/index.tsx @@ -1,6 +1,6 @@ -import { DocumentRegular } from '@fluentui/react-icons'; -import { Box, Flex, Heading, ScrollArea, Text } from '@radix-ui/themes'; +import { FileText } from 'lucide-react'; import type { ReactNode } from 'react'; +import { ScrollArea } from '@/components/ui/scroll-area'; import styles from './index.module.css'; /** @@ -14,25 +14,23 @@ export const MasterDetailLayout = ({ master: ReactNode; detail: ReactNode; }) => ( - +
{master} - - {detail} - - +
{detail}
+
); export const MasterColumn = ({ title, children }: { title: ReactNode; children: ReactNode }) => ( - - - {title} - - - - {children} +
+
+
{title}
+
+
+ +
{children}
- - +
+
); export const MasterListItem = ({ @@ -44,7 +42,7 @@ export const MasterListItem = ({ onSelect: () => void; children: ReactNode; }) => ( - {children} - +
); export const MasterListEmpty = ({ children }: { children: ReactNode }) => ( - - - {children} - - +
+

{children}

+
); export const DetailPlaceholder = ({ children }: { children: ReactNode }) => ( - - - {children} - +
+ +

{children}

+
); diff --git a/frontend/src/editor/components/Menus/lyric-line-menu.tsx b/frontend/src/editor/components/Menus/lyric-line-menu.tsx index 08398b4..fad547e 100644 --- a/frontend/src/editor/components/Menus/lyric-line-menu.tsx +++ b/frontend/src/editor/components/Menus/lyric-line-menu.tsx @@ -1,16 +1,17 @@ -import { ContextMenu } from '@radix-ui/themes'; import { atom, useAtomValue } from 'jotai'; import { useSetImmerAtom } from 'jotai-immer'; import * as React from 'react'; -import { useTranslation } from 'react-i18next'; +import { + ContextMenuCheckboxItem, + ContextMenuItem, + ContextMenuSeparator, +} from '@/components/ui/context-menu'; import { lyricLinesAtom, selectedLinesAtom } from '$/states/main'; import { type LyricLine, newLyricLine, newLyricWord } from '$/types/ttml'; const selectedLinesSizeAtom = atom((get) => get(selectedLinesAtom).size); export const LyricLineMenu = ({ lineIndex }: { lineIndex: number }) => { - const { t } = useTranslation(); - const selectedLinesSize = useAtomValue(selectedLinesSizeAtom); const selectedLines = useAtomValue(selectedLinesAtom); const editLyricLines = useSetImmerAtom(lyricLinesAtom); @@ -56,41 +57,38 @@ export const LyricLineMenu = ({ lineIndex }: { lineIndex: number }) => { return ( <> - - {t('contextMenu.bgLyric', '背景歌词')} - - - {t('contextMenu.duetLyric', '对唱歌词')} - - - + 背景歌词 + + + 对唱歌词 + + + { editLyricLines((state) => { state.lyricLines.splice(lineIndex, 0, newLyricLine()); }); }} > - {t('contextMenu.insertLineBefore', '在前插入空行')} - - + { editLyricLines((state) => { state.lyricLines.splice(lineIndex + 1, 0, newLyricLine()); }); }} > - {t('contextMenu.insertLineAfter', '在后插入空行')} - - - {t('contextMenu.copyLine', { - count: selectedLinesSize, - defaultValue: '复制行', - })} - - - {t('contextMenu.combineLine', '合并行')} - - + + 复制行 + + + 合并行 + + { editLyricLines((state) => { if (selectedLinesSize === 0) { @@ -101,11 +99,8 @@ export const LyricLineMenu = ({ lineIndex }: { lineIndex: number }) => { }); }} > - {t('contextMenu.deleteLine', { - count: selectedLinesSize, - defaultValue: '删除行', - })} - + 删除行 + ); diff --git a/frontend/src/editor/components/ReduceStutterDialog.tsx b/frontend/src/editor/components/ReduceStutterDialog.tsx new file mode 100644 index 0000000..616746a --- /dev/null +++ b/frontend/src/editor/components/ReduceStutterDialog.tsx @@ -0,0 +1,181 @@ +import { useAtom, useAtomValue } from 'jotai'; +import { useSetImmerAtom } from 'jotai-immer'; +import { Play } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { msToTimestamp } from '@/lib/format'; +import { audioEngine } from '$/modules/audio/audio-engine'; +import { reduceStutterDialogAtom } from '$/states/dialogs.ts'; +import { lyricLinesAtom } from '$/states/main.ts'; +import type { LyricWord } from '$/types/ttml'; + +/** 音节间隔小于该值(ms)视为卡顿 */ +const STUTTER_GAP_THRESHOLD = 100; + +interface StutterPair { + lineIndex: number; + wordIndex: number; + nextWordIndex: number; + prevWord: LyricWord; + nextWord: LyricWord; + gap: number; +} + +export function ReduceStutterDialog() { + const [open, setOpen] = useAtom(reduceStutterDialogAtom); + const lyricLines = useAtomValue(lyricLinesAtom); + const setLyricLines = useSetImmerAtom(lyricLinesAtom); + const [selectedPairs, setSelectedPairs] = useState>(new Set()); + + const stutterPairs = useMemo(() => { + const pairs: StutterPair[] = []; + lyricLines.lyricLines.forEach((line, lineIndex) => { + // 过滤掉纯空白音节,只比较有实际内容的音节 + const nonSpaceWords = line.words.filter((w) => w.word.trim().length > 0); + for (let i = 0; i < nonSpaceWords.length - 1; i++) { + const currentWord = nonSpaceWords[i]; + const nextWord = nonSpaceWords[i + 1]; + if (!currentWord || !nextWord) continue; + + const gap = nextWord.startTime - currentWord.endTime; + if (gap > 0 && gap < STUTTER_GAP_THRESHOLD) { + const wordIndex = line.words.findIndex((w) => w.id === currentWord.id); + const nextWordIndex = line.words.findIndex((w) => w.id === nextWord.id); + pairs.push({ + lineIndex, + wordIndex, + nextWordIndex, + prevWord: currentWord, + nextWord, + gap, + }); + } + } + }); + return pairs; + }, [lyricLines.lyricLines]); + + // 打开时默认全选所有卡顿项 + useEffect(() => { + if (open.open) { + setSelectedPairs(new Set(stutterPairs.map((_, index) => index.toString()))); + } + }, [open.open, stutterPairs]); + + const togglePair = (index: string) => { + setSelectedPairs((prev) => { + const next = new Set(prev); + if (next.has(index)) { + next.delete(index); + } else { + next.add(index); + } + return next; + }); + }; + + const handleConfirm = () => { + const selectedIndices = Array.from(selectedPairs).map((id) => parseInt(id, 10)); + + setLyricLines((draft) => { + selectedIndices.forEach((pairIndex) => { + const pair = stutterPairs[pairIndex]; + if (!pair) return; + + const line = draft.lyricLines[pair.lineIndex]; + if (!line) return; + + const prevWord = line.words[pair.wordIndex]; + const nextWord = line.words[pair.nextWordIndex]; + if (!prevWord || !nextWord) return; + + // 把两个音节的边界对齐到中间时间点 + const middleTime = Math.round((prevWord.endTime + nextWord.startTime) / 2); + prevWord.endTime = middleTime; + nextWord.startTime = middleTime; + }); + }); + + setOpen({ open: false }); + }; + + const handleClose = () => setOpen({ open: false }); + + const handlePlay = (pair: StutterPair) => { + // 播放前一个音节开始前 100ms 到后一个音节结束后 100ms + const startTime = Math.max(0, pair.prevWord.startTime - 100) / 1000; + const endTime = (pair.nextWord.endTime + 100) / 1000; + audioEngine.auditionRange(startTime, endTime); + }; + + return ( + setOpen({ open: v })}> + + + 消减卡顿 + + 检测到以下音节间隔小于 {STUTTER_GAP_THRESHOLD}ms,选中项将被调整为中间时间点 + + + + {stutterPairs.length === 0 ? ( +
未检测到卡顿
+ ) : ( + +
+ {stutterPairs.map((pair, index) => ( +
+ togglePair(index.toString())} + /> + + + {pair.prevWord.word} [{msToTimestamp(pair.prevWord.startTime)}~ + {msToTimestamp(pair.prevWord.endTime)}] - {pair.nextWord.word} [ + {msToTimestamp(pair.nextWord.startTime)}~{msToTimestamp(pair.nextWord.endTime)}] + : {pair.gap} ms + +
+ ))} +
+
+ )} + + + + + +
+
+ ); +} diff --git a/frontend/src/editor/components/RibbonBar/common.tsx b/frontend/src/editor/components/RibbonBar/common.tsx index d20b911..6c596cd 100644 --- a/frontend/src/editor/components/RibbonBar/common.tsx +++ b/frontend/src/editor/components/RibbonBar/common.tsx @@ -9,7 +9,6 @@ * https://github.com/amll-dev/amll-ttml-tool/blob/main/LICENSE */ -import { Flex, Separator, Text } from '@radix-ui/themes'; import { motion } from 'framer-motion'; import { type FC, @@ -22,26 +21,11 @@ import { export const RibbonSection: FC> = ({ children, label }) => ( <> - - - {children} - - - {label} - - - +
+
{children}
+ {label} +
+
); @@ -59,28 +43,16 @@ export const RibbonFrame = forwardRef(({ chil }, []); return ( - - - {children} - - + {children} + ); }); diff --git a/frontend/src/editor/components/RibbonBar/edit-mode.tsx b/frontend/src/editor/components/RibbonBar/edit-mode.tsx index 8b76830..84b4ab1 100644 --- a/frontend/src/editor/components/RibbonBar/edit-mode.tsx +++ b/frontend/src/editor/components/RibbonBar/edit-mode.tsx @@ -9,22 +9,16 @@ * https://github.com/amll-dev/amll-ttml-tool/blob/main/LICENSE */ -import { Add16Regular } from '@fluentui/react-icons'; -import { - Button, - Checkbox, - Flex, - Grid, - IconButton, - RadioGroup, - Select, - Text, - TextField, - Tooltip, -} from '@radix-ui/themes'; import type { Draft } from 'immer'; import { atom, useAtom, useAtomValue, useSetAtom, useStore } from 'jotai'; import { useSetImmerAtom } from 'jotai-immer'; +import { Plus } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Input } from '@/components/ui/input'; +import { Radio, RadioGroup } from '@/components/ui/radio-group'; +import { Select } from '@/components/ui/select'; +import { cn } from '@/lib/utils'; import { type FC, forwardRef, @@ -36,7 +30,6 @@ import { useRef, useState, } from 'react'; -import { useTranslation } from 'react-i18next'; import { I18nEditor } from '$/modules/lyric-editor/tools/i18nEditor.tsx'; import { LayoutMode, @@ -89,14 +82,14 @@ function EditField< const [fieldInput, setFieldInput] = useState(undefined); const [fieldPlaceholder, setFieldPlaceholder] = useState(''); const [durationInputInvalid, setDurationInputInvalid] = useState(false); - const [showDurationInput, setShowDurationInput] = useAtom(showEndTimeAsDurationAtom); + /** 时间字段固定按「结束时间」展示,不再提供「持续时间」切换 */ + const showDurationInput = useAtomValue(showEndTimeAsDurationAtom); const itemAtom = useMemo( () => (isWordField ? selectedWordsAtom : selectedLinesAtom), [isWordField] ); const editLyricLines = useSetImmerAtom(lyricLinesAtom); - const { t } = useTranslation(); const setEditingTimeField = useSetAtom(editingTimeFieldAtom); const [requestFocus, setRequestFocus] = useAtom(requestFocusAtom); @@ -388,7 +381,7 @@ function EditField< if (fieldName === 'endTime' && showDurationInput) { if (durationValue === MULTIPLE_VALUES) { setFieldInput(''); - setFieldPlaceholder(t('ribbonBar.editMode.multipleValues', '多个值...')); + setFieldPlaceholder('多个值...'); return; } if (typeof durationValue === 'number') { @@ -402,34 +395,22 @@ function EditField< } if (currentValue === MULTIPLE_VALUES) { setFieldInput(''); - setFieldPlaceholder(t('ribbonBar.editMode.multipleValues', '多个值...')); + setFieldPlaceholder('多个值...'); return; } setFieldInput(currentValue); setFieldPlaceholder(''); - }, [currentValue, durationValue, fieldName, showDurationInput, t]); + }, [currentValue, durationValue, fieldName, showDurationInput]); + + const invalid = durationInputInvalid || hasError; return ( <> - {fieldName === 'endTime' ? ( - - ) : ( - - {label} - - )} - {label} + atom((get) => get(itemAtom).size === 0), [itemAtom]); const isDisabledBase = useAtomValue(isDisabledAtom); - // 对于 isDuet 字段,检查选中的行是否设置了 agent - const hasAgentAtom = useMemo( - () => - atom((get) => { - if (fieldName !== 'isDuet' || isWordField) return false; - const selectedItems = get(itemAtom); - const lyricLines = get(lyricLinesAtom); - if (selectedItems.size === 0) return false; - const selectedLines = selectedItems as Set; - for (const line of lyricLines.lyricLines) { - if (selectedLines.has(line.id)) { - // 如果任何一个选中的行设置了 agent,则禁用 checkbox - if (line.agent) { - return true; - } - } - } - return false; - }), - [fieldName, isWordField, itemAtom] - ); - const hasAgent = useAtomValue(hasAgentAtom); - - const isDisabled = isDisabledBase || hasAgent; + const isDisabled = isDisabledBase; const checkboxId = useId(); return ( <> - - - + { - if (value === 'indeterminate') return; editLyricLines((state: Draft) => { const selectedItems = store.get(itemAtom); for (const line of state.lyricLines) { @@ -587,21 +544,28 @@ function CheckboxField< function EditModeField({ simpleModeLabel = '简单模式', advanceModeLabel = '高级模式' }) { const [layoutMode, setLayoutMode] = useAtom(layoutModeAtom); + const name = useId(); return ( - setLayoutMode(v as LayoutMode)} - size="1" - > - - - {simpleModeLabel} - - - {advanceModeLabel} - - - + + + + ); } @@ -623,7 +587,6 @@ const SONG_PART_OPTIONS = [ const NONE_VALUE = '__none__'; const SongPartField: FC = () => { - const { t } = useTranslation(); const selectedLines = useAtomValue(selectedLinesAtom); const lyricLines = useAtomValue(lyricLinesAtom); const editLyricLines = useSetImmerAtom(lyricLinesAtom); @@ -673,70 +636,68 @@ const SongPartField: FC = () => { return ( <> - - {t('ribbonBar.editMode.songPart', 'Song Part')} - - - + Song Part + +
+ + {isAddingCustom && ( + <> + setCustomPart(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleAddCustomPart(); + } }} + /> + + + )} +
); }; const AgentField: FC = () => { - const { t } = useTranslation(); const selectedLines = useAtomValue(selectedLinesAtom); const lyricLines = useAtomValue(lyricLinesAtom); const editLyricLines = useSetImmerAtom(lyricLinesAtom); @@ -876,44 +837,41 @@ const AgentField: FC = () => { return ( <> - - {t('ribbonBar.editMode.agent', 'Agent')} - - + Agent + + ); }; @@ -924,45 +882,38 @@ const AuxiliaryDisplayField: FC = () => { const [showWordRomanizationInput, setShowWordRomanizationInput] = useAtom( showWordRomanizationInputAtom ); - const { t } = useTranslation(); const idTranslation = useId(); const idRomanization = useId(); const idPerWord = useId(); return ( - - - - +
+ setShowTranslation(Boolean(c))} /> - - - + setShowRomanization(Boolean(c))} /> - - - + setShowWordRomanizationInput(Boolean(c))} /> - +
); }; @@ -975,81 +926,80 @@ export const EditModeRibbonBar: FC = forwardRef((_props, ref) => const idBpm = useId(); const editLyricLines = useSetImmerAtom(lyricLinesAtom); - const { t } = useTranslation(); return ( - - + +
- +
- - + +
- +
- - + +
- +
- - + +
{ @@ -1058,80 +1008,65 @@ export const EditModeRibbonBar: FC = forwardRef((_props, ref) => }} formatter={String} /> - +
- - + +
v} formatter={(v) => v} /> v} formatter={(v) => v || ''} /> - - + +
- - + +
v} formatter={(v) => v} textFieldStyle={{ width: '20em' }} /> v} formatter={(v) => v} textFieldStyle={{ width: '20em' }} /> - +
- + - - + + - + - - + +
- +
- - - - - + +
+ ((_props, ref) => toggleTab({ tabId: 'outline', open: Boolean(checked) }); }} /> - - - + ((_props, ref) => toggleTab({ tabId: 'bpm', open: Boolean(checked) }); }} /> - +
); diff --git a/frontend/src/editor/components/RibbonBar/index.tsx b/frontend/src/editor/components/RibbonBar/index.tsx index d31110f..3a1fec4 100644 --- a/frontend/src/editor/components/RibbonBar/index.tsx +++ b/frontend/src/editor/components/RibbonBar/index.tsx @@ -9,7 +9,6 @@ * https://github.com/amll-dev/amll-ttml-tool/blob/main/LICENSE */ -import { Card, Inset } from '@radix-ui/themes'; import { AnimatePresence } from 'framer-motion'; import { useAtomValue } from 'jotai'; import { forwardRef, lazy, memo } from 'react'; @@ -25,42 +24,30 @@ export const RibbonBar = memo( const toolMode = useAtomValue(toolModeAtom); return ( - - -
- - {toolMode === ToolMode.Edit && ( - - - - )} - {toolMode === ToolMode.Sync && ( - - - - )} - {toolMode === ToolMode.Preview && ( - - - - )} - -
-
-
+
+ + {toolMode === ToolMode.Edit && ( + + + + )} + {toolMode === ToolMode.Sync && ( + + + + )} + {toolMode === ToolMode.Preview && ( + + + + )} + +
+
); }) ); diff --git a/frontend/src/editor/components/RibbonBar/preview-mode.tsx b/frontend/src/editor/components/RibbonBar/preview-mode.tsx index a415a19..806a944 100644 --- a/frontend/src/editor/components/RibbonBar/preview-mode.tsx +++ b/frontend/src/editor/components/RibbonBar/preview-mode.tsx @@ -9,10 +9,10 @@ * https://github.com/amll-dev/amll-ttml-tool/blob/main/LICENSE */ -import { Checkbox, Grid, Text, TextField } from '@radix-ui/themes'; import { useAtom } from 'jotai'; import { forwardRef } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Input } from '@/components/ui/input'; import { hideObsceneWordsAtom, lyricWordFadeWidthAtom, @@ -26,41 +26,27 @@ export const PreviewModeRibbonBar = forwardRef((_props, ref) => const [showRomanLine, setShowRomanLine] = useAtom(showRomanLinesAtom); const [hideObsceneWords, setHideObsceneWords] = useAtom(hideObsceneWordsAtom); const [lyricWordFadeWidth, setLyricWordFadeWidth] = useAtom(lyricWordFadeWidthAtom); - const { t } = useTranslation(); return ( - - - - {t('ribbonBar.previewMode.showTranslation', '显示翻译')} - + +
+ 显示翻译 setShowTranslationLine(!!v)} /> - - {t('ribbonBar.previewMode.showRoman', '显示音译')} - + 显示音译 setShowRomanLine(!!v)} /> - - {t('ribbonBar.previewMode.maskObsceneWords', '屏蔽不雅用语')} - + 屏蔽不雅用语 setHideObsceneWords(!!v)} /> - +
- - - - {t('ribbonBar.previewMode.fadeWidth', '过渡宽度')} - - +
+ 过渡宽度 + { const value = Number.parseFloat(e.target.value); @@ -69,7 +55,7 @@ export const PreviewModeRibbonBar = forwardRef((_props, ref) => } }} /> - +
); diff --git a/frontend/src/editor/components/RibbonBar/sync-mode.tsx b/frontend/src/editor/components/RibbonBar/sync-mode.tsx index d11442d..5bbe5df 100644 --- a/frontend/src/editor/components/RibbonBar/sync-mode.tsx +++ b/frontend/src/editor/components/RibbonBar/sync-mode.tsx @@ -31,37 +31,36 @@ import { keySyncStartAtom, } from '$/states/keybindings.ts'; import { bgLyricIgnoreSyncAtom, lyricLinesAtom } from '$/states/main.ts'; -import { Checkbox, Flex, Grid, Kbd, Slider, Text, TextField } from '@radix-ui/themes'; import { useAtom, useAtomValue } from 'jotai'; import { useSetImmerAtom } from 'jotai-immer'; import { type FC, forwardRef } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Input } from '@/components/ui/input'; +import { Kbd } from '@/components/ui/kbd'; +import { Slider } from '@/components/ui/slider'; +import { formatKeyBindings } from '$/utils/keybindings.ts'; import { KeyBinding } from '../KeyBinding/index.tsx'; import { RibbonFrame, RibbonSection } from './common'; -import { formatKeyBindings } from '$/utils/keybindings.ts'; const EmptyBeatField = () => { const [currentEmptyBeat, setCurrentEmptyBeat] = useAtom(currentEmptyBeatAtom); const currentWordEmptyBeat = useCurrentLocation()?.word.emptyBeat || 0; - const { t } = useTranslation(); return ( <> - - {t('ribbonBar.syncMode.currentEmptyBeat', '当前空拍')} - + 当前空拍 setCurrentEmptyBeat(v[0] ?? 0)} + value={currentEmptyBeat} + onValueChange={setCurrentEmptyBeat} min={0} max={currentWordEmptyBeat} step={1} disabled={currentWordEmptyBeat === 0} />
- + {currentEmptyBeat} / {currentWordEmptyBeat} - + ); }; @@ -83,50 +82,38 @@ export const SyncModeRibbonBar: FC = forwardRef((_props, ref) => const [syncTimeOffset, setSyncTimeOffset] = useAtom(syncTimeOffsetAtom); const syncNextDualMode = useAtomValue(syncNextDualModeAtom); const syncNextAltKeys = useAtomValue(keySyncNextAltAtom); - const { t } = useTranslation(); return ( - - - - {t('ribbonBar.syncMode.timeOffset', '时间戳位移')} - - setSyncTimeOffset(e.target.valueAsNumber)} - > - - ms - + +
+ 时间戳位移 +
+ setSyncTimeOffset(e.target.valueAsNumber)} + /> + ms +
- +
- - - - {t('ribbonBar.syncMode.showTimestampUpdate', '呈现时间戳更新')} - + +
+ 呈现时间戳更新 setVisualizeTimestampUpdate(!!v)} /> - - {t('ribbonBar.syncMode.touchSyncPanel', '触控打轴辅助面板')} - + 触控打轴辅助面板 setShowTouchSyncPanel(!!v)} /> - - {t('ribbonBar.syncMode.bgLyricIgnoreSync', '背景歌词忽略打轴')} - + 背景歌词忽略打轴 { @@ -142,58 +129,44 @@ export const SyncModeRibbonBar: FC = forwardRef((_props, ref) => }); }} /> - +
- - - - {t('ribbonBar.syncMode.showTimestamps', '显示时间戳')} - + +
+ 显示时间戳 setShowTimestamps(!!v)} /> - - {t('ribbonBar.syncMode.highlightActiveWord', '高亮当前音节')} - + 高亮当前音节 setHighlightActiveWord(!!v)} /> - - {t('ribbonBar.syncMode.highlightErrors', '高亮错误')} - + 高亮错误 setHighlightErrors(!!v)} /> {showWordRomanizationInput && ( <> - - {t('ribbonBar.syncMode.showPerWordRomanization', '显示逐字音译')} - + 显示逐字音译 setdisplayRomanizationInSync(!!v)} /> )} - +
- - - - - {t('ribbonBar.syncMode.startSync', '起始轴')} - + +
+
+ 起始轴 - - {t('ribbonBar.syncMode.continuousSync', '连续轴')} - - + 连续轴 +
{syncNextDualMode ? {formatKeyBindings(syncNextAltKeys)} : null} - - - {t('ribbonBar.syncMode.endSync', '结束轴')} - +
+ 结束轴 - -
+
+
); diff --git a/frontend/src/editor/components/Sidebar/BpmPanel.tsx b/frontend/src/editor/components/Sidebar/BpmPanel.tsx index 3ff76e1..a0d85d1 100644 --- a/frontend/src/editor/components/Sidebar/BpmPanel.tsx +++ b/frontend/src/editor/components/Sidebar/BpmPanel.tsx @@ -1,25 +1,10 @@ -import { - ArrowLeft16Regular, - ArrowResetRegular, - Dismiss16Regular, - ErrorCircle16Regular, - Info16Regular, - Timer16Regular, -} from '@fluentui/react-icons'; -import { - Box, - Button, - Callout, - Card, - Checkbox, - Flex, - IconButton, - Text, - Tooltip, -} from '@radix-ui/themes'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { AlertCircle, ArrowLeft, Info, RotateCcw, Timer, X } from 'lucide-react'; import { type FC, useCallback, useId, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Alert, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { SimpleTooltip } from '@/components/ui/tooltip'; import { KeyBinding } from '$/components/KeyBinding'; import { useBpmControl, useBpmTapEngine } from '$/modules/audio/hooks'; import { @@ -39,7 +24,6 @@ function formatCalculationTime(timeMs: number): string { } export const BpmPanel: FC = () => { - const { t } = useTranslation(); const followRateCheckboxId = useId(); const showBeatLinesCheckboxId = useId(); const { @@ -104,7 +88,7 @@ export const BpmPanel: FC = () => { bpmValueText = `${currentBpm ?? Math.round(bpmState.result.bpm)}`; durationText = formatCalculationTime(bpmState.calculationTime); } else if (bpmState.status === 'analyzing') { - bpmValueText = t('sidebar.bpm.analyzing', '正在分析'); + bpmValueText = '正在分析'; } const isAnalyzing = bpmState.status === 'analyzing'; @@ -113,314 +97,214 @@ export const BpmPanel: FC = () => { isAdjusted || tapTimes.length > 0 || (isCompleted && bpmState.calculationTime === 0); return ( - - - - - BPM - +
+
+
+ BPM - - +
+ - + - {bpmValueText} - + - + - - + +
{isCompleted && isModified && ( - - + + )} -
+
- - - - {durationText} - - +
+ + {durationText} +
{bpmState.status === 'error' && ( - - - - - {bpmState.error} - + + + {bpmState.error} + )} - +
setFollowPlaybackRate(Boolean(checked))} /> - - - - + +
- +
setShowBeatLines(Boolean(checked))} /> - - - - + +
{!hasSeenTapWindowTip && (isKeyTapMode || isSpectrogramTapMode) && ( - - - - - - - {t('sidebar.bpm.slidingWindowTip', '只会使用最近 10 次校准数据计算 BPM')} - - - + +
只会使用最近 10 次校准数据计算 BPM
+ + )} {!isKeyTapMode && !isSpectrogramTapMode && ( - +
- +
)} {isKeyTapMode && ( - - - - +
+
+ - - + triggerTap()} - style={{ - textAlign: 'center', - color: isHighlighted ? 'var(--accent-11)' : undefined, - cursor: 'pointer', - userSelect: 'none', - }} > - {t('sidebar.bpm.tapInstructionPrefix', '根据节拍按下 ')} - - {t('sidebar.bpm.tapInstructionSuffix', ' 或点击此处')} - - - + 根据节拍按下 或点击此处 + +
+
- +
{Array.from({ length: 10 }).map((_, index) => ( - ))} - - - +
+
+
)} {isSpectrogramTapMode && ( - - - - +
+
+ - - + - {t('sidebar.bpm.clickOnSpectrogramToCalibrate', '在频谱图上点击以校准')} - - - + 在频谱图上点击以校准 + +
+
- +
{Array.from({ length: 10 }).map((_, index) => ( - ))} - - - +
+
+
)} - - +
+ ); }; diff --git a/frontend/src/editor/components/Sidebar/OutlinePanel.tsx b/frontend/src/editor/components/Sidebar/OutlinePanel.tsx index 4e543bb..0c7e247 100644 --- a/frontend/src/editor/components/Sidebar/OutlinePanel.tsx +++ b/frontend/src/editor/components/Sidebar/OutlinePanel.tsx @@ -1,7 +1,6 @@ -import { ContextMenu } from '@radix-ui/themes'; import { useAtomValue, useSetAtom, useStore } from 'jotai'; import { memo, useCallback, useMemo, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; +import { ContextMenu, ContextMenuContent, ContextMenuTrigger } from '@/components/ui/context-menu'; import { LyricLineMenu } from '$/components/Menus/lyric-line-menu'; import { useLyricListDrag } from '$/modules/lyric-drag/useLyricListDrag'; import { @@ -16,7 +15,6 @@ import { outlineJumpActionAtom } from '$/states/sidebar.ts'; import styles from './OutlinePanel.module.css'; export const OutlinePanel = memo(() => { - const { t } = useTranslation(); const store = useStore(); const { lyricLines } = useAtomValue(lyricLinesAtom); @@ -66,6 +64,9 @@ export const OutlinePanel = memo(() => { [store, isDragging] ); + // 仅在编辑模式下挂右键菜单 + const menuEnabled = toolMode === ToolMode.Edit; + return (
{ const textContent = line.words.map((w) => w.word).join(''); const hasText = textContent.trim().length > 0; + const item = ( +
{ + onPointerDown(e, line.id, index); + }} + onDoubleClick={() => handleItemDoubleClick(line.id)} + > +
{displayNumbers[index]}
+ +
+
+ {hasText ? textContent : (空行)} +
+
+
+ ); + return (
{ data-line-id={line.id} data-absolute-index={index} > - handleContextMenuOpen(opened, line.id)}> - -
{ - onPointerDown(e, line.id, index); - }} - onDoubleClick={() => handleItemDoubleClick(line.id)} - > -
{displayNumbers[index]}
- -
-
- {hasText ? ( - textContent - ) : ( - - {t('sidebar.outline.emptyLine', '(空行)')} - - )} -
-
-
-
+ {menuEnabled ? ( + handleContextMenuOpen(opened, line.id)}> + {item} - - - -
+ + + + + ) : ( + item + )}
); })} diff --git a/frontend/src/editor/components/Sidebar/ResizableSidebar.module.css b/frontend/src/editor/components/Sidebar/ResizableSidebar.module.css index 2649d50..aa41799 100644 --- a/frontend/src/editor/components/Sidebar/ResizableSidebar.module.css +++ b/frontend/src/editor/components/Sidebar/ResizableSidebar.module.css @@ -47,6 +47,8 @@ .title { flex: 1 1 auto; min-width: 0; + align-self: stretch; + height: 100%; overflow: hidden; } diff --git a/frontend/src/editor/components/Sidebar/ResizableSidebar.tsx b/frontend/src/editor/components/Sidebar/ResizableSidebar.tsx index bbe8832..9a5b4a8 100644 --- a/frontend/src/editor/components/Sidebar/ResizableSidebar.tsx +++ b/frontend/src/editor/components/Sidebar/ResizableSidebar.tsx @@ -1,9 +1,8 @@ -import { Dismiss16Regular } from '@fluentui/react-icons'; -import { Box, IconButton } from '@radix-ui/themes'; import { motion } from 'framer-motion'; import { useAtom, type WritableAtom } from 'jotai'; +import { X } from 'lucide-react'; import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; import styles from './ResizableSidebar.module.css'; const SNAP_CLOSE_THRESHOLD = 20; @@ -43,7 +42,6 @@ export const ResizableSidebar = ({ headerSingle, children, }: ResizableSidebarProps) => { - const { t } = useTranslation(); const [activePanel, setActivePanel] = useAtom(panelAtom); const [savedWidth, setSavedWidth] = useAtom(widthAtom); @@ -134,17 +132,17 @@ export const ResizableSidebar = ({
{isOpen ? (titles[activePanel] ?? null) : null}
- - - + +
- {children} +
{children}
{side === 'left' && resizer} diff --git a/frontend/src/editor/components/Sidebar/RightSidebar.tsx b/frontend/src/editor/components/Sidebar/RightSidebar.tsx index ff95524..e8f69cb 100644 --- a/frontend/src/editor/components/Sidebar/RightSidebar.tsx +++ b/frontend/src/editor/components/Sidebar/RightSidebar.tsx @@ -1,11 +1,9 @@ import { useAtom } from 'jotai'; -import { useTranslation } from 'react-i18next'; import { rightSidebarPanelAtom, rightSidebarWidthAtom } from '$/states/sidebar.ts'; import { ResizableSidebar } from './ResizableSidebar'; const MIN_WIDTH = 220; export const RightSidebar = () => { - const { t } = useTranslation(); const [activePanel] = useAtom(rightSidebarPanelAtom); return ( @@ -16,7 +14,7 @@ export const RightSidebar = () => { closedPanel="none" minWidth={MIN_WIDTH} maxWidthRatio={0.4} - titles={{ annotations: t('sidebar.annotations.title', '批注') }} + titles={{ annotations: '批注' }} > {activePanel === 'annotations' && null} diff --git a/frontend/src/editor/components/Sidebar/SidebarTabBar.tsx b/frontend/src/editor/components/Sidebar/SidebarTabBar.tsx index 95557b4..8b12001 100644 --- a/frontend/src/editor/components/Sidebar/SidebarTabBar.tsx +++ b/frontend/src/editor/components/Sidebar/SidebarTabBar.tsx @@ -1,7 +1,6 @@ -import { DismissRegular } from '@fluentui/react-icons'; import { AnimatePresence, motion } from 'framer-motion'; +import { X } from 'lucide-react'; import { type FC, useEffect, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; import type { SidebarPanelType } from '$/states/sidebar.ts'; import type { SidebarTab } from './index'; import styles from './SidebarTabBar.module.css'; @@ -19,7 +18,6 @@ export const SidebarTabBar: FC = ({ onSelectTab, onCloseTab, }) => { - const { t } = useTranslation(); const prevTabIdsRef = useRef>(new Set()); const prevTabIds = prevTabIdsRef.current; @@ -32,7 +30,7 @@ export const SidebarTabBar: FC = ({ if (singleTab) { return (
- {singleTab.getTitle(t)} + {singleTab.title}
); } @@ -67,17 +65,17 @@ export const SidebarTabBar: FC = ({ } }} > - {tab.getTitle(t)} + {tab.title} ); diff --git a/frontend/src/editor/components/Sidebar/index.tsx b/frontend/src/editor/components/Sidebar/index.tsx index 5c62959..d8298ac 100644 --- a/frontend/src/editor/components/Sidebar/index.tsx +++ b/frontend/src/editor/components/Sidebar/index.tsx @@ -1,4 +1,3 @@ -import type { TFunction } from 'i18next'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { type FC, useMemo } from 'react'; import { @@ -18,19 +17,19 @@ const MAX_WIDTH = 650; export interface SidebarTab { id: Exclude; - getTitle: (t: TFunction) => string; + title: string; component: FC; } export const SIDEBAR_TABS: SidebarTab[] = [ { id: 'outline', - getTitle: (t) => t('sidebar.outline.title', '大纲'), + title: '大纲', component: OutlinePanel, }, { id: 'bpm', - getTitle: (t) => t('sidebar.bpm.title', 'BPM'), + title: 'BPM', component: BpmPanel, }, ]; diff --git a/frontend/src/editor/components/SuspensePlaceHolder/index.tsx b/frontend/src/editor/components/SuspensePlaceHolder/index.tsx index 4cb1d43..dcb97f7 100644 --- a/frontend/src/editor/components/SuspensePlaceHolder/index.tsx +++ b/frontend/src/editor/components/SuspensePlaceHolder/index.tsx @@ -1,30 +1,20 @@ -import { Flex, Spinner, Text } from '@radix-ui/themes'; import { motion } from 'framer-motion'; +import { Loader2 } from 'lucide-react'; import { type PropsWithChildren, Suspense } from 'react'; -import { useTranslation } from 'react-i18next'; + export const SuspensePlaceHolder = (props: PropsWithChildren) => { - const { t } = useTranslation(); return ( - - - - {t('ribbonBar.loading', '面板载入中...')} - - - + + 面板载入中... + } > {props.children} diff --git a/frontend/src/editor/hooks/useFileOpener.ts b/frontend/src/editor/hooks/useFileOpener.ts index 3a5957f..4bab589 100644 --- a/frontend/src/editor/hooks/useFileOpener.ts +++ b/frontend/src/editor/hooks/useFileOpener.ts @@ -12,7 +12,6 @@ import { import { openDB } from 'idb'; import { useAtomValue, useSetAtom } from 'jotai'; import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; import { uid } from 'uid'; import { audioEngine } from '$/modules/audio/audio-engine'; import { getProjectList } from '$/modules/project/autosave/autosave'; @@ -30,6 +29,7 @@ import { fileUpdateSessionAtom, isDirtyAtom, lyricLinesAtom, + markLyricLinesSavedAtom, newLyricLinesAtom, projectIdAtom, saveFileNameAtom, @@ -167,13 +167,13 @@ export const useFileOpener = () => { const setProjectId = useSetAtom(projectIdAtom); const setSaveFileName = useSetAtom(saveFileNameAtom); const setSourceFileContent = useSetAtom(sourceFileContentAtom); + const setMarkSaved = useSetAtom(markLyricLinesSavedAtom); const setConfirmDialog = useSetAtom(confirmDialogAtom); const isDirty = useAtomValue(isDirtyAtom); const fileUpdateSession = useAtomValue(fileUpdateSessionAtom); const defaultTtmlAuthorGithub = useAtomValue(defaultTtmlAuthorGithubAtom); const defaultTtmlAuthorGithubLogin = useAtomValue(defaultTtmlAuthorGithubLoginAtom); const handleTtmlError = useTtmlErrorHandler(); - const { t } = useTranslation(); const setPushNotification = useSetAtom(pushNotificationAtom); const normalizeLyricLines = useCallback((lyricLines: LyricLine[]): TTMLLyric => { @@ -269,9 +269,7 @@ export const useFileOpener = () => { lyricData = normalizeLyricLines(rawLines); } else { setPushNotification({ - title: t('error.unsupportedFileFormat', '不支持的文件格式: {ext}', { - ext, - }), + title: `不支持的文件格式: ${ext}`, level: 'error', source: 'useFileOpener', }); @@ -313,7 +311,7 @@ export const useFileOpener = () => { } catch (e) { fileOpenerLogger.error(`Failed to open file: ${file.name}`, e); setPushNotification({ - title: t('error.openFileFailed', '打开文件失败'), + title: '打开文件失败', level: 'error', source: 'useFileOpener', }); @@ -328,7 +326,6 @@ export const useFileOpener = () => { normalizeLyricLines, defaultTtmlAuthorGithub, defaultTtmlAuthorGithubLogin, - t, setPushNotification, handleTtmlError, ] @@ -364,18 +361,15 @@ export const useFileOpener = () => { if (isDirty) { setConfirmDialog({ open: true, - title: t('confirmDialog.openFile.title', '确认打开文件'), - description: t( - 'confirmDialog.openFile.description', - '当前文件有未保存的更改。如果继续,这些更改将会丢失。确定要打开新文件吗?' - ), + title: '确认打开文件', + description: '当前文件有未保存的更改。如果继续,这些更改将会丢失。确定要打开新文件吗?', onConfirm: run, }); } else { run(); } }, - [fileUpdateSession, isDirty, performOpenFile, setConfirmDialog, t] + [fileUpdateSession, isDirty, performOpenFile, setConfirmDialog] ); const openCachedAudio = useCallback(async () => { @@ -383,7 +377,7 @@ export const useFileOpener = () => { const record = await readAudioCache(); if (!record?.file) { setPushNotification({ - title: t('audioPanel.cachedAudioMissing', '未找到缓存音频'), + title: '未找到缓存音频', level: 'warning', source: 'useFileOpener', }); @@ -394,19 +388,19 @@ export const useFileOpener = () => { const file = new File([record.file], name, { type }); await loadAudioFile(file, { extractMetadata: false }); setPushNotification({ - title: t('audioPanel.cachedAudioLoaded', '已从缓存加载音频'), + title: '已从缓存加载音频', level: 'success', source: 'useFileOpener', }); } catch (error) { fileOpenerLogger.error('Failed to load cached audio', error); setPushNotification({ - title: t('audioPanel.cachedAudioFailed', '读取缓存音频失败'), + title: '读取缓存音频失败', level: 'error', source: 'useFileOpener', }); } - }, [loadAudioFile, setPushNotification, t]); + }, [loadAudioFile, setPushNotification]); const loadTtmlContent = useCallback( async (ttml: string, options?: { projectId?: string; fileName?: string }) => { @@ -435,6 +429,7 @@ export const useFileOpener = () => { }); return { ...prev, metadata: nextMetadata }; }); + setMarkSaved(); return true; }, [ @@ -442,6 +437,7 @@ export const useFileOpener = () => { defaultTtmlAuthorGithubLogin, handleTtmlError, setLyricLines, + setMarkSaved, setNewLyricLines, setProjectId, setSaveFileName, diff --git a/frontend/src/editor/hooks/useRelativeTime.ts b/frontend/src/editor/hooks/useRelativeTime.ts index 8c19a81..7a4bd22 100644 --- a/frontend/src/editor/hooks/useRelativeTime.ts +++ b/frontend/src/editor/hooks/useRelativeTime.ts @@ -1,22 +1,16 @@ import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; /** 相对时间文案(刚刚 / N 分钟前 / N 小时前 / N 天前)。 */ export const useRelativeTime = () => { - const { t } = useTranslation(); + return useCallback((timestamp: number) => { + const diff = Date.now() - timestamp; + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); - return useCallback( - (timestamp: number) => { - const diff = Date.now() - timestamp; - const minutes = Math.floor(diff / 60000); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (days > 0) return t('time.daysAgo', '{count}天前', { count: days }); - if (hours > 0) return t('time.hoursAgo', '{count}小时前', { count: hours }); - if (minutes > 0) return t('time.minutesAgo', '{count}分钟前', { count: minutes }); - return t('time.justNow', '刚刚'); - }, - [t] - ); + if (days > 0) return `${days}天前`; + if (hours > 0) return `${hours}小时前`; + if (minutes > 0) return `${minutes}分钟前`; + return '刚刚'; + }, []); }; diff --git a/frontend/src/editor/i18n/index.ts b/frontend/src/editor/i18n/index.ts deleted file mode 100644 index ddacb4b..0000000 --- a/frontend/src/editor/i18n/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; - -if (!i18n.isInitialized) { - i18n.use(initReactI18next).init({ - resources: { zh: { translation: {} } }, - lng: 'zh', - fallbackLng: 'zh', - interpolation: { - escapeValue: false, - }, - returnNull: false, - }); -} - -export default i18n; diff --git a/frontend/src/editor/index.css b/frontend/src/editor/index.css index 558bf6a..1f3d558 100644 --- a/frontend/src/editor/index.css +++ b/frontend/src/editor/index.css @@ -44,7 +44,7 @@ body, overscroll-behavior: none; } -.radix-themes { +:root { --default-font-family: 'MiSans', Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; --default-font-family-mono: @@ -78,18 +78,6 @@ body, } } -.rt-BaseButton:where(.rt-variant-soft) { - background-color: var(--accent-a3); - - &:hover { - background-color: var(--accent-a4); - } -} - -.rt-BaseButton:where(.rt-variant-soft):where(:active:not([data-state='open'])) { - background-color: var(--accent-a5); -} - .amll-lyric-player { margin: 0 !important; padding: 0 !important; diff --git a/frontend/src/editor/modules/audio/audio-engine.ts b/frontend/src/editor/modules/audio/audio-engine.ts index 1ef47b2..b55fd91 100644 --- a/frontend/src/editor/modules/audio/audio-engine.ts +++ b/frontend/src/editor/modules/audio/audio-engine.ts @@ -62,6 +62,11 @@ class AudioEngineWrapper extends EventTarget { fileName: '', }; + private _loadToken = 0; + private _inflightLoads = new Map>(); + private _inflightUrl: string | null = null; + private _abortCurrent: AbortController | null = null; + //#region Audio context basics private _ctx: AudioContext | null = null; get ctx() { @@ -325,58 +330,164 @@ class AudioEngineWrapper extends EventTarget { return this.mapAudioMetadataToTTML(); } - /** - * 从 URL 加载音频。 - * - * 审核页的参考音频是后端给的直链:上传音频的 - * `/api/v1/uploads/file/xxx`,或者**网易云解析出来的播放地址**。 - * - * ## 做法:完整下载 → 交给引擎 - * - * 整个文件先 `fetch` 成 `File`,再走 `loadMusic()`(内部 - * `decodeAudioData` 一次性解码)。**不流式播放**,理由: - * - * - 时长立刻可知,歌词编辑器的时间轴/进度条不用等 - * - 解码只有一次失败点(`decodeAudioData`),错误信息是浏览器给的, - * 比 wasm 的 `last_error` 可读得多 - * - 审核时经常来回拖进度、反复试听同一段,全量在手更稳 - * - * 关于 CORS:`fetch` 确实要求对方带 `Access-Control-Allow-Origin`, - * 但本站两条音源都满足 —— 上传音频走同源 `/api/v1/uploads/file/*`, - * 网易云直链本身也带 CORS 头(详情页 `PlayerBoot` 早就在 - * `fetch` + `decodeAudioData` 这么用了)。 - * - * 关于文件名:`parseAudioTrackMetadata` 靠扩展名判断音频格式, - * 而网易云直链的 URL 里通常没有扩展名,所以从 pathname 猜一个; - * 猜不到就用 `audio`(此时只是读不出内嵌歌名,解码不受影响)。 - */ async loadMusicFromUrl(url: string): Promise { - let blob: Blob; + const inflight = this._inflightLoads.get(url); + if (inflight) return inflight; + + const token = ++this._loadToken; + if (this._inflightUrl !== null && this._inflightUrl !== url) { + this._abortCurrent?.abort(); + } + const controller = new AbortController(); + this._abortCurrent = controller; + this._inflightUrl = url; + + const task = (async (): Promise => { + let blob: Blob; + try { + blob = await this.downloadAudioBlob(url, controller.signal); + } catch (e) { + if (e instanceof DOMException && e.name === 'AbortError') { + throw new Error('音频加载已被更新的请求取代', { cause: e }); + } + throw new Error( + `无法下载参考音频(${url.slice(0, 120)})。原始错误:${ + e instanceof Error ? e.message : String(e) + }`, + { cause: e } + ); + } + + if (token !== this._loadToken) { + throw new Error('音频加载已被更新的请求取代'); + } + + if (blob.size === 0) { + throw new Error(`参考音频为空(${url.slice(0, 120)})`); + } + + const name = audioFileNameFromUrl(url) || 'audio'; + const file = new File([blob], name, { + type: blob.type || 'audio/*', + }); + const metadata = await this.loadMusic(file); + + if (token !== this._loadToken) { + throw new Error('音频加载已被更新的请求取代'); + } + return metadata; + })(); + + this._inflightLoads.set(url, task); try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP ${response.status} ${response.statusText}`); + return await task; + } finally { + // 无论成功失败都摘掉在途登记,允许之后重试同一 URL。 + if (this._inflightLoads.get(url) === task) { + this._inflightLoads.delete(url); + } + // 该 URL 已不再是「在途」时,同步清掉标记,避免下次误判为「切换源」。 + if (this._inflightLoads.size === 0) { + this._inflightUrl = null; + this._abortCurrent = null; } - blob = await response.blob(); - } catch (e) { - console.error('fetch/读取 blob 失败', e); - throw new Error( - `无法下载参考音频(${url.slice(0, 120)})。原始错误:${ - e instanceof Error ? e.message : String(e) - }`, - { cause: e } - ); } + } - if (blob.size === 0) { - throw new Error(`参考音频为空(${url.slice(0, 120)})`); - } + private async downloadAudioBlob(url: string, signal: AbortSignal): Promise { + /** 最多尝试轮次(首轮 + 续传)。按实测每次约 4~8MB 被掐断,足够拉完 20MB。 */ + const MAX_ROUNDS = 8; + /** 每轮中断后的退避 */ + const RETRY_BACKOFF_MS = 700; + + const chunks: Uint8Array[] = []; + let received = 0; + let total: number | null = null; + let contentType = 'audio/*'; + let lastError: unknown; + + for (let round = 0; round < MAX_ROUNDS; round++) { + if (signal.aborted) throw new DOMException('Aborted', 'AbortError'); + const startedAt = Date.now(); + try { + const headers: Record = {}; + // 已收部分字节时,从断点续传 + if (received > 0) headers.Range = `bytes=${received}-`; + + const response = await fetch(url, { + signal, + cache: 'no-store', + credentials: 'same-origin', + headers, + }); - const name = audioFileNameFromUrl(url) || 'audio'; - const file = new File([blob], name, { - type: blob.type || 'audio/*', - }); - return this.loadMusic(file); + if (round === 0) { + if (response.status === 206) { + throw new Error('服务器返回了分段内容(206),音频未完整下载'); + } + if (!response.ok) { + // 4xx 是确定性失败(地址不对 / 无权限),重试无意义 + const err = new Error(`HTTP ${response.status} ${response.statusText}`); + if (response.status >= 400 && response.status < 500) { + (err as { retryable?: boolean }).retryable = false; + } + throw err; + } + contentType = response.headers.get('content-type') || contentType; + const len = Number(response.headers.get('content-length')); + total = Number.isFinite(len) && len > 0 ? len : null; + } else if (response.status !== 206) { + throw new Error(`续传请求未返回 206(HTTP ${response.status})`); + } + + const reader = response.body?.getReader(); + if (!reader) { + // 没有可读流(极老环境):退回一次性 blob() + return await response.blob(); + } + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + if (value && value.length > 0) { + chunks.push(value); + received += value.length; + } + } + + // 服务端声明了长度却没传完:当成本轮中断,继续续传 + if (total !== null && received < total) { + throw new Error(`响应提前结束:已收 ${received}/${total} 字节`); + } + + const blob = new Blob(chunks as BlobPart[], { type: contentType }); + if (round > 0) { + console.info('[audio-engine] 音频断点续传完成', { + url, + bytes: received, + rounds: round + 1, + }); + } + return blob; + } catch (e) { + // 主动 abort(被更新的请求取代)不重试 + if (e instanceof DOMException && e.name === 'AbortError') throw e; + if (e instanceof Error && (e as { retryable?: boolean }).retryable === false) throw e; + lastError = e; + console.warn('音频下载中断,将断点续传', { + url, + round: round + 1, + received, + total, + ms: Date.now() - startedAt, + error: e instanceof Error ? `${e.name}: ${e.message}` : String(e), + }); + if (round === MAX_ROUNDS - 1) break; + await new Promise((r) => setTimeout(r, RETRY_BACKOFF_MS)); + if (signal.aborted) throw new DOMException('Aborted', 'AbortError'); + } + } + console.error('音频下载重试全部失败', { url, received, total, error: lastError }); + throw lastError; } private mapAudioMetadataToTTML(): TTMLMetadata[] { diff --git a/frontend/src/editor/modules/audio/components/AudioSlider.tsx b/frontend/src/editor/modules/audio/components/AudioSlider.tsx index ac9cd55..516821e 100644 --- a/frontend/src/editor/modules/audio/components/AudioSlider.tsx +++ b/frontend/src/editor/modules/audio/components/AudioSlider.tsx @@ -1,4 +1,3 @@ -import { Card } from '@radix-ui/themes'; import { useAtomValue } from 'jotai'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { audioEngine } from '$/modules/audio/audio-engine'; @@ -127,13 +126,9 @@ export const AudioSlider = () => { ); return ( -
{ isDraggingRef={isDraggingRef} />
-
+
); }; diff --git a/frontend/src/editor/modules/audio/components/index.tsx b/frontend/src/editor/modules/audio/components/index.tsx index a5e6c0c..c672b9a 100644 --- a/frontend/src/editor/modules/audio/components/index.tsx +++ b/frontend/src/editor/modules/audio/components/index.tsx @@ -9,30 +9,13 @@ * https://github.com/amll-dev/amll-ttml-tool/blob/main/LICENSE */ -import { - ChevronDownFilled, - ChevronUpFilled, - MyLocationRegular, - PauseFilled, - PlayFilled, - Speaker2Filled, -} from '@fluentui/react-icons'; -import { - Button, - Card, - Flex, - Grid, - HoverCard, - IconButton, - Inset, - Slider, - Spinner, - Text, - Tooltip, -} from '@radix-ui/themes'; import { useAtom, useAtomValue, useSetAtom, useStore } from 'jotai'; +import { ChevronDown, ChevronUp, Loader2, LocateFixed, Pause, Play, Volume2 } from 'lucide-react'; import { type FC, memo, useCallback, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; +import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; +import { Slider } from '@/components/ui/slider'; +import { SimpleTooltip } from '@/components/ui/tooltip'; import { audioEngine } from '$/modules/audio/audio-engine'; import { AudioSlider } from '$/modules/audio/components/AudioSlider'; import { useBpmControl } from '$/modules/audio/hooks'; @@ -120,17 +103,9 @@ const CurrentTimeDisplay = memo(() => { }, []); return ( - + 00:00.000 - + ); }); @@ -143,7 +118,6 @@ export const AudioControls: FC = memo(() => { const [volume, setVolume] = useAtom(volumeAtom); const [playbackRate, setPlaybackRate] = useAtom(playbackRateAtom); const { lyricLines } = useAtomValue(lyricLinesAtom); - const { t } = useTranslation(); const openTab = useSetAtom(openTabAtom); const setLocateAction = useSetAtom(locateActionAtom); @@ -183,120 +157,127 @@ export const AudioControls: FC = memo(() => { }, [playbackRate]); return ( - - - - - -
- -
- - - - - - - - - - - {t('audioPanel.volume', '音量')} - setVolume(v[0] ?? volume)} - /> - - {(volume * 100).toFixed()}% - - {t('audioPanel.playbackRate', '播放速度')} - setPlaybackRate(v[0] ?? playbackRate)} - /> - - {playbackRate.toFixed(2)}x - - {t('audioPanel.bpm', 'BPM')} - - {bpmState.status === 'analyzing' && ( - - )} - {bpmState.status === 'completed' && ( - - )} - {bpmState.status === 'error' && ( - - )} - {bpmState.status === 'idle' && ( - - )} - - - - - - - - - {audioPlaying ? : } - - - - - + + +
+
+ +
+
+ + + + + +
+ 音量 + setVolume(v)} + /> + + {(volume * 100).toFixed()}% + + 播放速度 + setPlaybackRate(v)} + /> + + {playbackRate.toFixed(2)}x + + BPM +
+ {bpmState.status === 'analyzing' && ( + + )} + {bpmState.status === 'completed' && ( + + )} + {bpmState.status === 'error' && ( + + )} + {bpmState.status === 'idle' && ( + + )} +
+ +
+
+
+ + + + + + + {msToTimestamp(currentDuration)} + + + + + + + +
+
+ ); }); diff --git a/frontend/src/editor/modules/audio/hooks/useAudioFeedback.ts b/frontend/src/editor/modules/audio/hooks/useAudioFeedback.ts index b6488f1..3db1e58 100644 --- a/frontend/src/editor/modules/audio/hooks/useAudioFeedback.ts +++ b/frontend/src/editor/modules/audio/hooks/useAudioFeedback.ts @@ -1,6 +1,5 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useEffect, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; import { uid } from 'uid'; import { audioEngineStateAtom, audioErrorAtom } from '$/modules/audio/states/index.ts'; import { @@ -13,7 +12,6 @@ export const useAudioFeedback = () => { const engineState = useAtomValue(audioEngineStateAtom); const [errorMsg, setErrorMsg] = useAtom(audioErrorAtom); const notificationId = useRef(null); - const { t } = useTranslation(); const setPushNotification = useSetAtom(pushNotificationAtom); const upsertNotification = useSetAtom(upsertNotificationAtom); const removeNotification = useSetAtom(removeNotificationAtom); @@ -25,7 +23,7 @@ export const useAudioFeedback = () => { } upsertNotification({ id: notificationId.current, - title: t('audio.status.loading', '正在加载音频...'), + title: '正在加载音频...', level: 'info', source: 'Audio', }); @@ -36,12 +34,12 @@ export const useAudioFeedback = () => { removeNotification(notificationId.current); notificationId.current = null; } - }, [engineState, t, upsertNotification, removeNotification]); + }, [engineState, upsertNotification, removeNotification]); useEffect(() => { if (errorMsg) { setPushNotification({ - title: `${t('audio.error.workerError', '处理音频时出错')}: ${errorMsg}`, + title: `处理音频时出错: ${errorMsg}`, level: 'error', source: 'Audio', }); @@ -52,5 +50,5 @@ export const useAudioFeedback = () => { setErrorMsg(null); } - }, [errorMsg, setErrorMsg, t, setPushNotification, removeNotification]); + }, [errorMsg, setErrorMsg, setPushNotification, removeNotification]); }; diff --git a/frontend/src/editor/modules/keyboard/types.ts b/frontend/src/editor/modules/keyboard/types.ts index c292cf1..f1b3036 100644 --- a/frontend/src/editor/modules/keyboard/types.ts +++ b/frontend/src/editor/modules/keyboard/types.ts @@ -1,8 +1,6 @@ -import type { ParseKeys } from 'i18next'; import type { WritableAtom } from 'jotai'; import type { RESET_KEYBINDING } from '$/utils/keybindings'; - -export type I18nKey = ParseKeys<'translation'>; +export type I18nKey = string; export type KeyBindingsConfig = string[]; export interface KeyBindingCommand { diff --git a/frontend/src/editor/modules/lyric-drag/DragGhostRenderer.tsx b/frontend/src/editor/modules/lyric-drag/DragGhostRenderer.tsx index 647baec..715704c 100644 --- a/frontend/src/editor/modules/lyric-drag/DragGhostRenderer.tsx +++ b/frontend/src/editor/modules/lyric-drag/DragGhostRenderer.tsx @@ -1,47 +1,32 @@ -import { ArrowExportRegular, CopyRegular } from '@fluentui/react-icons'; import { useAtomValue } from 'jotai'; -import { useEffect, useRef, useState } from 'react'; +import { ArrowUpFromLine, Copy } from 'lucide-react'; +import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; -import { useTranslation } from 'react-i18next'; import { draggedCountAtom, isCopyModeAtom } from '$/states/main'; import styles from './DragGhost.module.css'; export const DragGhostRenderer = () => { - const { t } = useTranslation(); - const draggedCount = useAtomValue(draggedCountAtom); const isCopyMode = useAtomValue(isCopyModeAtom); const [portalContainer, setPortalContainer] = useState(null); - const anchorRef = useRef(null); useEffect(() => { - if (anchorRef.current) { - const themeContainer = anchorRef.current.closest('.radix-themes'); - setPortalContainer(themeContainer || document.body); - } + setPortalContainer(document.body); }, []); - const actionText = isCopyMode ? t('common.copyTo', '复制到') : t('common.moveTo', '移动到'); - - if (!portalContainer) { - return )} - {rowHasDuplicate && ( - - {t('metadataDialog.duplicateMsg', '存在重复的元数据值')} - - )} - ); @@ -532,7 +551,6 @@ export const MetadataEditor = () => { [resolvedTheme] ); - const { t } = useTranslation(); /** * 从网易云 ID 拉取歌曲资料并填进元数据。 * @@ -575,140 +593,116 @@ export const MetadataEditor = () => { return [ // 歌词所匹配的歌曲名 { - label: t('metadataDialog.builtinOptions.musicName', '歌曲名称'), + label: '歌曲名称', value: 'musicName', - icon: , + icon: , }, // 歌词所匹配的歌手名 { - label: t('metadataDialog.builtinOptions.artists', '歌曲的艺术家'), + label: '歌曲的艺术家', value: 'artists', - icon: , + icon: , suggestion: true, validation: { verifier: (value: string) => !/^.+[,;&,;、].+$/.test(value), - message: t( - 'metadataDialog.builtinOptions.artistsInvalidMsg', - '如果有多个艺术家,请多次添加该键值,避免使用分隔符' - ), + message: '如果有多个艺术家,请多次添加该键值,避免使用分隔符', }, }, // 歌词所匹配的词曲作者 { - label: t('metadataDialog.builtinOptions.songwriter', '词曲作者'), + label: '词曲作者', value: 'songwriter', - icon: , + icon: , validation: { verifier: (value: string) => !/^.+[,;&,;、].+$/.test(value), - message: t( - 'metadataDialog.builtinOptions.songwriterInvalidMsg', - '如果有多个词曲作者,请多次添加该键值,避免使用分隔符' - ), + message: '如果有多个词曲作者,请多次添加该键值,避免使用分隔符', }, }, // 歌词所匹配的专辑名 { - label: t('metadataDialog.builtinOptions.album', '歌曲的专辑名'), + label: '歌曲的专辑名', value: 'album', - icon: , + icon: , }, // 歌词所匹配的网易云音乐 ID { - label: t('metadataDialog.builtinOptions.ncmMusicId', '网易云音乐 ID'), + label: '网易云音乐 ID', value: 'ncmMusicId', icon: , isLinkable: true, urlFormatter: (val) => getPlatformUrl('ncmMusicId', val), validation: { verifier: numeric, - message: t( - 'metadataDialog.builtinOptions.ncmMusicIdInvalidMsg', - '网易云音乐 ID 应为纯数字' - ), + message: '网易云音乐 ID 应为纯数字', severe: true, }, }, // 歌词所匹配的 QQ 音乐 ID { - label: t('metadataDialog.builtinOptions.qqMusicId', 'QQ 音乐 ID'), + label: 'QQ 音乐 ID', value: 'qqMusicId', icon: , isLinkable: true, urlFormatter: (val) => getPlatformUrl('qqMusicId', val), validation: { verifier: alphanumeric, - message: t( - 'metadataDialog.builtinOptions.qqMusicIdInvalidMsg', - 'QQ 音乐 ID 应为字母或数字' - ), + message: 'QQ 音乐 ID 应为字母或数字', severe: true, }, }, // 歌词所匹配的 Spotify 音乐 ID { - label: t('metadataDialog.builtinOptions.spotifyId', 'Spotify 音乐 ID'), + label: 'Spotify 音乐 ID', value: 'spotifyId', icon: , isLinkable: true, urlFormatter: (val) => getPlatformUrl('spotifyId', val), validation: { verifier: alphanumeric, - message: t( - 'metadataDialog.builtinOptions.spotifyIdInvalidMsg', - 'Spotify ID 应为字母或数字' - ), + message: 'Spotify ID 应为字母或数字', severe: true, }, }, // 歌词所匹配的 Apple Music 音乐 ID { - label: t('metadataDialog.builtinOptions.appleMusicId', 'Apple Music 音乐 ID'), + label: 'Apple Music 音乐 ID', value: 'appleMusicId', icon: , isLinkable: true, urlFormatter: (val) => getPlatformUrl('appleMusicId', val), validation: { verifier: numeric, - message: t( - 'metadataDialog.builtinOptions.appleMusicIdInvalidMsg', - 'Apple Music ID 应为纯数字' - ), + message: 'Apple Music ID 应为纯数字', severe: true, }, }, // 歌词所匹配的 ISRC 编码 { - label: t('metadataDialog.builtinOptions.isrc', '歌曲的 ISRC 号码'), + label: '歌曲的 ISRC 号码', value: 'isrc', - icon: , + icon: , isLinkable: true, urlFormatter: (val) => getPlatformUrl('isrc', val), validation: { verifier: (value: string) => /^[A-Z]{2}-?[A-Z0-9]{3}-?\d{2}-?\d{5}$/.test(value), - message: t( - 'metadataDialog.builtinOptions.isrcInvalidMsg', - 'ISRC 编码格式应为 CC-XXX-YY-NNNNN' - ), + message: 'ISRC 编码格式应为 CC-XXX-YY-NNNNN', severe: true, }, }, // 逐词歌词作者 GitHub ID,例如 39523898 { - label: t('metadataDialog.builtinOptions.ttmlAuthorGithub', '歌词作者 GitHub ID'), + label: '歌词作者 GitHub ID', value: 'ttmlAuthorGithub', icon: , validation: { verifier: numeric, - message: t( - 'metadataDialog.builtinOptions.ttmlAuthorGithubInvalidMsg', - 'GitHub ID 应为纯数字' - ), + message: 'GitHub ID 应为纯数字', severe: true, }, }, // 逐词歌词作者 GitHub 用户名,例如 Steve-xmh { - label: t('metadataDialog.builtinOptions.ttmlAuthorGithubLogin', '歌词作者 GitHub 用户名'), + label: '歌词作者 GitHub 用户名', value: 'ttmlAuthorGithubLogin', icon: , isLinkable: true, @@ -716,15 +710,12 @@ export const MetadataEditor = () => { validation: { verifier: (value: string) => /^(?!.*--)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/.test(value), - message: t( - 'metadataDialog.builtinOptions.ttmlAuthorGithubLoginInvalidMsg', - 'GitHub username should be alphanumeric or hyphens, up to 39 characters' - ), + message: 'GitHub username should be alphanumeric or hyphens, up to 39 characters', severe: true, }, }, ]; - }, [t]); + }, []); const customOptions: SelectOption[] = useMemo( () => @@ -733,7 +724,7 @@ export const MetadataEditor = () => { .map((metadata) => ({ label: metadata.key, value: metadata.key, - icon: , + icon: , })), [builtinOptions, lyricLines.metadata] ); @@ -772,7 +763,7 @@ export const MetadataEditor = () => { }, [builtinOptions, setLyricLines]); return ( - { setMetadataEditorDialog(open); @@ -790,38 +781,24 @@ export const MetadataEditor = () => { } }} > - `,它在自己所在的元素上渲染出 - * `data-accent-color="indigo"`(Theme 的默认值), - * 而 Radix 的色阶正是用这个属性选择器声明的: - * - * [data-accent-color='indigo'] { --accent-9: … } ← 0,1,0 - * - * 浮层在 body 子树里,跟工作台那套 `.editor-radix-theme` - * 覆写不在一条继承链上,CSS 类很难稳定压过它 —— - * 这就是「弹窗里按钮/复选框颜色和外面不一样」的原因。 - * - * 内联 style 优先级高于任何非 `!important` 声明, - * 直接写在 Content 元素上必然生效,也不用去猜层叠顺序。 - * - * 色值与 `components/review/editor-theme.css` 保持一致 - * (站点品牌红 #e0303f),深色模式的提亮档在下面按需覆盖。 - */ - style={accentStyle} - > - - {t('metadataDialog.title', '元数据编辑器')} - + {/* + * 主题色用**内联 style**下发,不用 CSS 类。 + * + * 原因:浮层 Portal 到 body,跟工作台的 CSS 覆写不在一条继承链上, + * CSS 类很难稳定压过浮层自身的色阶声明 —— 这就是 + * 「弹窗里按钮/复选框颜色和外面不一样」的原因。 + * + * 内联 style 优先级高于任何非 `!important` 声明,直接写在 + * Content 元素上必然生效,也不用去猜层叠顺序。 + * + * 色值与 `components/review/editor-theme.css` 保持一致 + * (站点品牌红 #e0303f),深色模式的提亮档在下面按需覆盖。 + */} + + 元数据编辑器
- - {activeOption?.label} - +
{activeOption?.label}
{activeOption && ( - - {activeOption.value} - +
{activeOption.value}
)}
@@ -897,36 +870,29 @@ export const MetadataEditor = () => { - +
- - +
-
-
+ + ); }; diff --git a/frontend/src/editor/modules/project/modals/VocalTagsEditor.tsx b/frontend/src/editor/modules/project/modals/VocalTagsEditor.tsx index 9d0b188..21f6fb6 100644 --- a/frontend/src/editor/modules/project/modals/VocalTagsEditor.tsx +++ b/frontend/src/editor/modules/project/modals/VocalTagsEditor.tsx @@ -1,9 +1,10 @@ -import { Add16Regular, Delete16Regular } from '@fluentui/react-icons'; -import { Button, Dialog, Flex, Text, TextField } from '@radix-ui/themes'; import { useAtom } from 'jotai'; import { useImmerAtom } from 'jotai-immer'; +import { Plus, Trash2 } from 'lucide-react'; import { useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; import { vocalTagsEditorDialogAtom } from '$/states/dialogs.ts'; import { lyricLinesAtom } from '$/states/main.ts'; import type { TTMLLyric } from '$/types/ttml'; @@ -54,7 +55,6 @@ const reassignVocalIds = (draft: TTMLLyric) => { export const VocalTagsEditor = () => { const [open, setOpen] = useAtom(vocalTagsEditorDialogAtom); const [lyricLines, setLyricLines] = useImmerAtom(lyricLinesAtom); - const { t } = useTranslation(); const [editingIndex, setEditingIndex] = useState(null); const [editingValue, setEditingValue] = useState(''); const [isAdding, setIsAdding] = useState(false); @@ -68,23 +68,19 @@ export const VocalTagsEditor = () => { }, [lyricLines.metadata]); return ( - - + +
- - {t('vocalTagsDialog.title', '演唱者标签')} - + 演唱者标签
- - - {t('vocalTagsDialog.mappingTitle', '演唱者标签')} - - +
+ 演唱者标签 +
- - +
+
- {vocalTags.length === 0 && ( - - {t('vocalTagsDialog.empty', '暂无演唱者标签。')} - - )} + {vocalTags.length === 0 && 暂无演唱者标签。}
{vocalTags.map((tag, index) => editingIndex === index ? ( - setEditingValue(e.currentTarget.value)} onKeyDown={(e) => { if (e.key !== 'Enter') return; @@ -168,24 +159,22 @@ export const VocalTagsEditor = () => { ) )} {isAdding ? ( - setNewTagValue(e.currentTarget.value)} onKeyDown={(e) => { if (e.key !== 'Enter') return; @@ -208,22 +197,22 @@ export const VocalTagsEditor = () => { ) : ( )}
-
-
+ + ); }; diff --git a/frontend/src/editor/modules/project/modals/metadata-dialog-theme.ts b/frontend/src/editor/modules/project/modals/metadata-dialog-theme.ts index 6462939..05b0b6e 100644 --- a/frontend/src/editor/modules/project/modals/metadata-dialog-theme.ts +++ b/frontend/src/editor/modules/project/modals/metadata-dialog-theme.ts @@ -1,32 +1,6 @@ -/** - * 弹窗 Radix accent 色阶下发 - * - * ## 为什么需要它 - * - * `MetadataEditor` 的 Dialog Portal 到 `body`,**不在 `editor-body` 子树里**, - * 所以 `editor-theme.css` 里挂在 `body.editor-body [data-accent-color]` - * 的品牌红色阶对它不生效 —— 弹窗里按钮/复选框/开关会回落到 Radix - * 内置的indigo。 - * - * 而 Radix 的 `Dialog.Content` 内部又套了一层 ``, - * 在自己元素上渲染 `data-accent-color="indigo"`,Radix 用 - * `[data-accent-color='…']`(优先级 0,1,0)声明色阶。CSS 类很难稳定 - * 压过它,所以这里用**内联 style**(优先级最高,一定生效)。 - * - * ## 色值来源:CSS 变量,不是硬编码 - * - * 色阶的唯一权威定义在 `components/review/editor-theme.css` - * (浅色档 + 深色档)。这里**读取**那些变量再回填成内联 style, - * 而不是抄一份色值 —— 之前这里硬编码了 34 个色值,和 CSS 那份 - * 逐字相同,改配色时必然两边漂移。 - * - * 读取时机:Portal 内容挂载后从 `document.body` 上取 - * (`editor-theme.css` 挂在 body 上,深色档由 `body.editor-body.dark` - * 决定)。取不到时回落到当前文件里的兜底值,保证不会渲染成 indigo。 - */ import type React from 'react'; -/** Radix accent 梯度:1~13 纯色,a1~a13 半透明,另有 4 个语义别名 */ +/** accent 梯度:1~13 纯色,a1~a13 半透明,另有 4 个语义别名 */ const ACCENT_VARS = [ '--accent-1', '--accent-2', @@ -59,12 +33,6 @@ const ACCENT_VARS = [ '--accent-track', ] as const; -/** - * 兜底色阶(品牌红)。 - * - * 仅在读不到 CSS 变量时使用 —— 例如 `editor-theme.css` 未加载 - * (如单独跑 Storybook)。正常路径下读到的是 CSS 里那份。 - */ const FALLBACK = { '--accent-1': '#fff5f6', '--accent-2': '#feeaec', @@ -97,12 +65,6 @@ const FALLBACK = { '--accent-track': '#fbdde0', } as const satisfies Record<(typeof ACCENT_VARS)[number], string>; -/** - * 从 body 上读取当前主题下的品牌红色阶。 - * - * 深色档由 `body.editor-body.dark` 提供,所以必须从 **body** 读而不是 - * 从弹窗元素读(弹窗不在 editor-body 内)。读到的值已经带上主题。 - */ export function readBrandAccentStyle(): React.CSSProperties { if (typeof document === 'undefined') return FALLBACK as React.CSSProperties; diff --git a/frontend/src/editor/modules/review/modals/ReportFormatEditor.tsx b/frontend/src/editor/modules/review/modals/ReportFormatEditor.tsx index 3ac1dff..f871a0a 100644 --- a/frontend/src/editor/modules/review/modals/ReportFormatEditor.tsx +++ b/frontend/src/editor/modules/review/modals/ReportFormatEditor.tsx @@ -1,14 +1,8 @@ -import { Box, Button, Flex, Switch, Text, TextArea } from '@radix-ui/themes'; +import { RotateCcw, Bold, Italic, Heading1, Heading2, Heading3, AlignJustify } from 'lucide-react'; import { useMemo, useRef, useState } from 'react'; -import { - ArrowReset20Regular, - TextBold20Regular, - TextHeader120Regular, - TextHeader220Regular, - TextHeader320Regular, - TextItalic20Regular, - TextLineSpacing20Regular, -} from '@fluentui/react-icons'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import { Textarea } from '@/components/ui/textarea'; import { DEFAULT_REVIEW_REPORT_FORMAT, resetReviewReportBlockFormat, @@ -126,55 +120,24 @@ export const ReportFormatEditor = ({ value, onChange, report }: ReportFormatEdit const insertVariable = (name: string) => insertTemplateText(`{{${name}}}`); return ( - +
{/* 区块列表 + 恢复默认 */} - - - 选择要编辑的区块,模板里用 {'{{变量}}'} 引用内容 - +
+ 选择要编辑的区块,模板里用 {'{{变量}}'} 引用内容 - +
- +
{/* 左:区块列表(独立滚动) */} - +
{reviewReportFormatBlockDefinitions.map((definition) => { const selected = selectedDefinition.kind === definition.kind; const changed = changedKinds.has(definition.kind); @@ -183,43 +146,16 @@ export const ReportFormatEditor = ({ value, onChange, report }: ReportFormatEdit key={definition.kind} type="button" onClick={() => setSelectedKind(definition.kind)} - style={{ - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: 4, - width: '100%', - minWidth: 0, - padding: '8px 10px', - textAlign: 'left', - cursor: 'pointer', - borderRadius: 6, - border: `1px solid ${selected ? 'var(--amll-primary)' : 'var(--amll-border)'}`, - background: selected - ? 'var(--amll-primary-soft)' + className={`flex w-full min-w-0 cursor-pointer flex-col items-start gap-1 rounded-md border px-2.5 py-2 text-left text-inherit transition-colors ${ + selected + ? 'border-primary bg-primary-soft' : changed - ? 'var(--amll-muted)' - : 'var(--amll-card)', - color: 'inherit', - }} + ? 'border-line bg-muted' + : 'border-line bg-card hover:bg-surface-2' + }`} > - - + + {definition.label} {/* @@ -227,181 +163,125 @@ export const ReportFormatEditor = ({ value, onChange, report }: ReportFormatEdit * 星号太弱,用户会以为是标点符号而不是状态。 */} {changed && ( - + 已改 )} - + {definition.description} ); })} - +
{/* 右:模板编辑区(独立滚动,宽度不撑破卡片) */} - - - - {selectedDefinition.label} - +
+
+ {selectedDefinition.label} - +
-