feat: support settings

This commit is contained in:
Henry Li
2026-01-20 23:43:21 +08:00
parent 3191a3845f
commit 10d253f461
25 changed files with 1355 additions and 217 deletions

View File

@@ -27,6 +27,7 @@
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
@@ -47,7 +48,7 @@
"lucide-react": "^0.562.0",
"motion": "^12.26.2",
"nanoid": "^5.1.6",
"next": "^15.2.8",
"next": "^16.1.4",
"next-themes": "^0.4.6",
"nuxt-og-image": "^5.1.13",
"react": "^19.0.0",

425
frontend/pnpm-lock.yaml generated
View File

@@ -41,6 +41,9 @@ importers:
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.4(@types/react@19.2.8)(react@19.2.3)
'@radix-ui/react-switch':
specifier: ^1.2.6
version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-toggle':
specifier: ^1.1.10
version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -73,7 +76,7 @@ importers:
version: 1.2.1
better-auth:
specifier: ^1.3
version: 1.4.12(next@15.2.8(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vue@3.5.26(typescript@5.9.3))
version: 1.4.12(next@16.1.4(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vue@3.5.26(typescript@5.9.3))
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -102,8 +105,8 @@ importers:
specifier: ^5.1.6
version: 5.1.6
next:
specifier: ^15.2.8
version: 15.2.8(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
specifier: ^16.1.4
version: 16.1.4(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -514,119 +517,155 @@ packages:
'@iconify/utils@3.1.0':
resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==}
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
'@img/colour@1.0.0':
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.33.5':
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.0.4':
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.0.4':
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.0.4':
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-ia32@0.33.5':
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.33.5':
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
@@ -671,60 +710,60 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
'@next/env@15.2.8':
resolution: {integrity: sha512-TaEsAki14R7BlgywA05t2PFYfwZiNlGUHyIQHVyloXX3y+Dm0HUITe5YwTkjtuOQuDhuuLotNEad4VtnmE11Uw==}
'@next/env@16.1.4':
resolution: {integrity: sha512-gkrXnZyxPUy0Gg6SrPQPccbNVLSP3vmW8LU5dwEttEEC1RwDivk8w4O+sZIjFvPrSICXyhQDCG+y3VmjlJf+9A==}
'@next/eslint-plugin-next@15.5.9':
resolution: {integrity: sha512-kUzXx0iFiXw27cQAViE1yKWnz/nF8JzRmwgMRTMh8qMY90crNsdXJRh2e+R0vBpFR3kk1yvAR7wev7+fCCb79Q==}
'@next/swc-darwin-arm64@15.2.5':
resolution: {integrity: sha512-4OimvVlFTbgzPdA0kh8A1ih6FN9pQkL4nPXGqemEYgk+e7eQhsst/p35siNNqA49eQA6bvKZ1ASsDtu9gtXuog==}
'@next/swc-darwin-arm64@16.1.4':
resolution: {integrity: sha512-T8atLKuvk13XQUdVLCv1ZzMPgLPW0+DWWbHSQXs0/3TjPrKNxTmUIhOEaoEyl3Z82k8h/gEtqyuoZGv6+Ugawg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.2.5':
resolution: {integrity: sha512-ohzRaE9YbGt1ctE0um+UGYIDkkOxHV44kEcHzLqQigoRLaiMtZzGrA11AJh2Lu0lv51XeiY1ZkUvkThjkVNBMA==}
'@next/swc-darwin-x64@16.1.4':
resolution: {integrity: sha512-AKC/qVjUGUQDSPI6gESTx0xOnOPQ5gttogNS3o6bA83yiaSZJek0Am5yXy82F1KcZCx3DdOwdGPZpQCluonuxg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.2.5':
resolution: {integrity: sha512-FMSdxSUt5bVXqqOoZCc/Seg4LQep9w/fXTazr/EkpXW2Eu4IFI9FD7zBDlID8TJIybmvKk7mhd9s+2XWxz4flA==}
'@next/swc-linux-arm64-gnu@16.1.4':
resolution: {integrity: sha512-POQ65+pnYOkZNdngWfMEt7r53bzWiKkVNbjpmCt1Zb3V6lxJNXSsjwRuTQ8P/kguxDC8LRkqaL3vvsFrce4dMQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@15.2.5':
resolution: {integrity: sha512-4ZNKmuEiW5hRKkGp2HWwZ+JrvK4DQLgf8YDaqtZyn7NYdl0cHfatvlnLFSWUayx9yFAUagIgRGRk8pFxS8Qniw==}
'@next/swc-linux-arm64-musl@16.1.4':
resolution: {integrity: sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@15.2.5':
resolution: {integrity: sha512-bE6lHQ9GXIf3gCDE53u2pTl99RPZW5V1GLHSRMJ5l/oB/MT+cohu9uwnCK7QUph2xIOu2a6+27kL0REa/kqwZw==}
'@next/swc-linux-x64-gnu@16.1.4':
resolution: {integrity: sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@15.2.5':
resolution: {integrity: sha512-y7EeQuSkQbTAkCEQnJXm1asRUuGSWAchGJ3c+Qtxh8LVjXleZast8Mn/rL7tZOm7o35QeIpIcid6ufG7EVTTcA==}
'@next/swc-linux-x64-musl@16.1.4':
resolution: {integrity: sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@15.2.5':
resolution: {integrity: sha512-gQMz0yA8/dskZM2Xyiq2FRShxSrsJNha40Ob/M2n2+JGRrZ0JwTVjLdvtN6vCxuq4ByhOd4a9qEf60hApNR2gQ==}
'@next/swc-win32-arm64-msvc@16.1.4':
resolution: {integrity: sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.2.5':
resolution: {integrity: sha512-tBDNVUcI7U03+3oMvJ11zrtVin5p0NctiuKmTGyaTIEAVj9Q77xukLXGXRnWxKRIIdFG4OTA2rUVGZDYOwgmAA==}
'@next/swc-win32-x64-msvc@16.1.4':
resolution: {integrity: sha512-JSVlm9MDhmTXw/sO2PE/MRj+G6XOSMZB+BcZ0a7d6KwVFZVpkHcb2okyoYFBaco6LeiL53BBklRlOrDDbOeE5w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1094,6 +1133,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-switch@1.2.6':
resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
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.11':
resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==}
peerDependencies:
@@ -1485,9 +1537,6 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -2114,6 +2163,10 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.9.16:
resolution: {integrity: sha512-KeUZdBuxngy825i8xvzaK1Ncnkx0tBmb3k8DkEuqjKRkmtvNTjey2ZsNeh8Dw4lfKvbCOu9oeNx2TKm2vHqcRw==}
hasBin: true
best-effort-json-parser@1.2.1:
resolution: {integrity: sha512-UICSLibQdzS1f+PBsi3u2YE3SsdXcWicHUg3IMvfuaePS2AYnZJdJeKhGv5OM8/mqJwPt79aDrEJ1oa84tELvw==}
@@ -2194,10 +2247,6 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
c12@3.3.3:
resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==}
peerDependencies:
@@ -2297,13 +2346,6 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@@ -3140,9 +3182,6 @@ packages:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
is-arrayish@0.3.4:
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
is-async-function@2.1.1:
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
engines: {node: '>= 0.4'}
@@ -3746,13 +3785,13 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@15.2.8:
resolution: {integrity: sha512-pe2trLKZTdaCuvNER0S9Wp+SP2APf7SfFmyUP9/w1SFA2UqmW0u+IsxCKkiky3n6um7mryaQIlgiDnKrf1ZwIw==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
next@16.1.4:
resolution: {integrity: sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
'@playwright/test': ^1.41.2
'@playwright/test': ^1.51.1
babel-plugin-react-compiler: '*'
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
@@ -4265,8 +4304,8 @@ packages:
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
engines: {node: '>= 0.4'}
sharp@0.33.5:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0:
@@ -4300,9 +4339,6 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-swizzle@0.2.4:
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
simple-wcswidth@1.1.2:
resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==}
@@ -4343,10 +4379,6 @@ packages:
peerDependencies:
react: ^18.0.0 || ^19.0.0
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
string.prototype.codepointat@0.2.1:
resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==}
@@ -5095,79 +5127,101 @@ snapshots:
'@iconify/types': 2.0.0
mlly: 1.8.0
'@img/sharp-darwin-arm64@0.33.5':
'@img/colour@1.0.0':
optional: true
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.4
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
'@img/sharp-darwin-x64@0.33.5':
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.0.4
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.0.4':
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.0.4':
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.0.4':
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm@1.0.5':
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.0.4':
'@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
'@img/sharp-libvips-linux-x64@1.0.4':
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.33.5':
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.0.4
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
'@img/sharp-linux-arm@0.33.5':
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.0.5
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
'@img/sharp-linux-s390x@0.33.5':
'@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.0.4
'@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
'@img/sharp-linux-x64@0.33.5':
'@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.0.4
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
'@img/sharp-linuxmusl-arm64@0.33.5':
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.33.5':
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
'@img/sharp-wasm32@0.33.5':
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.8.1
optional: true
'@img/sharp-win32-ia32@0.33.5':
'@img/sharp-win32-arm64@0.34.5':
optional: true
'@img/sharp-win32-x64@0.33.5':
'@img/sharp-win32-ia32@0.34.5':
optional: true
'@img/sharp-win32-x64@0.34.5':
optional: true
'@jridgewell/gen-mapping@0.3.13':
@@ -5228,34 +5282,34 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
'@next/env@15.2.8': {}
'@next/env@16.1.4': {}
'@next/eslint-plugin-next@15.5.9':
dependencies:
fast-glob: 3.3.1
'@next/swc-darwin-arm64@15.2.5':
'@next/swc-darwin-arm64@16.1.4':
optional: true
'@next/swc-darwin-x64@15.2.5':
'@next/swc-darwin-x64@16.1.4':
optional: true
'@next/swc-linux-arm64-gnu@15.2.5':
'@next/swc-linux-arm64-gnu@16.1.4':
optional: true
'@next/swc-linux-arm64-musl@15.2.5':
'@next/swc-linux-arm64-musl@16.1.4':
optional: true
'@next/swc-linux-x64-gnu@15.2.5':
'@next/swc-linux-x64-gnu@16.1.4':
optional: true
'@next/swc-linux-x64-musl@15.2.5':
'@next/swc-linux-x64-musl@16.1.4':
optional: true
'@next/swc-win32-arm64-msvc@15.2.5':
'@next/swc-win32-arm64-msvc@16.1.4':
optional: true
'@next/swc-win32-x64-msvc@15.2.5':
'@next/swc-win32-x64-msvc@16.1.4':
optional: true
'@noble/ciphers@2.1.1': {}
@@ -5647,6 +5701,21 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.8
'@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3)
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3)
'@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
'@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@radix-ui/primitive': 1.1.3
@@ -5934,8 +6003,6 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
'@swc/counter@0.1.3': {}
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -6601,9 +6668,11 @@ snapshots:
base64-js@1.5.1: {}
baseline-browser-mapping@2.9.16: {}
best-effort-json-parser@1.2.1: {}
better-auth@1.4.12(next@15.2.8(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vue@3.5.26(typescript@5.9.3)):
better-auth@1.4.12(next@16.1.4(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vue@3.5.26(typescript@5.9.3)):
dependencies:
'@better-auth/core': 1.4.12(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)
'@better-auth/telemetry': 1.4.12(@better-auth/core@1.4.12(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))
@@ -6618,7 +6687,7 @@ snapshots:
nanostores: 1.1.0
zod: 4.3.5
optionalDependencies:
next: 15.2.8(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 16.1.4(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
vue: 3.5.26(typescript@5.9.3)
@@ -6645,10 +6714,6 @@ snapshots:
dependencies:
fill-range: 7.1.1
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
c12@3.3.3:
dependencies:
chokidar: 5.0.0
@@ -6763,18 +6828,6 @@ snapshots:
color-name@1.1.4: {}
color-string@1.9.1:
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.4
optional: true
color@4.2.3:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
optional: true
comma-separated-tokens@2.0.3: {}
commander@7.2.0: {}
@@ -7861,9 +7914,6 @@ snapshots:
call-bound: 1.0.4
get-intrinsic: 1.3.0
is-arrayish@0.3.4:
optional: true
is-async-function@2.1.1:
dependencies:
async-function: 1.0.0
@@ -8641,28 +8691,27 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
next@15.2.8(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
next@16.1.4(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@next/env': 15.2.8
'@swc/counter': 0.1.3
'@next/env': 16.1.4
'@swc/helpers': 0.5.15
busboy: 1.6.0
baseline-browser-mapping: 2.9.16
caniuse-lite: 1.0.30001764
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(react@19.2.3)
optionalDependencies:
'@next/swc-darwin-arm64': 15.2.5
'@next/swc-darwin-x64': 15.2.5
'@next/swc-linux-arm64-gnu': 15.2.5
'@next/swc-linux-arm64-musl': 15.2.5
'@next/swc-linux-x64-gnu': 15.2.5
'@next/swc-linux-x64-musl': 15.2.5
'@next/swc-win32-arm64-msvc': 15.2.5
'@next/swc-win32-x64-msvc': 15.2.5
'@next/swc-darwin-arm64': 16.1.4
'@next/swc-darwin-x64': 16.1.4
'@next/swc-linux-arm64-gnu': 16.1.4
'@next/swc-linux-arm64-musl': 16.1.4
'@next/swc-linux-x64-gnu': 16.1.4
'@next/swc-linux-x64-musl': 16.1.4
'@next/swc-win32-arm64-msvc': 16.1.4
'@next/swc-win32-x64-msvc': 16.1.4
'@opentelemetry/api': 1.9.0
sharp: 0.33.5
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -9270,31 +9319,36 @@ snapshots:
es-errors: 1.3.0
es-object-atoms: 1.1.1
sharp@0.33.5:
sharp@0.34.5:
dependencies:
color: 4.2.3
'@img/colour': 1.0.0
detect-libc: 2.1.2
semver: 7.7.3
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5
'@img/sharp-libvips-darwin-arm64': 1.0.4
'@img/sharp-libvips-darwin-x64': 1.0.4
'@img/sharp-libvips-linux-arm': 1.0.5
'@img/sharp-libvips-linux-arm64': 1.0.4
'@img/sharp-libvips-linux-s390x': 1.0.4
'@img/sharp-libvips-linux-x64': 1.0.4
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
'@img/sharp-linux-arm': 0.33.5
'@img/sharp-linux-arm64': 0.33.5
'@img/sharp-linux-s390x': 0.33.5
'@img/sharp-linux-x64': 0.33.5
'@img/sharp-linuxmusl-arm64': 0.33.5
'@img/sharp-linuxmusl-x64': 0.33.5
'@img/sharp-wasm32': 0.33.5
'@img/sharp-win32-ia32': 0.33.5
'@img/sharp-win32-x64': 0.33.5
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
'@img/sharp-libvips-darwin-arm64': 1.2.4
'@img/sharp-libvips-darwin-x64': 1.2.4
'@img/sharp-libvips-linux-arm': 1.2.4
'@img/sharp-libvips-linux-arm64': 1.2.4
'@img/sharp-libvips-linux-ppc64': 1.2.4
'@img/sharp-libvips-linux-riscv64': 1.2.4
'@img/sharp-libvips-linux-s390x': 1.2.4
'@img/sharp-libvips-linux-x64': 1.2.4
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
'@img/sharp-linux-arm': 0.34.5
'@img/sharp-linux-arm64': 0.34.5
'@img/sharp-linux-ppc64': 0.34.5
'@img/sharp-linux-riscv64': 0.34.5
'@img/sharp-linux-s390x': 0.34.5
'@img/sharp-linux-x64': 0.34.5
'@img/sharp-linuxmusl-arm64': 0.34.5
'@img/sharp-linuxmusl-x64': 0.34.5
'@img/sharp-wasm32': 0.34.5
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-ia32': 0.34.5
'@img/sharp-win32-x64': 0.34.5
optional: true
shebang-command@2.0.0:
@@ -9344,11 +9398,6 @@ snapshots:
signal-exit@4.1.0: {}
simple-swizzle@0.2.4:
dependencies:
is-arrayish: 0.3.4
optional: true
simple-wcswidth@1.1.2: {}
sirv@3.0.2:
@@ -9400,8 +9449,6 @@ snapshots:
- '@types/react'
- supports-color
streamsearch@1.1.0: {}
string.prototype.codepointat@0.2.1: {}
string.prototype.includes@2.0.1:

View File

@@ -0,0 +1,104 @@
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className
)}
{...props}
/>
)
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center",
className
)}
{...props}
/>
)
}
const emptyMediaVariants = cva(
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
},
},
defaultVariants: {
variant: "default",
},
}
)
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
)
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
{...props}
/>
)
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className
)}
{...props}
/>
)
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
}

View File

@@ -0,0 +1,193 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="list"
data-slot="item-group"
className={cn("group/item-group flex flex-col", className)}
{...props}
/>
)
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn("my-0", className)}
{...props}
/>
)
}
const itemVariants = cva(
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "p-4 gap-4 ",
sm: "py-3 px-4 gap-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Item({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
)
}
const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
{
variants: {
variant: {
default: "bg-transparent",
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image:
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
}
)
function ItemMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-content"
className={cn(
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
className
)}
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-title"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
className
)}
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="item-description"
className={cn(
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-actions"
className={cn("flex items-center gap-2", className)}
{...props}
/>
)
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-header"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-footer"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
}

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@@ -0,0 +1,5 @@
"use client";
export function AcknowledgePage() {
return null;
}

View File

@@ -0,0 +1,193 @@
"use client";
import { MonitorSmartphoneIcon, MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { useMemo, type ComponentType, type SVGProps } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { enUS, zhCN, type Locale } from "@/core/i18n";
import { useI18n } from "@/core/i18n/hooks";
import { cn } from "@/lib/utils";
import { SettingsSection } from "./settings-section";
const languageOptions: { value: Locale; label: string }[] = [
{ value: "en-US", label: enUS.locale.localName },
{ value: "zh-CN", label: zhCN.locale.localName },
];
export function AppearanceSettingsPage() {
const { t, locale, changeLocale } = useI18n();
const { theme, setTheme, resolvedTheme } = useTheme();
const currentTheme = (theme ?? "system") as "system" | "light" | "dark";
const themeOptions = useMemo(
() => [
{
id: "system",
label: t.settings.appearance.system,
description: t.settings.appearance.systemDescription,
icon: MonitorSmartphoneIcon,
},
{
id: "light",
label: t.settings.appearance.light,
description: t.settings.appearance.lightDescription,
icon: SunIcon,
},
{
id: "dark",
label: t.settings.appearance.dark,
description: t.settings.appearance.darkDescription,
icon: MoonIcon,
},
],
[
t.settings.appearance.dark,
t.settings.appearance.darkDescription,
t.settings.appearance.light,
t.settings.appearance.lightDescription,
t.settings.appearance.system,
t.settings.appearance.systemDescription,
],
);
return (
<div className="space-y-8">
<SettingsSection
title={t.settings.appearance.themeTitle}
description={t.settings.appearance.themeDescription}
>
<div className="grid gap-3 lg:grid-cols-3">
{themeOptions.map((option) => (
<ThemePreviewCard
key={option.id}
icon={option.icon}
label={option.label}
description={option.description}
active={currentTheme === option.id}
mode={option.id as "system" | "light" | "dark"}
resolvedTheme={resolvedTheme}
onSelect={(value) => setTheme(value)}
/>
))}
</div>
</SettingsSection>
<Separator />
<SettingsSection
title={t.settings.appearance.languageTitle}
description={t.settings.appearance.languageDescription}
>
<Select
value={locale}
onValueChange={(value) => changeLocale(value as Locale)}
>
<SelectTrigger className="w-[220px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{languageOptions.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingsSection>
</div>
);
}
function ThemePreviewCard({
icon: Icon,
label,
description,
active,
mode,
resolvedTheme,
onSelect,
}: {
icon: ComponentType<SVGProps<SVGSVGElement>>;
label: string;
description: string;
active: boolean;
mode: "system" | "light" | "dark";
resolvedTheme?: string;
onSelect: (mode: "system" | "light" | "dark") => void;
}) {
const previewMode =
mode === "system" ? (resolvedTheme === "dark" ? "dark" : "light") : mode;
return (
<button
type="button"
onClick={() => onSelect(mode)}
className={cn(
"group flex h-full flex-col gap-3 rounded-lg border p-4 text-left transition-all",
active
? "border-primary ring-primary/30 shadow-sm ring-2"
: "hover:border-border hover:shadow-sm",
)}
>
<div className="flex items-start gap-3">
<div className="bg-muted rounded-md p-2">
<Icon className="size-4" />
</div>
<div className="space-y-1">
<div className="text-sm leading-none font-semibold">{label}</div>
<p className="text-muted-foreground text-xs leading-snug">
{description}
</p>
</div>
</div>
<div
className={cn(
"relative overflow-hidden rounded-md border text-xs transition-colors",
previewMode === "dark"
? "border-neutral-800 bg-neutral-900 text-neutral-200"
: "border-slate-200 bg-white text-slate-900",
)}
>
<div className="border-border/50 flex items-center gap-2 border-b px-3 py-2">
<div
className={cn(
"h-2 w-2 rounded-full",
previewMode === "dark" ? "bg-emerald-400" : "bg-emerald-500",
)}
/>
<div className="h-2 w-10 rounded-full bg-current/20" />
<div className="h-2 w-6 rounded-full bg-current/15" />
</div>
<div className="grid grid-cols-[1fr_240px] gap-3 px-3 py-3">
<div className="space-y-2">
<div className="h-3 w-3/4 rounded-full bg-current/15" />
<div className="h-3 w-1/2 rounded-full bg-current/10" />
<div className="h-[90px] rounded-md border border-current/10 bg-current/5" />
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-md bg-current/10" />
<div className="space-y-2">
<div className="h-2 w-14 rounded-full bg-current/15" />
<div className="h-2 w-10 rounded-full bg-current/10" />
</div>
</div>
<div className="flex flex-col gap-1 rounded-md border border-dashed border-current/15 p-2">
<div className="h-2 w-3/5 rounded-full bg-current/15" />
<div className="h-2 w-2/5 rounded-full bg-current/10" />
</div>
</div>
</div>
</div>
</button>
);
}

View File

@@ -0,0 +1 @@
export { SettingsDialog } from "./settings-dialog";

View File

@@ -0,0 +1,100 @@
"use client";
import { PaletteIcon, SparklesIcon, WrenchIcon } from "lucide-react";
import { useMemo, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { AcknowledgePage } from "@/components/workspace/settings/acknowledge-page";
import { AppearanceSettingsPage } from "@/components/workspace/settings/appearance-settings-page";
import { SkillSettingsPage } from "@/components/workspace/settings/skill-settings-page";
import { ToolSettingsPage } from "@/components/workspace/settings/tool-settings-page";
import { useI18n } from "@/core/i18n/hooks";
import { cn } from "@/lib/utils";
type SettingsSection = "appearance" | "tools" | "skills" | "acknowledge";
type SettingsDialogProps = React.ComponentProps<typeof Dialog> & {
defaultSection?: SettingsSection;
};
export function SettingsDialog({
defaultSection = "appearance",
...dialogProps
}: SettingsDialogProps) {
const { t } = useI18n();
const [activeSection, setActiveSection] =
useState<SettingsSection>(defaultSection);
const sections = useMemo(
() => [
{
id: "appearance",
label: t.settings.sections.appearance,
icon: PaletteIcon,
},
{ id: "tools", label: t.settings.sections.tools, icon: WrenchIcon },
{ id: "skills", label: t.settings.sections.skills, icon: SparklesIcon },
],
[
t.settings.sections.appearance,
t.settings.sections.tools,
t.settings.sections.skills,
],
);
return (
<Dialog {...dialogProps}>
<DialogContent
className="flex h-[75vh] max-h-[calc(100vh-2rem)] flex-col sm:max-w-5xl md:max-w-6xl"
aria-describedby={undefined}
>
<DialogHeader className="gap-1">
<DialogTitle>{t.settings.title}</DialogTitle>
<p className="text-muted-foreground text-sm">
{t.settings.description}
</p>
</DialogHeader>
<div className="grid min-h-0 flex-1 gap-4 md:grid-cols-[220px_1fr]">
<nav className="bg-muted/30 min-h-0 overflow-y-auto rounded-lg border p-2">
<ul className="space-y-1 pr-1">
{sections.map(({ id, label, icon: Icon }) => {
const active = activeSection === id;
return (
<li key={id}>
<button
type="button"
onClick={() => setActiveSection(id as SettingsSection)}
className={cn(
"flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
active
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
<Icon className="size-4" />
<span>{label}</span>
</button>
</li>
);
})}
</ul>
</nav>
<ScrollArea className="h-full min-h-0 rounded-lg border">
<div className="space-y-8 p-6">
{activeSection === "appearance" && <AppearanceSettingsPage />}
{activeSection === "tools" && <ToolSettingsPage />}
{activeSection === "skills" && <SkillSettingsPage />}
{activeSection === "acknowledge" && <AcknowledgePage />}
</div>
</ScrollArea>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,25 @@
import { cn } from "@/lib/utils";
export function SettingsSection({
className,
title,
description,
children,
}: {
className?: string;
title: React.ReactNode;
description?: React.ReactNode;
children: React.ReactNode;
}) {
return (
<section className={cn(className)}>
<header className="space-y-2">
<h3 className="text-lg font-semibold">{title}</h3>
{description && (
<p className="text-muted-foreground text-sm">{description}</p>
)}
</header>
<main className="mt-4">{children}</main>
</section>
);
}

View File

@@ -0,0 +1,91 @@
"use client";
import { SparklesIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import {
Item,
ItemActions,
ItemTitle,
ItemContent,
ItemDescription,
} from "@/components/ui/item";
import { Switch } from "@/components/ui/switch";
import { useI18n } from "@/core/i18n/hooks";
import { useEnableSkill, useSkills } from "@/core/skills/hooks";
import type { Skill } from "@/core/skills/type";
import { SettingsSection } from "./settings-section";
export function SkillSettingsPage() {
const { t } = useI18n();
const { skills, isLoading, error } = useSkills();
return (
<SettingsSection
title={t.settings.skills.title}
description={t.settings.skills.description}
>
{isLoading ? (
<div>Loading...</div>
) : error ? (
<div>Error: {error.message}</div>
) : (
<SkillSettingsList skills={skills} />
)}
</SettingsSection>
);
}
function SkillSettingsList({ skills }: { skills: Skill[] }) {
const { mutate: enableSkill } = useEnableSkill();
if (skills.length === 0) {
return (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<SparklesIcon />
</EmptyMedia>
<EmptyTitle>No skill yet</EmptyTitle>
<EmptyDescription>
Put your skill folders under the `/skills/custom` folder under the
root folder of DeerFlow.
</EmptyDescription>
</EmptyHeader>
</Empty>
);
}
return (
<div className="flex w-full flex-col gap-4">
{skills.map((skill) => (
<Item className="w-full" variant="outline" key={skill.name}>
<ItemContent>
<ItemTitle>
<div className="flex items-center gap-2">
<div>{skill.name}</div>
<Badge variant="outline">{skill.category}</Badge>
</div>
</ItemTitle>
<ItemDescription className="line-clamp-4">
{skill.description}
</ItemDescription>
</ItemContent>
<ItemActions>
<Switch
checked={skill.enabled}
onCheckedChange={(checked) =>
enableSkill({ skillName: skill.name, enabled: checked })
}
/>
</ItemActions>
</Item>
))}
</div>
);
}

View File

@@ -0,0 +1,68 @@
"use client";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemTitle,
} from "@/components/ui/item";
import { Switch } from "@/components/ui/switch";
import { useI18n } from "@/core/i18n/hooks";
import { useMCPConfig, useEnableMCPServer } from "@/core/mcp/hooks";
import type { MCPServerConfig } from "@/core/mcp/types";
import { SettingsSection } from "./settings-section";
export function ToolSettingsPage() {
const { t } = useI18n();
const { config, isLoading, error } = useMCPConfig();
return (
<SettingsSection
title={t.settings.tools.title}
description={t.settings.tools.description}
>
{isLoading ? (
<div>Loading...</div>
) : error ? (
<div>Error: {error.message}</div>
) : (
config && <MCPServerList servers={config.mcp_servers} />
)}
</SettingsSection>
);
}
function MCPServerList({
servers,
}: {
servers: Record<string, MCPServerConfig>;
}) {
const { mutate: enableMCPServer } = useEnableMCPServer();
return (
<div className="flex w-full flex-col gap-4">
{Object.entries(servers).map(([name, config]) => (
<Item className="w-full" variant="outline" key={name}>
<ItemContent>
<ItemTitle>
<div className="flex items-center gap-2">
<div>{name}</div>
</div>
</ItemTitle>
<ItemDescription className="line-clamp-4">
{config.description}
</ItemDescription>
</ItemContent>
<ItemActions>
<Switch
checked={config.enabled}
onCheckedChange={(checked) =>
enableMCPServer({ serverName: name, enabled: checked })
}
/>
</ItemActions>
</Item>
))}
</div>
);
}

View File

@@ -1,6 +1,7 @@
"use client";
import { SettingsIcon } from "lucide-react";
import { useState } from "react";
import {
Sidebar,
@@ -14,6 +15,7 @@ import {
SidebarGroup,
SidebarGroupContent,
} from "@/components/ui/sidebar";
import { SettingsDialog } from "@/components/workspace/settings";
import { useI18n } from "@/core/i18n/hooks";
import { RecentChatList } from "./recent-chat-list";
@@ -24,32 +26,42 @@ export function WorkspaceSidebar({
...props
}: React.ComponentProps<typeof Sidebar>) {
const { t } = useI18n();
const [settingsOpen, setSettingsOpen] = useState(false);
return (
<Sidebar variant="sidebar" collapsible="icon" {...props}>
<SidebarHeader className="py-0">
<WorkspaceHeader />
</SidebarHeader>
<SidebarContent>
<WorkspaceNavMenu />
<RecentChatList />
</SidebarContent>
<SidebarFooter>
<SidebarGroup className="px-0">
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<div className="text-muted-foreground cursor-pointer">
<SettingsIcon size={16} />
<span>{t.common.settings}</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<>
<Sidebar variant="sidebar" collapsible="icon" {...props}>
<SidebarHeader className="py-0">
<WorkspaceHeader />
</SidebarHeader>
<SidebarContent>
<WorkspaceNavMenu />
<RecentChatList />
</SidebarContent>
<SidebarFooter>
<SidebarGroup className="px-0">
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<button
type="button"
className="text-muted-foreground flex w-full cursor-pointer items-center gap-2"
onClick={() => setSettingsOpen(true)}
>
<SettingsIcon size={16} />
<span>{t.common.settings}</span>
</button>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
</>
);
}

View File

@@ -1,6 +1,11 @@
import type { Translations } from "./types";
export const enUS: Translations = {
// Locale meta
locale: {
localName: "English",
},
// Common
common: {
home: "Home",
@@ -83,4 +88,41 @@ export const enUS: Translations = {
readFile: "Read file",
writeFile: "Write file",
},
// Settings
settings: {
title: "Settings",
description: "Adjust how DeerFlow looks and behaves for you.",
sections: {
appearance: "Appearance",
tools: "Tools",
skills: "Skills",
acknowledge: "Acknowledge",
},
appearance: {
themeTitle: "Theme",
themeDescription:
"Choose how the interface follows your device or stays fixed.",
system: "System",
light: "Light",
dark: "Dark",
systemDescription: "Match the operating system preference automatically.",
lightDescription: "Bright palette with higher contrast for daytime.",
darkDescription: "Dim palette that reduces glare for focus.",
languageTitle: "Language",
languageDescription: "Switch between languages.",
},
tools: {
title: "Tools",
description: "Manage the configuration and enabled status of MCP tools.",
},
skills: {
title: "Skills",
description: "Manage the configuration and enabled status of the skills.",
},
acknowledge: {
emptyTitle: "Acknowledgements",
emptyDescription: "Credits and acknowledgements will show here.",
},
},
};

View File

@@ -1,4 +1,9 @@
export interface Translations {
// Locale meta
locale: {
localName: string;
};
// Common
common: {
home: string;
@@ -80,4 +85,40 @@ export interface Translations {
readFile: string;
writeFile: string;
};
// Settings
settings: {
title: string;
description: string;
sections: {
appearance: string;
tools: string;
skills: string;
acknowledge: string;
};
appearance: {
themeTitle: string;
themeDescription: string;
system: string;
light: string;
dark: string;
systemDescription: string;
lightDescription: string;
darkDescription: string;
languageTitle: string;
languageDescription: string;
};
tools: {
title: string;
description: string;
};
skills: {
title: string;
description: string;
};
acknowledge: {
emptyTitle: string;
emptyDescription: string;
};
};
}

View File

@@ -1,6 +1,11 @@
import type { Translations } from "./types";
export const zhCN: Translations = {
// Locale meta
locale: {
localName: "中文",
},
// Common
common: {
home: "首页",
@@ -83,4 +88,40 @@ export const zhCN: Translations = {
readFile: "读取文件",
writeFile: "写入文件",
},
// Settings
settings: {
title: "设置",
description: "根据你的偏好调整 DeerFlow 的界面和行为。",
sections: {
appearance: "外观",
tools: "工具",
skills: "技能",
acknowledge: "致谢",
},
appearance: {
themeTitle: "主题",
themeDescription: "跟随系统或选择固定的界面模式。",
system: "系统",
light: "浅色",
dark: "深色",
systemDescription: "自动匹配操作系统偏好。",
lightDescription: "更明亮的配色,适合日间使用。",
darkDescription: "更暗的配色,减少眩光方便专注。",
languageTitle: "语言",
languageDescription: "在不同语言之间切换。",
},
tools: {
title: "工具",
description: "管理 MCP 工具的配置和启用状态。",
},
skills: {
title: "技能",
description: "管理智能体的技能配置和启用状态。",
},
acknowledge: {
emptyTitle: "致谢",
emptyDescription: "相关的致谢信息会展示在这里。",
},
},
};

View File

@@ -0,0 +1,24 @@
import { env } from "@/env";
import type { MCPConfig } from "./types";
export async function loadMCPConfig() {
const response = await fetch(
`${env.NEXT_PUBLIC_BACKEND_BASE_URL}/api/mcp/config`,
);
return response.json() as Promise<MCPConfig>;
}
export async function updateMCPConfig(config: MCPConfig) {
const response = await fetch(
`${env.NEXT_PUBLIC_BACKEND_BASE_URL}/api/mcp/config`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(config),
},
);
return response.json();
}

View File

@@ -0,0 +1,44 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { loadMCPConfig, updateMCPConfig } from "./api";
export function useMCPConfig() {
const { data, isLoading, error } = useQuery({
queryKey: ["mcpConfig"],
queryFn: () => loadMCPConfig(),
});
return { config: data, isLoading, error };
}
export function useEnableMCPServer() {
const queryClient = useQueryClient();
const { config } = useMCPConfig();
return useMutation({
mutationFn: async ({
serverName,
enabled,
}: {
serverName: string;
enabled: boolean;
}) => {
if (!config) {
throw new Error("MCP config not found");
}
if (!config.mcp_servers[serverName]) {
throw new Error(`MCP server ${serverName} not found`);
}
await updateMCPConfig({
mcp_servers: {
...config.mcp_servers,
[serverName]: {
...config.mcp_servers[serverName],
enabled,
},
},
});
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["mcpConfig"] });
},
});
}

View File

@@ -0,0 +1,2 @@
export * from "./api";
export * from "./types";

View File

@@ -0,0 +1,8 @@
export interface MCPServerConfig extends Record<string, unknown> {
enabled: boolean;
description: string;
}
export interface MCPConfig {
mcp_servers: Record<string, MCPServerConfig>;
}

View File

@@ -0,0 +1,25 @@
import { env } from "@/env";
import type { Skill } from "./type";
export async function loadSkills() {
const skills = await fetch(`${env.NEXT_PUBLIC_BACKEND_BASE_URL}/api/skills`);
const json = await skills.json();
return json.skills as Skill[];
}
export async function enableSkill(skillName: string, enabled: boolean) {
const response = await fetch(
`${env.NEXT_PUBLIC_BACKEND_BASE_URL}/api/skills/${skillName}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
enabled,
}),
},
);
return response.json();
}

View File

@@ -0,0 +1,31 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { enableSkill } from "./api";
import { loadSkills } from ".";
export function useSkills() {
const { data, isLoading, error } = useQuery({
queryKey: ["skills"],
queryFn: () => loadSkills(),
});
return { skills: data ?? [], isLoading, error };
}
export function useEnableSkill() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
skillName,
enabled,
}: {
skillName: string;
enabled: boolean;
}) => {
await enableSkill(skillName, enabled);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
},
});
}

View File

@@ -0,0 +1,2 @@
export * from "./api";
export * from "./type";

View File

@@ -0,0 +1,7 @@
export interface Skill {
name: string;
description: string;
category: string;
license: string;
enabled: boolean;
}

View File

@@ -22,7 +22,7 @@
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "preserve",
"jsx": "react-jsx",
"plugins": [
{
"name": "next"