Browse Source

chore: Optimize multi-theme switching

vben 9 months ago
parent
commit
6afed34437
55 changed files with 2086 additions and 534 deletions
  1. 0 1
      apps/web-antd/package.json
  2. 47 0
      apps/web-antd/src/app.vue
  3. 243 241
      apps/web-antd/src/views/dashboard/index.vue
  4. 2 2
      internal/lint-configs/eslint-config/package.json
  5. 48 8
      internal/tailwind-config/src/index.ts
  6. 1 1
      internal/vite-config/package.json
  7. 2 2
      package.json
  8. 1 0
      packages/@core/forward/preferences/package.json
  9. 7 2
      packages/@core/forward/preferences/src/config.ts
  10. 89 10
      packages/@core/forward/preferences/src/constants.ts
  11. 16 10
      packages/@core/forward/preferences/src/preferences.test.ts
  12. 89 23
      packages/@core/forward/preferences/src/preferences.ts
  13. 13 2
      packages/@core/forward/preferences/src/types.ts
  14. 1 1
      packages/@core/forward/preferences/src/use-preferences.ts
  15. 7 0
      packages/@core/shared/colorful/build.config.ts
  16. 42 0
      packages/@core/shared/colorful/package.json
  17. 45 0
      packages/@core/shared/colorful/src/generator.ts
  18. 2 0
      packages/@core/shared/colorful/src/index.ts
  19. 1 1
      packages/@core/shared/colorful/src/utils.test.ts
  20. 0 0
      packages/@core/shared/colorful/src/utils.ts
  21. 6 0
      packages/@core/shared/colorful/tsconfig.json
  22. 287 3
      packages/@core/shared/design-tokens/src/dark/index.css
  23. 281 0
      packages/@core/shared/design-tokens/src/default/index.css
  24. 0 1
      packages/@core/shared/toolkit/package.json
  25. 1 1
      packages/@core/shared/toolkit/src/index.ts
  26. 30 0
      packages/@core/shared/toolkit/src/update-css-variables.test.ts
  27. 35 0
      packages/@core/shared/toolkit/src/update-css-variables.ts
  28. 21 0
      packages/@core/shared/typings/src/app.d.ts
  29. 1 0
      packages/@core/uikit/shadcn-ui/package.json
  30. 1 1
      packages/@core/uikit/shadcn-ui/src/components/menu-badge/menu-badge.vue
  31. 1 1
      packages/@core/uikit/shadcn-ui/src/components/ui/badge/badge.ts
  32. 1 1
      packages/@core/uikit/shadcn-ui/src/components/ui/button/button.ts
  33. 3 10
      packages/business/layouts/src/authentication/authentication.vue
  34. 1 1
      packages/business/layouts/src/authentication/toolbar.vue
  35. 1 1
      packages/business/universal-ui/package.json
  36. 3 2
      packages/business/universal-ui/src/authentication/login.vue
  37. 3 3
      packages/business/universal-ui/src/authentication/register.vue
  38. 26 14
      packages/business/universal-ui/src/authentication/widgets/color-toggle.vue
  39. 1 1
      packages/business/universal-ui/src/index.ts
  40. 2 1
      packages/business/universal-ui/src/preferences/blocks/index.ts
  41. 137 0
      packages/business/universal-ui/src/preferences/blocks/theme/builtin.vue
  42. 0 90
      packages/business/universal-ui/src/preferences/blocks/theme/color.vue
  43. 38 0
      packages/business/universal-ui/src/preferences/blocks/theme/radius.vue
  44. 5 1
      packages/business/universal-ui/src/preferences/blocks/theme/theme.vue
  45. 0 1
      packages/business/universal-ui/src/preferences/icons/setting.vue
  46. 11 10
      packages/business/universal-ui/src/preferences/preferences-widget.vue
  47. 42 31
      packages/business/universal-ui/src/preferences/preferences.vue
  48. 3 1
      packages/business/universal-ui/src/preferences/trigger.vue
  49. 3 4
      packages/business/universal-ui/src/theme-toggle/theme-toggle.vue
  50. 19 1
      packages/locales/src/langs/en-US.yaml
  51. 19 1
      packages/locales/src/langs/zh-CN.yaml
  52. 0 4
      packages/styles/src/tokens/dark.scss
  53. 0 1
      packages/styles/src/tokens/light.scss
  54. 444 44
      pnpm-lock.yaml
  55. 4 0
      vben-admin.code-workspace

+ 0 - 1
apps/web-antd/package.json

@@ -38,7 +38,6 @@
     "@vben/styles": "workspace:*",
     "@vben/types": "workspace:*",
     "@vben/universal-ui": "workspace:*",
-    "@vben/chart-ui": "workspace:*",
     "@vben/utils": "workspace:*",
     "@vueuse/core": "^10.11.0",
     "ant-design-vue": "^4.2.3",

+ 47 - 0
apps/web-antd/src/app.vue

@@ -42,3 +42,50 @@ const tokenTheme = computed(() => {
     </ConfigProvider>
   </GlobalProvider>
 </template>
+
+<!-- <style>
+:root {
+  --background: 0 0% 100%;
+  --foreground: 240 10% 3.9%;
+  --card: 0 0% 100%;
+  --card-foreground: 240 10% 3.9%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 240 10% 3.9%;
+  --primary: 240 5.9% 10%;
+  --primary-foreground: 0 0% 98%;
+  --secondary: 240 4.8% 95.9%;
+  --secondary-foreground: 240 5.9% 10%;
+  --muted: 240 4.8% 95.9%;
+  --muted-foreground: 240 3.8% 46.1%;
+  --accent: 240 4.8% 95.9%;
+  --accent-foreground: 240 5.9% 10%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 0 0% 98%;
+  --border: 240 5.9% 90%;
+  --input: 240 5.9% 90%;
+  --ring: 240 5.9% 10%;
+  --radius: 0.25rem;
+}
+
+.dark {
+  --background: 240 10% 3.9%;
+  --foreground: 0 0% 98%;
+  --card: 240 10% 3.9%;
+  --card-foreground: 0 0% 98%;
+  --popover: 240 10% 3.9%;
+  --popover-foreground: 0 0% 98%;
+  --primary: 0 0% 98%;
+  --primary-foreground: 240 5.9% 10%;
+  --secondary: 240 3.7% 15.9%;
+  --secondary-foreground: 0 0% 98%;
+  --muted: 240 3.7% 15.9%;
+  --muted-foreground: 240 5% 64.9%;
+  --accent: 240 3.7% 15.9%;
+  --accent-foreground: 0 0% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 0 0% 98%;
+  --border: 240 3.7% 15.9%;
+  --input: 240 3.7% 15.9%;
+  --ring: 240 4.9% 83.9%;
+}
+</style> -->

+ 243 - 241
apps/web-antd/src/views/dashboard/index.vue

@@ -1,249 +1,251 @@
 <script lang="ts" setup>
-import { ref } from 'vue';
+// import { ref } from 'vue';
+
+// import { echartsInstance as echarts } from '@vben/chart-ui';
 
 defineOptions({ name: 'WelCome' });
-import { Dashboard } from '@vben/universal-ui';
-import { echartsInstance as echarts } from '@vben/chart-ui';
-const cardList = ref([
-  {
-    title: '访问数',
-    extra: '月',
-    leftContent: '2000',
-    rightContent: 'flat-color-icons:conference-call',
-    leftFooter: '总访问数',
-    color: 'green',
-    rightFooter: '5000',
-  },
-  {
-    title: '销售额',
-    extra: '日',
-    leftContent: '$1350',
-    rightContent: 'flat-color-icons:sales-performance',
-    leftFooter: '总销售额',
-    color: 'red',
-    rightFooter: '$550000',
-  },
-]);
-const chartTabs = ref([
-  {
-    name: '1',
-    title: '流量趋势',
-    option: {
-      color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],
 
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: {
-          type: 'cross',
-          // label: {
-          //   backgroundColor: '#6a7985',
-          // },
-        },
-      },
-      legend: {
-        data: ['Line 1', 'Line 2', 'Line 3', 'Line 4', 'Line 5'],
-      },
-      toolbox: {
-        feature: {
-          saveAsImage: {},
-        },
-      },
-      grid: {
-        left: '3%',
-        right: '4%',
-        bottom: '3%',
-        containLabel: true,
-      },
-      xAxis: [
-        {
-          type: 'category',
-          boundaryGap: false,
-          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
-        },
-      ],
-      yAxis: [
-        {
-          type: 'value',
-        },
-      ],
-      series: [
-        {
-          name: 'Line 1',
-          type: 'line',
-          stack: 'Total',
-          smooth: true,
-          lineStyle: {
-            width: 0,
-          },
-          showSymbol: false,
-          areaStyle: {
-            opacity: 0.8,
-            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-              {
-                offset: 0,
-                color: 'rgb(128, 255, 165)',
-              },
-              {
-                offset: 1,
-                color: 'rgb(1, 191, 236)',
-              },
-            ]),
-          },
-          emphasis: {
-            focus: 'series',
-          },
-          data: [140, 232, 101, 264, 90, 340, 250],
-        },
-        {
-          name: 'Line 2',
-          type: 'line',
-          stack: 'Total',
-          smooth: true,
-          lineStyle: {
-            width: 0,
-          },
-          showSymbol: false,
-          areaStyle: {
-            opacity: 0.8,
-            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-              {
-                offset: 0,
-                color: 'rgb(0, 221, 255)',
-              },
-              {
-                offset: 1,
-                color: 'rgb(77, 119, 255)',
-              },
-            ]),
-          },
-          emphasis: {
-            focus: 'series',
-          },
-          data: [120, 282, 111, 234, 220, 340, 310],
-        },
-        {
-          name: 'Line 3',
-          type: 'line',
-          stack: 'Total',
-          smooth: true,
-          lineStyle: {
-            width: 0,
-          },
-          showSymbol: false,
-          areaStyle: {
-            opacity: 0.8,
-            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-              {
-                offset: 0,
-                color: 'rgb(55, 162, 255)',
-              },
-              {
-                offset: 1,
-                color: 'rgb(116, 21, 219)',
-              },
-            ]),
-          },
-          emphasis: {
-            focus: 'series',
-          },
-          data: [320, 132, 201, 334, 190, 130, 220],
-        },
-        {
-          name: 'Line 4',
-          type: 'line',
-          stack: 'Total',
-          smooth: true,
-          lineStyle: {
-            width: 0,
-          },
-          showSymbol: false,
-          areaStyle: {
-            opacity: 0.8,
-            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-              {
-                offset: 0,
-                color: 'rgb(255, 0, 135)',
-              },
-              {
-                offset: 1,
-                color: 'rgb(135, 0, 157)',
-              },
-            ]),
-          },
-          emphasis: {
-            focus: 'series',
-          },
-          data: [220, 402, 231, 134, 190, 230, 120],
-        },
-        {
-          name: 'Line 5',
-          type: 'line',
-          stack: 'Total',
-          smooth: true,
-          lineStyle: {
-            width: 0,
-          },
-          showSymbol: false,
-          label: {
-            show: true,
-            position: 'top',
-          },
-          areaStyle: {
-            opacity: 0.8,
-            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-              {
-                offset: 0,
-                color: 'rgb(255, 191, 0)',
-              },
-              {
-                offset: 1,
-                color: 'rgb(224, 62, 76)',
-              },
-            ]),
-          },
-          emphasis: {
-            focus: 'series',
-          },
-          data: [220, 302, 181, 234, 210, 290, 150],
-        },
-      ],
-    },
-  },
-  {
-    name: '2',
-    title: '访问量',
-    option: {
-      xAxis: {
-        type: 'category',
-        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
-      },
-      yAxis: {
-        type: 'value',
-      },
-      series: [
-        {
-          data: [
-            120,
-            {
-              value: 200,
-              itemStyle: {
-                color: '#a90000',
-              },
-            },
-            150,
-            80,
-            70,
-            110,
-            130,
-          ],
-          type: 'bar',
-        },
-      ],
-    },
-  },
-]);
+// const cardList = ref([
+//   {
+//     color: 'green',
+//     extra: '月',
+//     leftContent: '2000',
+//     leftFooter: '总访问数',
+//     rightContent: 'flat-color-icons:conference-call',
+//     rightFooter: '5000',
+//     title: '访问数',
+//   },
+//   {
+//     color: 'red',
+//     extra: '日',
+//     leftContent: '$1350',
+//     leftFooter: '总销售额',
+//     rightContent: 'flat-color-icons:sales-performance',
+//     rightFooter: '$550000',
+//     title: '销售额',
+//   },
+// ]);
+// const chartTabs = ref([
+//   {
+//     name: '1',
+//     option: {
+//       color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],
+
+//       grid: {
+//         bottom: '3%',
+//         containLabel: true,
+//         left: '3%',
+//         right: '4%',
+//       },
+//       legend: {
+//         data: ['Line 1', 'Line 2', 'Line 3', 'Line 4', 'Line 5'],
+//       },
+//       series: [
+//         {
+//           areaStyle: {
+//             color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+//               {
+//                 color: 'rgb(128, 255, 165)',
+//                 offset: 0,
+//               },
+//               {
+//                 color: 'rgb(1, 191, 236)',
+//                 offset: 1,
+//               },
+//             ]),
+//             opacity: 0.8,
+//           },
+//           data: [140, 232, 101, 264, 90, 340, 250],
+//           emphasis: {
+//             focus: 'series',
+//           },
+//           lineStyle: {
+//             width: 0,
+//           },
+//           name: 'Line 1',
+//           showSymbol: false,
+//           smooth: true,
+//           stack: 'Total',
+//           type: 'line',
+//         },
+//         {
+//           areaStyle: {
+//             color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+//               {
+//                 color: 'rgb(0, 221, 255)',
+//                 offset: 0,
+//               },
+//               {
+//                 color: 'rgb(77, 119, 255)',
+//                 offset: 1,
+//               },
+//             ]),
+//             opacity: 0.8,
+//           },
+//           data: [120, 282, 111, 234, 220, 340, 310],
+//           emphasis: {
+//             focus: 'series',
+//           },
+//           lineStyle: {
+//             width: 0,
+//           },
+//           name: 'Line 2',
+//           showSymbol: false,
+//           smooth: true,
+//           stack: 'Total',
+//           type: 'line',
+//         },
+//         {
+//           areaStyle: {
+//             color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+//               {
+//                 color: 'rgb(55, 162, 255)',
+//                 offset: 0,
+//               },
+//               {
+//                 color: 'rgb(116, 21, 219)',
+//                 offset: 1,
+//               },
+//             ]),
+//             opacity: 0.8,
+//           },
+//           data: [320, 132, 201, 334, 190, 130, 220],
+//           emphasis: {
+//             focus: 'series',
+//           },
+//           lineStyle: {
+//             width: 0,
+//           },
+//           name: 'Line 3',
+//           showSymbol: false,
+//           smooth: true,
+//           stack: 'Total',
+//           type: 'line',
+//         },
+//         {
+//           areaStyle: {
+//             color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+//               {
+//                 color: 'rgb(255, 0, 135)',
+//                 offset: 0,
+//               },
+//               {
+//                 color: 'rgb(135, 0, 157)',
+//                 offset: 1,
+//               },
+//             ]),
+//             opacity: 0.8,
+//           },
+//           data: [220, 402, 231, 134, 190, 230, 120],
+//           emphasis: {
+//             focus: 'series',
+//           },
+//           lineStyle: {
+//             width: 0,
+//           },
+//           name: 'Line 4',
+//           showSymbol: false,
+//           smooth: true,
+//           stack: 'Total',
+//           type: 'line',
+//         },
+//         {
+//           areaStyle: {
+//             color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+//               {
+//                 color: 'rgb(255, 191, 0)',
+//                 offset: 0,
+//               },
+//               {
+//                 color: 'rgb(224, 62, 76)',
+//                 offset: 1,
+//               },
+//             ]),
+//             opacity: 0.8,
+//           },
+//           data: [220, 302, 181, 234, 210, 290, 150],
+//           emphasis: {
+//             focus: 'series',
+//           },
+//           label: {
+//             position: 'top',
+//             show: true,
+//           },
+//           lineStyle: {
+//             width: 0,
+//           },
+//           name: 'Line 5',
+//           showSymbol: false,
+//           smooth: true,
+//           stack: 'Total',
+//           type: 'line',
+//         },
+//       ],
+//       toolbox: {
+//         feature: {
+//           saveAsImage: {},
+//         },
+//       },
+//       tooltip: {
+//         axisPointer: {
+//           type: 'cross',
+//           // label: {
+//           //   backgroundColor: '#6a7985',
+//           // },
+//         },
+//         trigger: 'axis',
+//       },
+//       xAxis: [
+//         {
+//           boundaryGap: false,
+//           data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+//           type: 'category',
+//         },
+//       ],
+//       yAxis: [
+//         {
+//           type: 'value',
+//         },
+//       ],
+//     },
+//     title: '流量趋势',
+//   },
+//   {
+//     name: '2',
+//     option: {
+//       series: [
+//         {
+//           data: [
+//             120,
+//             {
+//               itemStyle: {
+//                 color: '#a90000',
+//               },
+//               value: 200,
+//             },
+//             150,
+//             80,
+//             70,
+//             110,
+//             130,
+//           ],
+//           type: 'bar',
+//         },
+//       ],
+//       xAxis: {
+//         data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+//         type: 'category',
+//       },
+//       yAxis: {
+//         type: 'value',
+//       },
+//     },
+//     title: '访问量',
+//   },
+// ]);
 </script>
 
 <template>
-  <Dashboard :cardList="cardList" :chartTabs="chartTabs"></Dashboard>
+  <div>dashboard</div>
+  <!-- <Dashboard :card-list="cardList" :chart-tabs="chartTabs" /> -->
 </template>

+ 2 - 2
internal/lint-configs/eslint-config/package.json

@@ -32,8 +32,8 @@
   "devDependencies": {
     "@eslint/js": "^9.5.0",
     "@types/eslint": "^8.56.10",
-    "@typescript-eslint/eslint-plugin": "^7.13.1",
-    "@typescript-eslint/parser": "^7.13.1",
+    "@typescript-eslint/eslint-plugin": "^7.14.1",
+    "@typescript-eslint/parser": "^7.14.1",
     "eslint": "^8.57.0",
     "eslint-config-prettier": "^9.1.0",
     "eslint-plugin-eslint-comments": "^3.2.0",

+ 48 - 8
internal/tailwind-config/src/index.ts

@@ -24,6 +24,31 @@ packages.forEach((pkg) => {
   }
 });
 
+function createColorsPattern(name: string) {
+  return {
+    100: `hsl(var(--${name}-100))`,
+    200: `hsl(var(--${name}-200))`,
+    300: `hsl(var(--${name}-300))`,
+    400: `hsl(var(--${name}-400))`,
+    500: `hsl(var(--${name}-500))`,
+    600: `hsl(var(--${name}-600))`,
+    700: `hsl(var(--${name}-700))`,
+    800: `hsl(var(--${name}-800))`,
+    900: `hsl(var(--${name}-900))`,
+    1000: `hsl(var(--${name}-1000))`,
+    active: `hsl(var(--${name}-700))`,
+    background: `hsl(var(--${name}-100))`,
+    'background-hover': `hsl(var(--${name}-200))`,
+    border: `hsl(var(--${name}-300))`,
+    'border-hover': `hsl(var(--${name}-400))`,
+    foreground: `hsl(var(--${name}-foreground))`,
+    hover: `hsl(var(--${name}-500))`,
+    text: `hsl(var(--${name}-900))`,
+    'text-active': `hsl(var(--${name}-1000))`,
+    'text-hover': `hsl(var(--${name}-800))`,
+  };
+}
+
 export default {
   content: [
     './index.html',
@@ -73,17 +98,23 @@ export default {
           foreground: 'hsl(var(--accent-foreground))',
           hover: 'hsl(var(--accent-hover))',
         },
+        authentication: 'hsl(var(--authentication))',
         background: 'hsl(var(--background))',
         border: 'hsl(var(--border))',
         card: {
           DEFAULT: 'hsl(var(--card))',
           foreground: 'hsl(var(--card-foreground))',
         },
+
         destructive: {
+          ...createColorsPattern('destructive'),
           DEFAULT: 'hsl(var(--destructive))',
-          foreground: 'hsl(var(--destructive-foreground))',
         },
-        foreground: 'hsl(var(--foreground) / <alpha-value>)',
+        foreground: 'hsl(var(--foreground))',
+        green: {
+          ...createColorsPattern('green'),
+          foreground: 'hsl(var(--success-foreground))',
+        },
         heavy: {
           DEFAULT: 'hsl(var(--heavy))',
           foreground: 'hsl(var(--heavy-foreground))',
@@ -102,21 +133,30 @@ export default {
           foreground: 'hsl(var(--popover-foreground))',
         },
         primary: {
-          DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
-          foreground: 'hsl(var(--primary-foreground) / <alpha-value>)',
+          ...createColorsPattern('primary'),
+          DEFAULT: 'hsl(var(--primary))',
+        },
+        red: {
+          ...createColorsPattern('red'),
+          foreground: 'hsl(var(--destructive-foreground))',
         },
         ring: 'hsl(var(--ring))',
         secondary: {
-          DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
-          desc: 'hsl(var(--secondary-desc) / <alpha-value>)',
-          foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)',
+          DEFAULT: 'hsl(var(--secondary))',
+          desc: 'hsl(var(--secondary-desc))',
+          foreground: 'hsl(var(--secondary-foreground))',
         },
+
         success: {
+          ...createColorsPattern('success'),
           DEFAULT: 'hsl(var(--success))',
-          foreground: 'hsl(var(--success-foreground))',
         },
         warning: {
+          ...createColorsPattern('warning'),
           DEFAULT: 'hsl(var(--warning))',
+        },
+        yellow: {
+          ...createColorsPattern('yellow'),
           foreground: 'hsl(var(--warning-foreground))',
         },
       },

+ 1 - 1
internal/vite-config/package.json

@@ -46,7 +46,7 @@
     "rollup": "^4.18.0",
     "rollup-plugin-visualizer": "^5.12.0",
     "sass": "^1.77.6",
-    "unplugin-turbo-console": "^1.8.6",
+    "unplugin-turbo-console": "^1.8.7",
     "vite": "^5.3.1",
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-dts": "^3.9.1",

+ 2 - 2
package.json

@@ -51,7 +51,7 @@
     "@changesets/cli": "^2.27.6",
     "@ls-lint/ls-lint": "^2.2.3",
     "@types/jsdom": "^21.1.7",
-    "@types/node": "^20.14.8",
+    "@types/node": "^20.14.9",
     "@vben/commitlint-config": "workspace:*",
     "@vben/eslint-config": "workspace:*",
     "@vben/lint-staged-config": "workspace:*",
@@ -69,7 +69,7 @@
     "jsdom": "^24.1.0",
     "rimraf": "^5.0.7",
     "taze": "^0.13.8",
-    "turbo": "^2.0.4",
+    "turbo": "^2.0.5",
     "typescript": "^5.5.2",
     "unbuild": "^2.0.0",
     "vite": "^5.3.1",

+ 1 - 0
packages/@core/forward/preferences/package.json

@@ -30,6 +30,7 @@
   },
   "dependencies": {
     "@vben-core/cache": "workspace:*",
+    "@vben-core/colorful": "workspace:*",
     "@vben-core/toolkit": "workspace:*",
     "@vben-core/typings": "workspace:*",
     "@vueuse/core": "^10.11.0",

+ 7 - 2
packages/@core/forward/preferences/src/config.ts

@@ -18,7 +18,6 @@ const defaultPreferences: Preferences = {
     name: 'Vben Admin Pro',
     semiDarkMenu: true,
     showPreference: true,
-    themeMode: 'dark',
   },
   breadcrumb: {
     enable: true,
@@ -67,7 +66,13 @@ const defaultPreferences: Preferences = {
     showIcon: true,
   },
   theme: {
-    colorPrimary: 'hsl(211 91% 39%)',
+    builtinType: 'default',
+    colorDestructive: 'hsl(348 100% 61%)',
+    colorPrimary: 'hsl(245 82% 67%)',
+    colorSuccess: 'hsl(144 57% 58%)',
+    colorWarning: 'hsl(42 84% 61%)',
+    mode: 'dark',
+    radius: '0.5',
   },
   transition: {
     enable: true,

+ 89 - 10
packages/@core/forward/preferences/src/constants.ts

@@ -1,3 +1,5 @@
+import type { BuiltinThemeType } from '@vben-core/typings';
+
 import type { SupportedLanguagesType } from './types';
 
 interface Language {
@@ -5,17 +7,17 @@ interface Language {
   text: string;
 }
 
-export const COLOR_PRIMARY_RESETS = [
-  'hsl(211 91% 39%)',
-  'hsl(212 100% 45%)',
-  'hsl(181 84% 32%)',
-  'hsl(161 90% 43%)',
-  'hsl(231 98% 65%)',
-  'hsl(245 82% 67%)',
-  'hsl(347 77% 60%)',
-];
+interface BuiltinThemePreset {
+  color: string;
+  darkPrimaryColor?: string;
+  primaryColor?: string;
+  type: BuiltinThemeType;
+}
 
-export const SUPPORT_LANGUAGES: Language[] = [
+/**
+ * Supported languages
+ */
+const SUPPORT_LANGUAGES: Language[] = [
   {
     key: 'zh-CN',
     text: '简体中文',
@@ -25,3 +27,80 @@ export const SUPPORT_LANGUAGES: Language[] = [
     text: 'English',
   },
 ];
+
+const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
+  {
+    color: 'hsl(245 82% 67%)',
+    type: 'default',
+  },
+  {
+    color: 'hsl(231 98% 65%)',
+    type: 'violet',
+  },
+  {
+    color: 'hsl(347 77% 60%)',
+    type: 'pink',
+  },
+  {
+    color: 'hsl(0 75% 42%)',
+    type: 'rose',
+  },
+  {
+    color: 'hsl(212 100% 45%)',
+    type: 'sky-blue',
+  },
+  {
+    color: 'hsl(211 91% 39%)',
+    type: 'deep-blue',
+  },
+  {
+    color: 'hsl(161 90% 43%)',
+    type: 'green',
+  },
+  {
+    color: 'hsl(181 84% 32%)',
+    type: 'deep-green',
+  },
+  {
+    color: 'hsl(18 89% 40%)',
+    type: 'orange',
+  },
+  {
+    color: 'hsl(42 84% 61%)',
+    type: 'yellow',
+  },
+  {
+    color: 'hsl(240 5% 26%)',
+    darkPrimaryColor: 'hsl(0 0 98%)',
+    primaryColor: 'hsl(240 5.9% 10%)',
+    type: 'zinc',
+  },
+  {
+    color: 'hsl(0 0% 25%)',
+    darkPrimaryColor: 'hsl(0 0 98%)',
+    primaryColor: 'hsl(240 5.9% 10%)',
+    type: 'neutral',
+  },
+  {
+    color: 'hsl(215 25% 27%)',
+    darkPrimaryColor: 'hsl(0 0 98%)',
+    primaryColor: 'hsl(240 5.9% 10%)',
+    type: 'slate',
+  },
+  {
+    color: 'hsl(217 19% 27%)',
+    darkPrimaryColor: 'hsl(0 0 98%)',
+    primaryColor: 'hsl(240 5.9% 10%)',
+    type: 'gray',
+  },
+  {
+    color: '',
+    type: 'custom',
+  },
+];
+
+export const COLOR_PRESETS = [...BUILT_IN_THEME_PRESETS].slice(0, 7);
+
+export { BUILT_IN_THEME_PRESETS, SUPPORT_LANGUAGES };
+
+export type { BuiltinThemePreset };

+ 16 - 10
packages/@core/forward/preferences/src/preferences.test.ts

@@ -55,7 +55,6 @@ describe('preferences', () => {
     const overrides: any = {
       app: {
         locale: 'en-US',
-        themeMode: 'light',
       },
     };
     await preferenceManager.initPreferences({
@@ -79,10 +78,12 @@ describe('preferences', () => {
 
   it('updates theme mode correctly', () => {
     preferenceManager.updatePreferences({
-      app: { themeMode: 'light' },
+      theme: {
+        mode: 'light',
+      },
     });
 
-    expect(preferenceManager.getPreferences().app.themeMode).toBe('light');
+    expect(preferenceManager.getPreferences().theme.mode).toBe('light');
   });
 
   it('updates color modes correctly', () => {
@@ -97,7 +98,9 @@ describe('preferences', () => {
   it('resets preferences to default', () => {
     // 先更新一些偏好设置
     preferenceManager.updatePreferences({
-      app: { themeMode: 'light' },
+      theme: {
+        mode: 'light',
+      },
     });
 
     // 然后重置偏好设置
@@ -146,10 +149,10 @@ describe('preferences', () => {
   });
   it('updates the sidebar collapse state correctly', () => {
     preferenceManager.updatePreferences({
-      sidebar: { collapse: true },
+      sidebar: { collapsed: true },
     });
 
-    expect(preferenceManager.getPreferences().sidebar.collapse).toBe(true);
+    expect(preferenceManager.getPreferences().sidebar.collapsed).toBe(true);
   });
   it('updates the navigation style type correctly', () => {
     preferenceManager.updatePreferences({
@@ -164,8 +167,11 @@ describe('preferences', () => {
   it('resets preferences to default correctly', () => {
     // 先更新一些偏好设置
     preferenceManager.updatePreferences({
-      app: { locale: 'en-US', themeMode: 'light' },
-      sidebar: { collapse: true, width: 200 },
+      app: { locale: 'en-US' },
+      sidebar: { collapsed: true, width: 200 },
+      theme: {
+        mode: 'light',
+      },
     });
 
     // 然后重置偏好设置
@@ -232,10 +238,10 @@ describe('preferences', () => {
     await preferenceManager.initPreferences(overrides);
 
     preferenceManager.updatePreferences({
-      app: { themeMode: 'light' },
+      theme: { mode: 'light' },
     });
 
-    expect(preferenceManager.getPreferences().app.themeMode).toBe('light');
+    expect(preferenceManager.getPreferences().theme.mode).toBe('light');
   });
 });
 

+ 89 - 23
packages/@core/forward/preferences/src/preferences.ts

@@ -5,16 +5,17 @@ import type { Preferences } from './types';
 import { markRaw, reactive, readonly, watch } from 'vue';
 
 import { StorageManager } from '@vben-core/cache';
-import { convertToHslCssVar, merge } from '@vben-core/toolkit';
+import { generatorColorVariables } from '@vben-core/colorful';
+import { merge, updateCSSVariables } from '@vben-core/toolkit';
 
 import {
   breakpointsTailwind,
   useBreakpoints,
-  useCssVar,
   useDebounceFn,
 } from '@vueuse/core';
 
 import { defaultPreferences } from './config';
+import { BUILT_IN_THEME_PRESETS } from './constants';
 
 const STORAGE_KEY = 'preferences';
 const STORAGE_KEY_LOCALE = `${STORAGE_KEY}-locale`;
@@ -59,7 +60,7 @@ class PreferenceManager {
   private _savePreferences(preference: Preferences) {
     this.cache?.setItem(STORAGE_KEY, preference);
     this.cache?.setItem(STORAGE_KEY_LOCALE, preference.app.locale);
-    this.cache?.setItem(STORAGE_KEY_THEME, preference.app.themeMode);
+    this.cache?.setItem(STORAGE_KEY_THEME, preference.theme.mode);
   }
 
   /**
@@ -72,11 +73,7 @@ class PreferenceManager {
     const themeUpdates = updates.theme || {};
     const appUpdates = updates.app || {};
 
-    if (themeUpdates.colorPrimary) {
-      this.updateCssVar(this.state);
-    }
-
-    if (appUpdates.themeMode) {
+    if (themeUpdates && Object.keys(themeUpdates).length > 0) {
       this.updateTheme(this.state);
     }
 
@@ -149,7 +146,7 @@ class PreferenceManager {
       .matchMedia('(prefers-color-scheme: dark)')
       .addEventListener('change', ({ matches: isDark }) => {
         this.updatePreferences({
-          app: { themeMode: isDark ? 'dark' : 'light' },
+          theme: { mode: isDark ? 'dark' : 'light' },
         });
         this.updateTheme(this.state);
       });
@@ -178,15 +175,37 @@ class PreferenceManager {
    * 更新 CSS 变量
    * @param  preference - 当前偏好设置对象,它的颜色值将被转换成 HSL 格式并设置为 CSS 变量。
    */
-  private updateCssVar(preference: Preferences) {
-    if (preference.theme) {
-      for (const [key, value] of Object.entries(preference.theme)) {
-        if (['colorPrimary'].includes(key)) {
-          const cssVarValue = useCssVar(`--primary`);
-          cssVarValue.value = convertToHslCssVar(value);
-        }
-      }
+  private updateMainColors(preference: Preferences) {
+    if (!preference.theme) {
+      return;
+    }
+    const { colorDestructive, colorPrimary, colorSuccess, colorWarning } =
+      preference.theme;
+
+    const colorVariables = generatorColorVariables([
+      { color: colorPrimary, name: 'primary' },
+      { alias: 'warning', color: colorWarning, name: 'yellow' },
+      { alias: 'success', color: colorSuccess, name: 'green' },
+      { alias: 'destructive', color: colorDestructive, name: 'red' },
+    ]);
+
+    if (colorPrimary) {
+      document.documentElement.style.setProperty(
+        '--primary',
+        colorVariables['--primary-600'],
+      );
+    }
+
+    if (colorVariables['--green-600']) {
+      colorVariables['--success'] = colorVariables['--green-600'];
     }
+    if (colorVariables['--yellow-600']) {
+      colorVariables['--warning'] = colorVariables['--yellow-600'];
+    }
+    if (colorVariables['--red-600']) {
+      colorVariables['--destructive'] = colorVariables['--red-600'];
+    }
+    updateCSSVariables(colorVariables);
   }
 
   /**
@@ -206,14 +225,61 @@ class PreferenceManager {
   private updateTheme(preferences: Preferences) {
     // 当修改到颜色变量时,更新 css 变量
     const root = document.documentElement;
-    if (root) {
-      const themeMode = preferences?.app?.themeMode;
-      if (!themeMode) {
-        return;
-      }
-      const dark = isDarkTheme(themeMode);
+    if (!root) {
+      return;
+    }
+
+    const {
+      builtinType,
+      colorDestructive,
+      colorPrimary,
+      colorSuccess,
+      colorWarning,
+      mode,
+      radius,
+    } = preferences?.theme ?? {};
+
+    if (mode) {
+      const dark = isDarkTheme(mode);
       root.classList.toggle('dark', dark);
     }
+
+    if (builtinType) {
+      const rootTheme = root.dataset.theme;
+      if (rootTheme !== builtinType) {
+        root.dataset.theme = builtinType;
+      }
+    }
+
+    const currentBuiltType = BUILT_IN_THEME_PRESETS.find(
+      (item) => item.type === builtinType,
+    );
+
+    let builtinTypeColorPrimary: string | undefined = '';
+
+    if (currentBuiltType) {
+      const isDark = isDarkTheme(this.state.theme.mode);
+
+      const color = isDark
+        ? currentBuiltType.darkPrimaryColor || currentBuiltType.primaryColor
+        : currentBuiltType.primaryColor;
+      builtinTypeColorPrimary = color || currentBuiltType.color;
+    }
+
+    if (
+      builtinTypeColorPrimary ||
+      colorPrimary ||
+      colorDestructive ||
+      colorSuccess ||
+      colorWarning
+    ) {
+      preferences.theme.colorPrimary = builtinTypeColorPrimary || colorPrimary;
+      this.updateMainColors(preferences);
+    }
+
+    if (radius) {
+      document.documentElement.style.setProperty('--radius', `${radius}rem`);
+    }
   }
 
   // public getFlatPreferences() {

+ 13 - 2
packages/@core/forward/preferences/src/types.ts

@@ -1,4 +1,5 @@
 import type {
+  BuiltinThemeType,
   ContentCompactType,
   LayoutHeaderModeType,
   LayoutType,
@@ -45,8 +46,6 @@ interface AppPreferences {
   semiDarkMenu: boolean;
   /** 是否显示偏好设置 */
   showPreference: boolean;
-  /** 当前主题 */
-  themeMode: ThemeModeType;
 }
 
 interface BreadcrumbPreferences {
@@ -132,8 +131,20 @@ interface TabbarPreferences {
 }
 
 interface ThemePreferences {
+  /** 内置主题名 */
+  builtinType: BuiltinThemeType;
+  /** 错误色 */
+  colorDestructive: string;
   /** 主题色 */
   colorPrimary: string;
+  /** 成功色 */
+  colorSuccess: string;
+  /** 警告色 */
+  colorWarning: string;
+  /** 当前主题 */
+  mode: ThemeModeType;
+  /** 圆角 */
+  radius: string;
 }
 
 interface TransitionPreferences {

+ 1 - 1
packages/@core/forward/preferences/src/use-preferences.ts

@@ -24,7 +24,7 @@ function usePreferences() {
    * @returns 如果主题为暗黑模式,返回 true,否则返回 false。
    */
   const isDark = computed(() => {
-    return isDarkTheme(appPreferences.value.themeMode);
+    return isDarkTheme(preferences.theme.mode);
   });
 
   const theme = computed(() => {

+ 7 - 0
packages/@core/shared/colorful/build.config.ts

@@ -0,0 +1,7 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+  clean: true,
+  declaration: true,
+  entries: ['src/index'],
+});

+ 42 - 0
packages/@core/shared/colorful/package.json

@@ -0,0 +1,42 @@
+{
+  "name": "@vben-core/colorful",
+  "version": "5.0.0",
+  "homepage": "https://github.com/vbenjs/vue-vben-admin",
+  "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+    "directory": "packages/@vben-core/shared/colorful"
+  },
+  "license": "MIT",
+  "type": "module",
+  "scripts": {
+    "build": "pnpm unbuild",
+    "stub": "pnpm unbuild --stub"
+  },
+  "files": [
+    "dist"
+  ],
+  "sideEffects": false,
+  "main": "./dist/index.mjs",
+  "module": "./dist/index.mjs",
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "development": "./src/index.ts",
+      "default": "./dist/index.mjs"
+    }
+  },
+  "publishConfig": {
+    "exports": {
+      ".": {
+        "types": "./dist/index.d.ts",
+        "default": "./dist/index.mjs"
+      }
+    }
+  },
+  "dependencies": {
+    "@ant-design/colors": "^7.0.2",
+    "@ctrl/tinycolor": "4.1.0"
+  }
+}

+ 45 - 0
packages/@core/shared/colorful/src/generator.ts

@@ -0,0 +1,45 @@
+import { generate } from '@ant-design/colors';
+
+import { convertToHslCssVar } from './utils';
+
+export * from '@ant-design/colors';
+
+interface Opts {
+  backgroundColor?: string;
+  theme?: 'dark' | 'default';
+}
+
+interface ColorItem {
+  alias?: string;
+  color: string;
+  name: string;
+}
+
+function generatorColorVariables(colorItems: ColorItem[], opts?: Opts) {
+  const colorVariables: Record<string, string> = {};
+
+  colorItems.forEach(({ alias, color, name }) => {
+    if (color) {
+      const colors = generate(color, opts);
+      let mainColor = colors[5];
+      colors.forEach((colorValue, colorIndex) => {
+        const hslColor = convertToHslCssVar(colorValue);
+        colorVariables[`--${name}-${colorIndex + 1}00`] = hslColor;
+        if (alias) {
+          colorVariables[`--${alias}-${colorIndex + 1}00`] = hslColor;
+        }
+
+        if (colorIndex === 5) {
+          mainColor = hslColor;
+        }
+      });
+      if (alias) {
+        colorVariables[`--${alias}`] = mainColor;
+      }
+    }
+  });
+
+  return colorVariables;
+}
+
+export { generatorColorVariables };

+ 2 - 0
packages/@core/shared/colorful/src/index.ts

@@ -0,0 +1,2 @@
+export * from './generator';
+export * from './utils';

+ 1 - 1
packages/@core/shared/toolkit/src/color.test.ts → packages/@core/shared/colorful/src/utils.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect, it } from 'vitest';
 
-import { convertToHsl, convertToHslCssVar, isValidColor } from './color';
+import { convertToHsl, convertToHslCssVar, isValidColor } from './utils';
 
 describe('color conversion functions', () => {
   it('should correctly convert color to HSL format', () => {

+ 0 - 0
packages/@core/shared/toolkit/src/color.ts → packages/@core/shared/colorful/src/utils.ts


+ 6 - 0
packages/@core/shared/colorful/tsconfig.json

@@ -0,0 +1,6 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@vben/tsconfig/library.json",
+  "include": ["src"],
+  "exclude": ["node_modules"]
+}

+ 287 - 3
packages/@core/shared/design-tokens/src/dark/index.css

@@ -1,4 +1,6 @@
-:root.dark {
+:root.dark,
+:root.dark[data-theme='custom'],
+:root.dark[data-theme='default'] {
   /* Default background color of <body />...etc */
   --background: 220deg 13.04% 8%;
   --foreground: 220 13% 91%;
@@ -16,18 +18,22 @@
   --muted-foreground: 215 20.2% 65.1%;
 
   /* 主题颜色 */
-  --primary: 211 91% 39%;
+
+  /* --primary: 245 82% 67%; */
   --primary-foreground: 0 0 98%;
 
   /* Used for destructive actions such as <Button variant="destructive"> */
-  --destructive: 0 63% 31%;
+
+  --destructive: 0 78% 68%;
   --destructive-foreground: 0 0 98%;
 
   /* Used for success actions such as <message> */
+
   --success: 144 57% 58%;
   --success-foreground: 0 0 98%;
 
   /* Used for warning actions such as <message> */
+
   --warning: 42 84% 61%;
   --warning-foreground: 0 0 98%;
 
@@ -64,5 +70,283 @@
   /* 基本圆角大小 */
   --radius: 0.5rem;
 
+  /* =============component & UI============= */
+
+  /* authentication */
+  --authentication: 240deg 11% 2%;
+
   color-scheme: dark;
 }
+
+:root.dark[data-theme='violet'] {
+  --background: 224 71.4% 4.1%;
+  --foreground: 210 20% 98%;
+  --card: 224 71.4% 4.1%;
+  --card-foreground: 210 20% 98%;
+  --popover: 224 71.4% 4.1%;
+  --popover-foreground: 210 20% 98%;
+  --primary-foreground: 210 20% 98%;
+  --secondary: 215 27.9% 16.9%;
+  --secondary-foreground: 210 20% 98%;
+  --muted: 215 27.9% 16.9%;
+  --muted-foreground: 217.9 10.6% 64.9%;
+  --accent: 215 27.9% 16.9%;
+  --accent-foreground: 210 20% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 210 20% 98%;
+  --border: 215 27.9% 16.9%;
+  --input: 215 27.9% 16.9%;
+  --ring: 263.4 70% 50.4%;
+}
+
+:root.dark[data-theme='pink'] {
+  --background: 20 14.3% 4.1%;
+  --foreground: 0 0% 95%;
+  --card: 24 9.8% 10%;
+  --card-foreground: 0 0% 95%;
+  --popover: 0 0% 9%;
+  --popover-foreground: 0 0% 95%;
+  --primary-foreground: 355.7 100% 97.3%;
+  --secondary: 240 3.7% 15.9%;
+  --secondary-foreground: 0 0% 98%;
+  --muted: 0 0% 15%;
+  --muted-foreground: 240 5% 64.9%;
+  --accent: 12 6.5% 15.1%;
+  --accent-foreground: 0 0% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 0 85.7% 97.3%;
+  --border: 240 3.7% 15.9%;
+  --input: 240 3.7% 15.9%;
+  --ring: 346.8 77.2% 49.8%;
+}
+
+:root.dark[data-theme='rose'] {
+  --background: 0 0% 3.9%;
+  --foreground: 0 0% 98%;
+  --card: 0 0% 3.9%;
+  --card-foreground: 0 0% 98%;
+  --popover: 0 0% 3.9%;
+  --popover-foreground: 0 0% 98%;
+  --primary-foreground: 0 85.7% 97.3%;
+  --secondary: 0 0% 14.9%;
+  --secondary-foreground: 0 0% 98%;
+  --muted: 0 0% 14.9%;
+  --muted-foreground: 0 0% 63.9%;
+  --accent: 0 0% 14.9%;
+  --accent-foreground: 0 0% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 0 0% 98%;
+  --border: 0 0% 14.9%;
+  --input: 0 0% 14.9%;
+  --ring: 0 72.2% 50.6%;
+}
+
+:root.dark[data-theme='sky-blue'] {
+  --background: 222.2 84% 4.9%;
+  --foreground: 210 40% 98%;
+  --card: 222.2 84% 4.9%;
+  --card-foreground: 210 40% 98%;
+  --popover: 222.2 84% 4.9%;
+  --popover-foreground: 210 40% 98%;
+  --primary-foreground: 210 20% 98%;
+  --secondary: 217.2 32.6% 17.5%;
+  --secondary-foreground: 210 40% 98%;
+  --muted: 217.2 32.6% 17.5%;
+  --muted-foreground: 215 20.2% 65.1%;
+  --accent: 217.2 32.6% 17.5%;
+  --accent-foreground: 210 40% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 210 40% 98%;
+  --border: 217.2 32.6% 17.5%;
+  --input: 217.2 32.6% 17.5%;
+  --ring: 224.3 76.3% 48%;
+}
+
+:root.dark[data-theme='deep-blue'] {
+  --background: 222.2 84% 4.9%;
+  --foreground: 210 40% 98%;
+  --card: 222.2 84% 4.9%;
+  --card-foreground: 210 40% 98%;
+  --popover: 222.2 84% 4.9%;
+  --popover-foreground: 210 40% 98%;
+  --primary-foreground: 210 20% 98%;
+  --secondary: 217.2 32.6% 17.5%;
+  --secondary-foreground: 210 40% 98%;
+  --muted: 217.2 32.6% 17.5%;
+  --muted-foreground: 215 20.2% 65.1%;
+  --accent: 217.2 32.6% 17.5%;
+  --accent-foreground: 210 40% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 210 40% 98%;
+  --border: 217.2 32.6% 17.5%;
+  --input: 217.2 32.6% 17.5%;
+  --ring: 224.3 76.3% 48%;
+}
+
+:root.dark[data-theme='green'] {
+  --background: 20 14.3% 4.1%;
+  --foreground: 0 0% 95%;
+  --card: 24 9.8% 10%;
+  --card-foreground: 0 0% 95%;
+  --popover: 0 0% 9%;
+  --popover-foreground: 0 0% 95%;
+  --primary-foreground: 210 20% 98%;
+  --secondary: 240 3.7% 15.9%;
+  --secondary-foreground: 0 0% 98%;
+  --muted: 0 0% 15%;
+  --muted-foreground: 240 5% 64.9%;
+  --accent: 12 6.5% 15.1%;
+  --accent-foreground: 0 0% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 0 85.7% 97.3%;
+  --border: 240 3.7% 15.9%;
+  --input: 240 3.7% 15.9%;
+  --ring: 142.4 71.8% 29.2%;
+}
+
+:root.dark[data-theme='deep-green'] {
+  --background: 20 14.3% 4.1%;
+  --foreground: 0 0% 95%;
+  --card: 24 9.8% 10%;
+  --card-foreground: 0 0% 95%;
+  --popover: 0 0% 9%;
+  --popover-foreground: 0 0% 95%;
+  --primary-foreground: 210 20% 98%;
+  --secondary: 240 3.7% 15.9%;
+  --secondary-foreground: 0 0% 98%;
+  --muted: 0 0% 15%;
+  --muted-foreground: 240 5% 64.9%;
+  --accent: 12 6.5% 15.1%;
+  --accent-foreground: 0 0% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 0 85.7% 97.3%;
+  --border: 240 3.7% 15.9%;
+  --input: 240 3.7% 15.9%;
+  --ring: 142.4 71.8% 29.2%;
+}
+
+:root.dark[data-theme='orange'] {
+  --background: 20 14.3% 4.1%;
+  --foreground: 60 9.1% 97.8%;
+  --card: 20 14.3% 4.1%;
+  --card-foreground: 60 9.1% 97.8%;
+  --popover: 20 14.3% 4.1%;
+  --popover-foreground: 60 9.1% 97.8%;
+  --primary-foreground: 60 9.1% 97.8%;
+  --secondary: 12 6.5% 15.1%;
+  --secondary-foreground: 60 9.1% 97.8%;
+  --muted: 12 6.5% 15.1%;
+  --muted-foreground: 24 5.4% 63.9%;
+  --accent: 12 6.5% 15.1%;
+  --accent-foreground: 60 9.1% 97.8%;
+  --destructive: 0 72.2% 50.6%;
+  --destructive-foreground: 60 9.1% 97.8%;
+  --border: 12 6.5% 15.1%;
+  --input: 12 6.5% 15.1%;
+  --ring: 20.5 90.2% 48.2%;
+}
+
+:root.dark[data-theme='yellow'] {
+  --background: 20 14.3% 4.1%;
+  --foreground: 60 9.1% 97.8%;
+  --card: 20 14.3% 4.1%;
+  --card-foreground: 60 9.1% 97.8%;
+  --popover: 20 14.3% 4.1%;
+  --popover-foreground: 60 9.1% 97.8%;
+  --primary-foreground: 26 83.3% 14.1%;
+  --secondary: 12 6.5% 15.1%;
+  --secondary-foreground: 60 9.1% 97.8%;
+  --muted: 12 6.5% 15.1%;
+  --muted-foreground: 24 5.4% 63.9%;
+  --accent: 12 6.5% 15.1%;
+  --accent-foreground: 60 9.1% 97.8%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 60 9.1% 97.8%;
+  --border: 12 6.5% 15.1%;
+  --input: 12 6.5% 15.1%;
+  --ring: 35.5 91.7% 32.9%;
+}
+
+:root.dark[data-theme='zinc'] {
+  --background: 240 10% 3.9%;
+  --foreground: 0 0% 98%;
+  --card: 240 10% 3.9%;
+  --card-foreground: 0 0% 98%;
+  --popover: 240 10% 3.9%;
+  --popover-foreground: 0 0% 98%;
+  --primary-foreground: 240 5.9% 10%;
+  --secondary: 240 3.7% 15.9%;
+  --secondary-foreground: 0 0% 98%;
+  --muted: 240 3.7% 15.9%;
+  --muted-foreground: 240 5% 64.9%;
+  --accent: 240 3.7% 15.9%;
+  --accent-foreground: 0 0% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 0 0% 98%;
+  --border: 240 3.7% 15.9%;
+  --input: 240 3.7% 15.9%;
+  --ring: 240 4.9% 83.9%;
+}
+
+:root.dark[data-theme='neutral'] {
+  --background: 0 0% 3.9%;
+  --foreground: 0 0% 98%;
+  --card: 0 0% 3.9%;
+  --card-foreground: 0 0% 98%;
+  --popover: 0 0% 3.9%;
+  --popover-foreground: 0 0% 98%;
+  --primary-foreground: 0 0% 9%;
+  --secondary: 0 0% 14.9%;
+  --secondary-foreground: 0 0% 98%;
+  --muted: 0 0% 14.9%;
+  --muted-foreground: 0 0% 63.9%;
+  --accent: 0 0% 14.9%;
+  --accent-foreground: 0 0% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 0 0% 98%;
+  --border: 0 0% 14.9%;
+  --input: 0 0% 14.9%;
+  --ring: 0 0% 83.1%;
+}
+
+:root.dark[data-theme='slate'] {
+  --background: 222.2 84% 4.9%;
+  --foreground: 210 40% 98%;
+  --card: 222.2 84% 4.9%;
+  --card-foreground: 210 40% 98%;
+  --popover: 222.2 84% 4.9%;
+  --popover-foreground: 210 40% 98%;
+  --primary-foreground: 222.2 47.4% 11.2%;
+  --secondary: 217.2 32.6% 17.5%;
+  --secondary-foreground: 210 40% 98%;
+  --muted: 217.2 32.6% 17.5%;
+  --muted-foreground: 215 20.2% 65.1%;
+  --accent: 217.2 32.6% 17.5%;
+  --accent-foreground: 210 40% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 210 40% 98%;
+  --border: 217.2 32.6% 17.5%;
+  --input: 217.2 32.6% 17.5%;
+  --ring: 212.7 26.8% 83.9;
+}
+
+:root.dark[data-theme='gray'] {
+  --background: 224 71.4% 4.1%;
+  --foreground: 210 20% 98%;
+  --card: 224 71.4% 4.1%;
+  --card-foreground: 210 20% 98%;
+  --popover: 224 71.4% 4.1%;
+  --popover-foreground: 210 20% 98%;
+  --primary-foreground: 220.9 39.3% 11%;
+  --secondary: 215 27.9% 16.9%;
+  --secondary-foreground: 210 20% 98%;
+  --muted: 215 27.9% 16.9%;
+  --muted-foreground: 217.9 10.6% 64.9%;
+  --accent: 215 27.9% 16.9%;
+  --accent-foreground: 210 20% 98%;
+  --destructive: 0 62.8% 30.6%;
+  --destructive-foreground: 210 20% 98%;
+  --border: 215 27.9% 16.9%;
+  --input: 215 27.9% 16.9%;
+  --ring: 216 12.2% 83.9%;
+}

+ 281 - 0
packages/@core/shared/design-tokens/src/default/index.css

@@ -17,22 +17,27 @@
   --muted-foreground: 215.4 16.3% 46.9%;
 
   /* 主题颜色 */
+
   --primary: 211 91% 39%;
   --primary-foreground: 0 0 98%;
 
   /* Used for destructive actions such as <Button variant="destructive"> */
+
   --destructive: 0 78% 68%;
   --destructive-foreground: 0 0 98%;
 
   /* Used for success actions such as <message> */
+
   --success: 144 57% 58%;
   --success-foreground: 0 0 98%;
 
   /* Used for warning actions such as <message> */
+
   --warning: 42 84% 61%;
   --warning-foreground: 0 0 98%;
 
   /* Secondary colors for <Button /> */
+
   --secondary: 240 5% 96%;
   --secondary-foreground: 240 6% 10%;
 
@@ -73,6 +78,9 @@
 
   /* =============component & UI============= */
 
+  /* authentication */
+  --authentication: 231deg 61% 44%;
+
   /* menu */
   --menu: 0deg 0% 100%;
   --menu-darken: 0deg 0% 95%;
@@ -84,3 +92,276 @@
   accent-color: var(--primary);
   color-scheme: light;
 }
+
+:root[data-theme='violet'] {
+  --background: 0 0% 100%;
+  --foreground: 224 71.4% 4.1%;
+  --card: 0 0% 100%;
+  --card-foreground: 224 71.4% 4.1%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 224 71.4% 4.1%;
+  --primary-foreground: 210 20% 98%;
+  --secondary: 220 14.3% 95.9%;
+  --secondary-foreground: 220.9 39.3% 11%;
+  --muted: 220 14.3% 95.9%;
+  --muted-foreground: 220 8.9% 46.1%;
+  --accent: 220 14.3% 95.9%;
+  --accent-foreground: 220.9 39.3% 11%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 210 20% 98%;
+  --border: 220 13% 91%;
+  --input: 220 13% 91%;
+  --ring: 262.1 83.3% 57.8%;
+}
+
+:root[data-theme='pink'] {
+  --background: 0 0% 100%;
+  --foreground: 240 10% 3.9%;
+  --card: 0 0% 100%;
+  --card-foreground: 240 10% 3.9%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 240 10% 3.9%;
+  --primary-foreground: 355.7 100% 97.3%;
+  --secondary: 240 4.8% 95.9%;
+  --secondary-foreground: 240 5.9% 10%;
+  --muted: 240 4.8% 95.9%;
+  --muted-foreground: 240 3.8% 46.1%;
+  --accent: 240 4.8% 95.9%;
+  --accent-foreground: 240 5.9% 10%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 0 0% 98%;
+  --border: 240 5.9% 90%;
+  --input: 240 5.9% 90%;
+  --ring: 346.8 77.2% 49.8%;
+}
+
+:root[data-theme='rose'] {
+  --background: 0 0% 100%;
+  --foreground: 240 10% 3.9%;
+  --card: 0 0% 100%;
+  --card-foreground: 240 10% 3.9%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 240 10% 3.9%;
+  --primary-foreground: 355.7 100% 97.3%;
+  --secondary: 240 4.8% 95.9%;
+  --secondary-foreground: 240 5.9% 10%;
+  --muted: 240 4.8% 95.9%;
+  --muted-foreground: 240 3.8% 46.1%;
+  --accent: 240 4.8% 95.9%;
+  --accent-foreground: 240 5.9% 10%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 0 0% 98%;
+  --border: 240 5.9% 90%;
+  --input: 240 5.9% 90%;
+  --ring: 346.8 77.2% 49.8%;
+}
+
+:root[data-theme='sky-blue'] {
+  --background: 0 0% 100%;
+  --foreground: 222.2 84% 4.9%;
+  --card: 0 0% 100%;
+  --card-foreground: 222.2 84% 4.9%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 222.2 84% 4.9%;
+  --primary-foreground: 210 40% 98%;
+  --secondary: 210 40% 96.1%;
+  --secondary-foreground: 222.2 47.4% 11.2%;
+  --muted: 210 40% 96.1%;
+  --muted-foreground: 215.4 16.3% 46.9%;
+  --accent: 210 40% 96.1%;
+  --accent-foreground: 222.2 47.4% 11.2%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 210 40% 98%;
+  --border: 214.3 31.8% 91.4%;
+  --input: 214.3 31.8% 91.4%;
+  --ring: 221.2 83.2% 53.3%;
+}
+
+:root[data-theme='deep-blue'] {
+  --background: 0 0% 100%;
+  --foreground: 222.2 84% 4.9%;
+  --card: 0 0% 100%;
+  --card-foreground: 222.2 84% 4.9%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 222.2 84% 4.9%;
+  --primary-foreground: 210 40% 98%;
+  --secondary: 210 40% 96.1%;
+  --secondary-foreground: 222.2 47.4% 11.2%;
+  --muted: 210 40% 96.1%;
+  --muted-foreground: 215.4 16.3% 46.9%;
+  --accent: 210 40% 96.1%;
+  --accent-foreground: 222.2 47.4% 11.2%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 210 40% 98%;
+  --border: 214.3 31.8% 91.4%;
+  --input: 214.3 31.8% 91.4%;
+  --ring: 221.2 83.2% 53.3%;
+}
+
+:root[data-theme='green'] {
+  --background: 0 0% 100%;
+  --foreground: 240 10% 3.9%;
+  --card: 0 0% 100%;
+  --card-foreground: 240 10% 3.9%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 240 10% 3.9%;
+  --primary-foreground: 355.7 100% 97.3%;
+  --secondary: 240 4.8% 95.9%;
+  --secondary-foreground: 240 5.9% 10%;
+  --muted: 240 4.8% 95.9%;
+  --muted-foreground: 240 3.8% 46.1%;
+  --accent: 240 4.8% 95.9%;
+  --accent-foreground: 240 5.9% 10%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 0 0% 98%;
+  --border: 240 5.9% 90%;
+  --input: 240 5.9% 90%;
+  --ring: 142.1 76.2% 36.3%;
+}
+
+:root[data-theme='deep-green'] {
+  --background: 0 0% 100%;
+  --foreground: 240 10% 3.9%;
+  --card: 0 0% 100%;
+  --card-foreground: 240 10% 3.9%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 240 10% 3.9%;
+  --primary-foreground: 355.7 100% 97.3%;
+  --secondary: 240 4.8% 95.9%;
+  --secondary-foreground: 240 5.9% 10%;
+  --muted: 240 4.8% 95.9%;
+  --muted-foreground: 240 3.8% 46.1%;
+  --accent: 240 4.8% 95.9%;
+  --accent-foreground: 240 5.9% 10%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 0 0% 98%;
+  --border: 240 5.9% 90%;
+  --input: 240 5.9% 90%;
+  --ring: 142.1 76.2% 36.3%;
+}
+
+:root[data-theme='orange'] {
+  --background: 0 0% 100%;
+  --foreground: 20 14.3% 4.1%;
+  --card: 0 0% 100%;
+  --card-foreground: 20 14.3% 4.1%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 20 14.3% 4.1%;
+  --primary-foreground: 60 9.1% 97.8%;
+  --secondary: 60 4.8% 95.9%;
+  --secondary-foreground: 24 9.8% 10%;
+  --muted: 60 4.8% 95.9%;
+  --muted-foreground: 25 5.3% 44.7%;
+  --accent: 60 4.8% 95.9%;
+  --accent-foreground: 24 9.8% 10%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 60 9.1% 97.8%;
+  --border: 20 5.9% 90%;
+  --input: 20 5.9% 90%;
+  --ring: 24.6 95% 53.1%;
+}
+
+:root[data-theme='yellow'] {
+  --background: 0 0% 100%;
+  --foreground: 20 14.3% 4.1%;
+  --card: 0 0% 100%;
+  --card-foreground: 20 14.3% 4.1%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 20 14.3% 4.1%;
+  --primary-foreground: 26 83.3% 14.1%;
+  --secondary: 60 4.8% 95.9%;
+  --secondary-foreground: 24 9.8% 10%;
+  --muted: 60 4.8% 95.9%;
+  --muted-foreground: 25 5.3% 44.7%;
+  --accent: 60 4.8% 95.9%;
+  --accent-foreground: 24 9.8% 10%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 60 9.1% 97.8%;
+  --border: 20 5.9% 90%;
+  --input: 20 5.9% 90%;
+  --ring: 20 14.3% 4.1%;
+}
+
+:root[data-theme='zinc'] {
+  --background: 0 0% 100%;
+  --foreground: 240 10% 3.9%;
+  --card: 0 0% 100%;
+  --card-foreground: 240 10% 3.9%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 240 10% 3.9%;
+  --primary-foreground: 0 0% 98%;
+  --secondary: 240 4.8% 95.9%;
+  --secondary-foreground: 240 5.9% 10%;
+  --muted: 240 4.8% 95.9%;
+  --muted-foreground: 240 3.8% 46.1%;
+  --accent: 240 4.8% 95.9%;
+  --accent-foreground: 240 5.9% 10%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 0 0% 98%;
+  --border: 240 5.9% 90%;
+  --input: 240 5.9% 90%;
+  --ring: 240 5.9% 10%;
+}
+
+:root[data-theme='neutral'] {
+  --background: 0 0% 100%;
+  --foreground: 0 0% 3.9%;
+  --card: 0 0% 100%;
+  --card-foreground: 0 0% 3.9%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 0 0% 3.9%;
+  --primary-foreground: 0 0% 98%;
+  --secondary: 0 0% 96.1%;
+  --secondary-foreground: 0 0% 9%;
+  --muted: 0 0% 96.1%;
+  --muted-foreground: 0 0% 45.1%;
+  --accent: 0 0% 96.1%;
+  --accent-foreground: 0 0% 9%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 0 0% 98%;
+  --border: 0 0% 89.8%;
+  --input: 0 0% 89.8%;
+  --ring: 0 0% 3.9%;
+}
+
+:root[data-theme='slate'] {
+  --background: 0 0% 100%;
+  --foreground: 222.2 84% 4.9%;
+  --card: 0 0% 100%;
+  --card-foreground: 222.2 84% 4.9%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 222.2 84% 4.9%;
+  --primary-foreground: 210 40% 98%;
+  --secondary: 210 40% 96.1%;
+  --secondary-foreground: 222.2 47.4% 11.2%;
+  --muted: 210 40% 96.1%;
+  --muted-foreground: 215.4 16.3% 46.9%;
+  --accent: 210 40% 96.1%;
+  --accent-foreground: 222.2 47.4% 11.2%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 210 40% 98%;
+  --border: 214.3 31.8% 91.4%;
+  --input: 214.3 31.8% 91.4%;
+  --ring: 222.2 84% 4.9%;
+}
+
+:root[data-theme='gray'] {
+  --background: 0 0% 100%;
+  --foreground: 224 71.4% 4.1%;
+  --card: 0 0% 100%;
+  --card-foreground: 224 71.4% 4.1%;
+  --popover: 0 0% 100%;
+  --popover-foreground: 224 71.4% 4.1%;
+  --primary-foreground: 210 20% 98%;
+  --secondary: 220 14.3% 95.9%;
+  --secondary-foreground: 220.9 39.3% 11%;
+  --muted: 220 14.3% 95.9%;
+  --muted-foreground: 220 8.9% 46.1%;
+  --accent: 220 14.3% 95.9%;
+  --accent-foreground: 220.9 39.3% 11%;
+  --destructive: 0 84.2% 60.2%;
+  --destructive-foreground: 210 20% 98%;
+  --border: 220 13% 91%;
+  --input: 220 13% 91%;
+  --ring: 224 71.4% 4.1%;
+}

+ 0 - 1
packages/@core/shared/toolkit/package.json

@@ -36,7 +36,6 @@
     }
   },
   "dependencies": {
-    "@ctrl/tinycolor": "4.1.0",
     "@vue/shared": "^3.4.30",
     "clsx": "2.1.1",
     "dayjs": "^1.11.11",

+ 1 - 1
packages/@core/shared/toolkit/src/index.ts

@@ -1,5 +1,4 @@
 export * from './cn';
-export * from './color';
 export * from './diff';
 export * from './hash';
 export * from './inference';
@@ -8,4 +7,5 @@ export * from './merge';
 export * from './namespace';
 export * from './nprogress';
 export * from './tree';
+export * from './update-css-variables';
 export * from './window';

+ 30 - 0
packages/@core/shared/toolkit/src/update-css-variables.test.ts

@@ -0,0 +1,30 @@
+import { expect, it } from 'vitest';
+
+import { updateCSSVariables } from './update-css-variables';
+
+it('updateCSSVariables should update CSS variables in :root selector', () => {
+  // 模拟初始的内联样式表内容
+  const initialStyleContent = ':root { --primaryColor: red; }';
+  document.head.innerHTML = `<style id="custom-styles">${initialStyleContent}</style>`;
+
+  // 要更新的CSS变量和它们的新值
+  const updatedVariables = {
+    fontSize: '16px',
+    primaryColor: 'blue',
+    secondaryColor: 'green',
+  };
+
+  // 调用函数来更新CSS变量
+  updateCSSVariables(updatedVariables, 'custom-styles');
+
+  // 获取更新后的样式内容
+  const styleElement = document.querySelector('#custom-styles');
+  const updatedStyleContent = styleElement ? styleElement.textContent : '';
+
+  // 检查更新后的样式内容是否包含正确的更新值
+  expect(
+    updatedStyleContent?.includes('primaryColor: blue;') &&
+      updatedStyleContent?.includes('secondaryColor: green;') &&
+      updatedStyleContent?.includes('fontSize: 16px;'),
+  ).toBe(true);
+});

+ 35 - 0
packages/@core/shared/toolkit/src/update-css-variables.ts

@@ -0,0 +1,35 @@
+/**
+ * 更新 CSS 变量的函数
+ * @param variables 要更新的 CSS 变量与其新值的映射
+ */
+function updateCSSVariables(
+  variables: { [key: string]: string },
+  id = '__vben-styles__',
+): void {
+  // 获取或创建内联样式表元素
+  const styleElement =
+    document.querySelector(`#${id}`) || document.createElement('style');
+
+  styleElement.id = id;
+
+  // 构建要更新的 CSS 变量的样式文本
+  let cssText = ':root {';
+  for (const key in variables) {
+    if (Object.prototype.hasOwnProperty.call(variables, key)) {
+      cssText += `${key}: ${variables[key]};`;
+    }
+  }
+  cssText += '}';
+
+  // 将样式文本赋值给内联样式表
+  styleElement.textContent = cssText;
+
+  // 将内联样式表添加到文档头部
+  if (!document.querySelector(`#${id}`)) {
+    setTimeout(() => {
+      document.head.append(styleElement);
+    });
+  }
+}
+
+export { updateCSSVariables };

+ 21 - 0
packages/@core/shared/typings/src/app.d.ts

@@ -9,11 +9,32 @@ type LayoutType =
 
 type ThemeModeType = 'auto' | 'dark' | 'light';
 
+type BuiltinThemeType =
+  | 'custom'
+  | 'deep-blue'
+  | 'deep-green'
+  | 'default'
+  | 'default'
+  | 'gray'
+  | 'green'
+  | 'neutral'
+  | 'orange'
+  | 'pink'
+  | 'red'
+  | 'rose'
+  | 'sky-blue'
+  | 'slate'
+  | 'stone'
+  | 'violet'
+  | 'yellow'
+  | 'zinc';
+
 type ContentCompactType = 'compact' | 'wide';
 
 type LayoutHeaderModeType = 'auto' | 'auto-scroll' | 'fixed' | 'static';
 
 export type {
+  BuiltinThemeType,
   ContentCompactType,
   LayoutHeaderModeType,
   LayoutType,

+ 1 - 0
packages/@core/uikit/shadcn-ui/package.json

@@ -43,6 +43,7 @@
   },
   "dependencies": {
     "@radix-icons/vue": "^1.0.0",
+    "@vben-core/colorful": "workspace:*",
     "@vben-core/iconify": "workspace:*",
     "@vben-core/toolkit": "workspace:*",
     "@vben-core/typings": "workspace:*",

+ 1 - 1
packages/@core/uikit/shadcn-ui/src/components/menu-badge/menu-badge.vue

@@ -3,7 +3,7 @@ import type { MenuRecordBadgeRaw } from '@vben-core/typings';
 
 import { computed } from 'vue';
 
-import { isValidColor } from '@vben-core/toolkit';
+import { isValidColor } from '@vben-core/colorful';
 
 import BadgeDot from './menu-badge-dot.vue';
 

+ 1 - 1
packages/@core/uikit/shadcn-ui/src/components/ui/badge/badge.ts

@@ -11,7 +11,7 @@ export const badgeVariants = cva(
         default:
           'border-transparent bg-accent hover:bg-accent text-primary-foreground shadow',
         destructive:
-          'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
+          'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive-hover',
         outline: 'text-foreground',
         secondary:
           'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',

+ 1 - 1
packages/@core/uikit/shadcn-ui/src/components/ui/button/button.ts

@@ -19,7 +19,7 @@ export const buttonVariants = cva(
         default:
           'bg-primary text-primary-foreground shadow hover:bg-primary/90',
         destructive:
-          'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
+          'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive-hover',
         ghost: 'hover:bg-accent hover:text-accent-foreground',
         heavy: 'hover:bg-heavy hover:text-heavy-foreground',
         icon: 'hover:bg-accent hover:text-accent-foreground text-foreground/80',

+ 3 - 10
packages/business/layouts/src/authentication/authentication.vue

@@ -17,9 +17,7 @@ const appName = computed(() => preferences.app.name);
 </script>
 
 <template>
-  <div
-    class="bg-background flex min-h-full flex-1 select-none overflow-x-hidden"
-  >
+  <div class="flex min-h-full flex-1 select-none overflow-x-hidden">
     <AuthenticationFromView
       v-if="authPanelLeft"
       class="-enter-x min-h-full w-2/5"
@@ -47,9 +45,7 @@ const appName = computed(() => preferences.app.name);
       </div>
     </div>
     <div v-if="!authPanelCenter" class="relative hidden w-0 flex-1 lg:block">
-      <div
-        class="absolute inset-0 h-full w-full bg-[var(--color-authentication)]"
-      >
+      <div class="bg-authentication absolute inset-0 h-full w-full">
         <div class="flex-col-center -enter-x mr-20 h-full">
           <SloganIcon :alt="appName" class="animate-float h-64 w-2/5" />
           <div class="text-1xl mt-6 font-sans text-white lg:text-2xl">
@@ -61,10 +57,7 @@ const appName = computed(() => preferences.app.name);
         </div>
       </div>
     </div>
-    <div
-      v-if="authPanelCenter"
-      class="flex-center w-full dark:bg-[var(--color-authentication)]"
-    >
+    <div v-if="authPanelCenter" class="flex-center bg-authentication w-full">
       <AuthenticationFromView
         class="enter-y md:bg-background w-full rounded-3xl pb-20 shadow-2xl md:w-2/3 lg:w-1/2 xl:w-2/5"
       >

+ 1 - 1
packages/business/layouts/src/authentication/toolbar.vue

@@ -12,7 +12,7 @@ defineOptions({
 </script>
 <template>
   <div
-    class="flex-center bg-background absolute right-2 top-4 rounded-3xl px-3 py-1 dark:bg-[var(--color-authentication)]"
+    class="flex-center bg-accent absolute right-2 top-4 rounded-3xl px-3 py-1"
   >
     <div class="hidden md:flex">
       <AuthenticationColorToggle />

+ 1 - 1
packages/business/universal-ui/package.json

@@ -40,6 +40,7 @@
     "@vben-core/design": "workspace:*"
   },
   "dependencies": {
+    "@vben-core/colorful": "workspace:*",
     "@vben-core/design": "workspace:*",
     "@vben-core/iconify": "workspace:*",
     "@vben-core/preferences": "workspace:*",
@@ -49,7 +50,6 @@
     "@vben/locales": "workspace:*",
     "@vueuse/core": "^10.11.0",
     "@vueuse/integrations": "^10.11.0",
-    "@vben/chart-ui": "workspace:*",
     "qrcode": "^1.5.3",
     "vue": "^3.4.30",
     "vue-router": "^4.4.0"

+ 3 - 2
packages/business/universal-ui/src/authentication/login.vue

@@ -189,11 +189,12 @@ function handleGo(path: string) {
 
       <span
         v-if="showForgetPassword"
-        class="text-primary hover:text-primary/80 cursor-pointer text-sm font-normal"
+        class="text-primary hover:text-primary-hover active:text-primary-active cursor-pointer text-sm font-normal"
         @click="handleGo(forgetPasswordPath)"
       >
         {{ $t('authentication.forget-password') }}
       </span>
+
       <!-- <VbenButton variant="ghost" @click="handleGo('/auth/forget-password')">
         忘记密码?
       </VbenButton> -->
@@ -235,7 +236,7 @@ function handleGo(path: string) {
     <div v-if="showRegister" class="text-center text-sm">
       {{ $t('authentication.account-tip') }}
       <span
-        class="text-primary hover:text-primary/80 cursor-pointer text-sm font-normal"
+        class="text-primary hover:text-primary-hover active:text-primary-active cursor-pointer text-sm font-normal"
         @click="handleGo(registerPath)"
       >
         {{ $t('authentication.create-account') }}

+ 3 - 3
packages/business/universal-ui/src/authentication/register.vue

@@ -132,11 +132,11 @@ function goLogin() {
           name="agreePolicy"
         >
           {{ $t('authentication.sign-up-agree') }}
-          <span class="text-primary hover:text-primary/80">{{
+          <span class="text-primary hover:text-primary-hover">{{
             $t('authentication.sign-up-privacy-policy')
           }}</span>
           &
-          <span class="text-primary hover:text-primary/80">
+          <span class="text-primary hover:text-primary-hover">
             {{ $t('authentication.sign-up-terms') }}
           </span>
         </VbenCheckbox>
@@ -158,7 +158,7 @@ function goLogin() {
     <div class="mt-4 text-center text-sm">
       {{ $t('authentication.already-account') }}
       <span
-        class="text-primary hover:text-primary/80 cursor-pointer text-sm font-normal"
+        class="text-primary hover:text-primary-hover cursor-pointer text-sm font-normal"
         @click="goLogin()"
       >
         {{ $t('authentication.go-login') }}

+ 26 - 14
packages/business/universal-ui/src/authentication/widgets/color-toggle.vue

@@ -1,7 +1,9 @@
 <script setup lang="ts">
+import type { BuiltinThemeType } from '@vben/types';
+
 import { IcRoundColorLens } from '@vben-core/iconify';
 import {
-  COLOR_PRIMARY_RESETS,
+  COLOR_PRESETS,
   preferences,
   updatePreferences,
 } from '@vben-core/preferences';
@@ -11,10 +13,10 @@ defineOptions({
   name: 'AuthenticationColorToggle',
 });
 
-function handleUpdate(value: string) {
+function handleUpdate(value: BuiltinThemeType) {
   updatePreferences({
     theme: {
-      colorPrimary: value,
+      builtinType: value,
     },
   });
 }
@@ -23,22 +25,32 @@ function handleUpdate(value: string) {
 <template>
   <div class="group relative flex items-center overflow-hidden">
     <div
-      class="ease-ou flex w-0 overflow-hidden transition-all duration-500 group-hover:w-48"
+      class="ease-ou flex w-0 overflow-hidden transition-all duration-500 group-hover:w-60"
     >
-      <template v-for="color in COLOR_PRIMARY_RESETS" :key="color">
+      <template v-for="preset in COLOR_PRESETS" :key="preset.color">
         <VbenIconButton
           class="flex-center flex-shrink-0"
-          @click="handleUpdate(color)"
+          @click="handleUpdate(preset.type)"
         >
           <div
-            :class="[
-              preferences.theme.colorPrimary === color
-                ? `before:opacity-100`
-                : '',
-            ]"
-            :style="{ backgroundColor: color }"
-            class="relative h-3.5 w-3.5 rounded-[2px] before:absolute before:left-0.5 before:top-0.5 before:h-2.5 before:w-2.5 before:rounded-[2px] before:border before:border-gray-900 before:opacity-0 before:transition-all before:duration-150 before:content-[''] hover:scale-110"
-          ></div>
+            :style="{ backgroundColor: preset.color }"
+            class="flex-center relative size-5 rounded-full hover:scale-110"
+          >
+            <svg
+              v-if="preferences.theme.builtinType === preset.type"
+              class="h-3.5 w-3.5 text-white"
+              height="1em"
+              viewBox="0 0 15 15"
+              width="1em"
+            >
+              <path
+                clip-rule="evenodd"
+                d="M11.467 3.727c.289.189.37.576.181.865l-4.25 6.5a.625.625 0 0 1-.944.12l-2.75-2.5a.625.625 0 0 1 .841-.925l2.208 2.007l3.849-5.886a.625.625 0 0 1 .865-.181"
+                fill="currentColor"
+                fill-rule="evenodd"
+              />
+            </svg>
+          </div>
         </VbenIconButton>
       </template>
     </div>

+ 1 - 1
packages/business/universal-ui/src/index.ts

@@ -1,5 +1,6 @@
 export * from './authentication';
 export * from './coze-assistant';
+export * from './dashboard';
 export * from './fallback';
 export * from './global-provider';
 export * from './global-search';
@@ -8,4 +9,3 @@ export * from './notification';
 export * from './preferences';
 export * from './theme-toggle';
 export * from './user-dropdown';
-export * from './dashboard';

+ 2 - 1
packages/business/universal-ui/src/preferences/blocks/index.ts

@@ -11,6 +11,7 @@ export { default as Sidebar } from './layout/sidebar.vue';
 export { default as Tabbar } from './layout/tabbar.vue';
 export { default as GlobalShortcutKeys } from './shortcut-keys/global.vue';
 export { default as SwitchItem } from './switch-item.vue';
-export { default as ThemeColor } from './theme/color.vue';
+export { default as BuiltinTheme } from './theme/builtin.vue';
 export { default as ColorMode } from './theme/color-mode.vue';
+export { default as Radius } from './theme/radius.vue';
 export { default as Theme } from './theme/theme.vue';

+ 137 - 0
packages/business/universal-ui/src/preferences/blocks/theme/builtin.vue

@@ -0,0 +1,137 @@
+<script setup lang="ts">
+import type { BuiltinThemeType } from '@vben/types';
+
+import { computed, ref } from 'vue';
+
+import { $t } from '@vben/locales';
+import { TinyColor, convertToHsl } from '@vben-core/colorful';
+import { MdiEditBoxOutline } from '@vben-core/iconify';
+import {
+  BUILT_IN_THEME_PRESETS,
+  type BuiltinThemePreset,
+} from '@vben-core/preferences';
+
+defineOptions({
+  name: 'PreferenceBuiltinTheme',
+});
+
+const props = defineProps<{ isDark: boolean }>();
+
+const colorInput = ref();
+const modelValue = defineModel<BuiltinThemeType>({ default: 'default' });
+const themeColorPrimary = defineModel<string>('themeColorPrimary');
+
+const inputValue = computed(() => {
+  return new TinyColor(themeColorPrimary.value).toHexString();
+});
+
+function typeView(name: BuiltinThemeType) {
+  switch (name) {
+    case 'default': {
+      return $t('preferences.theme.default');
+    }
+    case 'violet': {
+      return $t('preferences.theme.violet');
+    }
+    case 'pink': {
+      return $t('preferences.theme.pink');
+    }
+    case 'rose': {
+      return $t('preferences.theme.rose');
+    }
+    case 'sky-blue': {
+      return $t('preferences.theme.sky-blue');
+    }
+    case 'deep-blue': {
+      return $t('preferences.theme.deep-blue');
+    }
+
+    case 'green': {
+      return $t('preferences.theme.green');
+    }
+    case 'deep-green': {
+      return $t('preferences.theme.deep-green');
+    }
+    case 'orange': {
+      return $t('preferences.theme.orange');
+    }
+    case 'yellow': {
+      return $t('preferences.theme.yellow');
+    }
+    case 'zinc': {
+      return $t('preferences.theme.zinc');
+    }
+    case 'neutral': {
+      return $t('preferences.theme.neutral');
+    }
+    case 'slate': {
+      return $t('preferences.theme.slate');
+    }
+    case 'gray': {
+      return $t('preferences.theme.gray');
+    }
+    case 'custom': {
+      return $t('preferences.theme.custom');
+    }
+  }
+}
+
+function handleSelect(theme: BuiltinThemePreset) {
+  modelValue.value = theme.type;
+  const primaryColor = props.isDark
+    ? theme.darkPrimaryColor || theme.primaryColor
+    : theme.primaryColor;
+
+  themeColorPrimary.value = primaryColor || theme.color;
+}
+
+function handleInputChange(e: Event) {
+  const target = e.target as HTMLInputElement;
+  themeColorPrimary.value = convertToHsl(target.value);
+}
+
+function selectColor() {
+  colorInput.value?.[0]?.click?.();
+}
+</script>
+
+<template>
+  <div class="flex w-full flex-wrap justify-between">
+    <template v-for="theme in BUILT_IN_THEME_PRESETS" :key="theme.type">
+      <div class="flex cursor-pointer flex-col" @click="handleSelect(theme)">
+        <div
+          :class="{
+            'outline-box-active': theme.type === modelValue,
+          }"
+          class="outline-box flex-center group cursor-pointer"
+        >
+          <template v-if="theme.type !== 'custom'">
+            <div
+              :style="{ backgroundColor: theme.color }"
+              class="mx-10 my-2 size-5 rounded-md"
+            ></div>
+          </template>
+          <template v-else>
+            <div class="size-full px-10 py-2" @click.stop="selectColor">
+              <div class="flex-center relative size-5 rounded-sm">
+                <MdiEditBoxOutline
+                  class="absolute z-10 size-5 opacity-60 group-hover:opacity-100"
+                />
+                <input
+                  ref="colorInput"
+                  :value="inputValue"
+                  class="absolute inset-0 opacity-0"
+                  type="color"
+                  @input="handleInputChange"
+                />
+              </div>
+            </div>
+          </template>
+        </div>
+        <div class="text-muted-foreground my-2 text-center text-xs">
+          {{ typeView(theme.type) }}
+        </div>
+      </div>
+    </template>
+  </div>
+</template>

+ 0 - 90
packages/business/universal-ui/src/preferences/blocks/theme/color.vue

@@ -1,90 +0,0 @@
-<script setup lang="ts">
-import type { CSSProperties } from 'vue';
-import { computed, ref, watch, watchEffect } from 'vue';
-
-import { MdiEditBoxOutline } from '@vben-core/iconify';
-import { TinyColor, convertToHsl } from '@vben-core/toolkit';
-
-defineOptions({
-  name: 'PreferenceColor',
-});
-
-const props = withDefaults(defineProps<{ colorPrimaryPresets: string[] }>(), {
-  colorPrimaryPresets: () => [],
-});
-
-const colorInput = ref();
-const currentColor = ref(props.colorPrimaryPresets?.[0]);
-
-const modelValue = defineModel<string>();
-
-const activeColor = computed((): CSSProperties => {
-  return {
-    outlineColor: currentColor.value,
-    outlineWidth: '2px',
-  };
-});
-
-function isActive(color: string): string[] {
-  return color === currentColor.value ? ['outline-box-active'] : [];
-}
-
-const inputStyle = computed((): CSSProperties => {
-  return props.colorPrimaryPresets.includes(currentColor.value)
-    ? {}
-    : activeColor.value;
-});
-
-const inputValue = computed(() => {
-  return new TinyColor(modelValue.value).toHexString();
-});
-
-function selectColor() {
-  colorInput.value.click();
-}
-
-function handleInputChange(e: Event) {
-  const target = e.target as HTMLInputElement;
-  modelValue.value = convertToHsl(target.value);
-}
-
-// 监听颜色变化,转成系统可识别的 hsl 格式
-watch(currentColor, (val) => {
-  modelValue.value = convertToHsl(val);
-});
-
-watchEffect(() => {
-  if (modelValue.value) {
-    currentColor.value = modelValue.value;
-  }
-});
-</script>
-
-<template>
-  <div class="flex w-full flex-wrap justify-between">
-    <template v-for="color in colorPrimaryPresets" :key="color">
-      <div
-        :class="isActive(color)"
-        class="outline-box p-2"
-        @click="currentColor = color"
-      >
-        <div
-          :style="{ backgroundColor: color }"
-          class="h-5 w-5 rounded-md"
-        ></div>
-      </div>
-    </template>
-    <div :style="inputStyle" class="outline-box p-2" @click="selectColor">
-      <div class="flex-center bg-accent relative h-5 w-5 rounded-md">
-        <MdiEditBoxOutline class="absolute z-10" />
-        <input
-          ref="colorInput"
-          :value="inputValue"
-          class="absolute inset-0 opacity-0"
-          type="color"
-          @input="handleInputChange"
-        />
-      </div>
-    </div>
-  </div>
-</template>

+ 38 - 0
packages/business/universal-ui/src/preferences/blocks/theme/radius.vue

@@ -0,0 +1,38 @@
+<script setup lang="ts">
+import { ToggleGroup, ToggleGroupItem } from '@vben-core/shadcn-ui';
+
+defineOptions({
+  name: 'PreferenceColorMode',
+});
+
+const modelValue = defineModel<string | undefined>('themeRadius', {
+  default: '0.5',
+});
+
+const items = [
+  { label: '0', value: '0' },
+  { label: '0.25', value: '0.25' },
+  { label: '0.5', value: '0.5' },
+  { label: '0.75', value: '0.75' },
+  { label: '1', value: '1' },
+];
+</script>
+
+<template>
+  <ToggleGroup
+    v-model="modelValue"
+    class="gap-2"
+    size="sm"
+    type="single"
+    variant="outline"
+  >
+    <template v-for="item in items" :key="item.value">
+      <ToggleGroupItem
+        :value="item.value"
+        class="data-[state=on]:bg-primary data-[state=on]:text-primary-foreground h-7 w-16 rounded-sm"
+      >
+        {{ item.label }}
+      </ToggleGroupItem>
+    </template>
+  </ToggleGroup>
+</template>

+ 5 - 1
packages/business/universal-ui/src/preferences/blocks/theme/theme.vue

@@ -1,4 +1,8 @@
 <script setup lang="ts">
+import type { ThemeModeType } from '@vben-core/preferences';
+
+import type { Component } from 'vue';
+
 import { $t } from '@vben/locales';
 import {
   IcRoundMotionPhotosAuto,
@@ -17,7 +21,7 @@ const appSemiDarkMenu = defineModel<boolean>('appSemiDarkMenu', {
   default: true,
 });
 
-const THEME_PRESET = [
+const THEME_PRESET: Array<{ icon: Component; name: ThemeModeType }> = [
   {
     icon: IcRoundWbSunny,
     name: 'light',

+ 0 - 1
packages/business/universal-ui/src/preferences/icons/setting.vue

@@ -7,7 +7,6 @@
   >
     <path
       d="M19.9 12.66a1 1 0 0 1 0-1.32l1.28-1.44a1 1 0 0 0 .12-1.17l-2-3.46a1 1 0 0 0-1.07-.48l-1.88.38a1 1 0 0 1-1.15-.66l-.61-1.83a1 1 0 0 0-.95-.68h-4a1 1 0 0 0-1 .68l-.56 1.83a1 1 0 0 1-1.15.66L5 4.79a1 1 0 0 0-1 .48L2 8.73a1 1 0 0 0 .1 1.17l1.27 1.44a1 1 0 0 1 0 1.32L2.1 14.1a1 1 0 0 0-.1 1.17l2 3.46a1 1 0 0 0 1.07.48l1.88-.38a1 1 0 0 1 1.15.66l.61 1.83a1 1 0 0 0 1 .68h4a1 1 0 0 0 .95-.68l.61-1.83a1 1 0 0 1 1.15-.66l1.88.38a1 1 0 0 0 1.07-.48l2-3.46a1 1 0 0 0-.12-1.17ZM18.41 14l.8.9l-1.28 2.22l-1.18-.24a3 3 0 0 0-3.45 2L12.92 20h-2.56L10 18.86a3 3 0 0 0-3.45-2l-1.18.24l-1.3-2.21l.8-.9a3 3 0 0 0 0-4l-.8-.9l1.28-2.2l1.18.24a3 3 0 0 0 3.45-2L10.36 4h2.56l.38 1.14a3 3 0 0 0 3.45 2l1.18-.24l1.28 2.22l-.8.9a3 3 0 0 0 0 3.98m-6.77-6a4 4 0 1 0 4 4a4 4 0 0 0-4-4m0 6a2 2 0 1 1 2-2a2 2 0 0 1-2 2"
-      fill="white"
     />
   </svg>
 </template>

+ 11 - 10
packages/business/universal-ui/src/preferences/preferences-widget.vue

@@ -1,10 +1,6 @@
 <script lang="ts" setup>
 import { loadLocaleMessages } from '@vben/locales';
-import {
-  COLOR_PRIMARY_RESETS,
-  preferences,
-  updatePreferences,
-} from '@vben-core/preferences';
+import { preferences, updatePreferences } from '@vben-core/preferences';
 
 import Preferences from './preferences.vue';
 </script>
@@ -18,13 +14,11 @@ import Preferences from './preferences.vue';
     :app-layout="preferences.app.layout"
     :app-locale="preferences.app.locale"
     :app-semi-dark-menu="preferences.app.semiDarkMenu"
-    :app-theme-mode="preferences.app.themeMode"
     :breadcrumb-enable="preferences.breadcrumb.enable"
     :breadcrumb-hide-only-one="preferences.breadcrumb.hideOnlyOne"
     :breadcrumb-home="preferences.breadcrumb.showHome"
     :breadcrumb-icon="preferences.breadcrumb.showIcon"
     :breadcrumb-style-type="preferences.breadcrumb.styleType"
-    :color-primary-presets="COLOR_PRIMARY_RESETS"
     :footer-enable="preferences.footer.enable"
     :footer-fixed="preferences.footer.fixed"
     :header-enable="preferences.header.enable"
@@ -43,7 +37,10 @@ import Preferences from './preferences.vue';
     :sidebar-enable="preferences.sidebar.enable"
     :tabbar-enable="preferences.tabbar.enable"
     :tabbar-show-icon="preferences.tabbar.showIcon"
+    :theme-builtin-type="preferences.theme.builtinType"
     :theme-color-primary="preferences.theme.colorPrimary"
+    :theme-mode="preferences.theme.mode"
+    :theme-radius="preferences.theme.radius"
     :transition-enable="preferences.transition.enable"
     :transition-name="preferences.transition.name"
     :transition-progress="preferences.transition.progress"
@@ -72,9 +69,6 @@ import Preferences from './preferences.vue';
     @update:app-semi-dark-menu="
       (val) => updatePreferences({ app: { semiDarkMenu: val } })
     "
-    @update:app-theme-mode="
-      (val) => updatePreferences({ app: { themeMode: val } })
-    "
     @update:breadcrumb-enable="
       (val) => updatePreferences({ breadcrumb: { enable: val } })
     "
@@ -136,9 +130,16 @@ import Preferences from './preferences.vue';
     @update:tabbar-show-icon="
       (val) => updatePreferences({ tabbar: { showIcon: val } })
     "
+    @update:theme-builtin-type="
+      (val) => updatePreferences({ theme: { builtinType: val } })
+    "
     @update:theme-color-primary="
       (val) => updatePreferences({ theme: { colorPrimary: val } })
     "
+    @update:theme-mode="(val) => updatePreferences({ theme: { mode: val } })"
+    @update:theme-radius="
+      (val) => updatePreferences({ theme: { radius: val } })
+    "
     @update:transition-enable="
       (val) => updatePreferences({ transition: { enable: val } })
     "

+ 42 - 31
packages/business/universal-ui/src/preferences/preferences.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import type {
+  BuiltinThemeType,
   ContentCompactType,
   LayoutHeaderModeType,
   LayoutType,
@@ -35,6 +36,7 @@ import {
   Animation,
   Block,
   Breadcrumb,
+  BuiltinTheme,
   ColorMode,
   Content,
   Footer,
@@ -43,19 +45,14 @@ import {
   Header,
   Layout,
   Navigation,
+  Radius,
   Sidebar,
   Tabbar,
   Theme,
-  ThemeColor,
 } from './blocks';
 import Trigger from './trigger.vue';
 import { useOpenPreferences } from './use-open-preferences';
 
-withDefaults(defineProps<{ colorPrimaryPresets: string[] }>(), {
-  colorPrimaryPresets: () => [],
-});
-
-const appThemeMode = defineModel<ThemeModeType>('appThemeMode');
 const appLocale = defineModel<SupportedLanguagesType>('appLocale');
 const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
 const appAiAssistant = defineModel<boolean>('appAiAssistant');
@@ -70,6 +67,9 @@ const transitionName = defineModel<string>('transitionName');
 const transitionEnable = defineModel<boolean>('transitionEnable');
 
 const themeColorPrimary = defineModel<string>('themeColorPrimary');
+const themeBuiltinType = defineModel<BuiltinThemeType>('themeBuiltinType');
+const themeMode = defineModel<ThemeModeType>('themeMode');
+const themeRadius = defineModel<string>('themeRadius');
 
 const sidebarEnable = defineModel<boolean>('sidebarEnable');
 const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
@@ -115,6 +115,7 @@ const shortcutKeysGlobalPreferences = defineModel<boolean>(
 
 const {
   diffPreference,
+  isDark,
   isFullContent,
   isHeaderNav,
   isMixedNav,
@@ -124,14 +125,10 @@ const {
 } = usePreferences();
 const { copy } = useClipboard();
 
-const activeTab = ref('general');
+const activeTab = ref('appearance');
 
 const tabs = computed((): SegmentedItem[] => {
   return [
-    {
-      label: $t('preferences.general'),
-      value: 'general',
-    },
     {
       label: $t('preferences.appearance'),
       value: 'appearance',
@@ -140,11 +137,14 @@ const tabs = computed((): SegmentedItem[] => {
       label: $t('preferences.layout'),
       value: 'layout',
     },
-
     {
       label: $t('preferences.shortcut-keys.title'),
       value: 'shortcutKey',
     },
+    {
+      label: $t('preferences.general'),
+      value: 'general',
+    },
   ];
 });
 
@@ -200,18 +200,45 @@ function handleReset() {
 
       <div class="p-4 pt-4">
         <VbenSegmented v-model="activeTab" :tabs="tabs">
+          <template #general>
+            <Block :title="$t('preferences.general')">
+              <General
+                v-model:app-ai-assistant="appAiAssistant"
+                v-model:app-dynamic-title="appDynamicTitle"
+                v-model:app-locale="appLocale"
+              />
+            </Block>
+
+            <Block :title="$t('preferences.animation')">
+              <Animation
+                v-model:transition-enable="transitionEnable"
+                v-model:transition-name="transitionName"
+                v-model:transition-progress="transitionProgress"
+              />
+            </Block>
+          </template>
           <template #appearance>
-            <Block :title="$t('preferences.theme')">
+            <Block :title="$t('preferences.theme.name')">
               <Theme
-                v-model="appThemeMode"
+                v-model="themeMode"
                 v-model:app-semi-dark-menu="appSemiDarkMenu"
               />
             </Block>
-            <Block :title="$t('preferences.theme-color')">
+            <!-- <Block :title="$t('preferences.theme-color')">
               <ThemeColor
                 v-model="themeColorPrimary"
                 :color-primary-presets="colorPrimaryPresets"
               />
+            </Block> -->
+            <Block :title="$t('preferences.theme.builtin')">
+              <BuiltinTheme
+                v-model="themeBuiltinType"
+                v-model:theme-color-primary="themeColorPrimary"
+                :is-dark="isDark"
+              />
+            </Block>
+            <Block :title="$t('preferences.theme.radius')">
+              <Radius v-model="themeRadius" />
             </Block>
             <Block :title="$t('preferences.other')">
               <ColorMode
@@ -281,23 +308,7 @@ function handleReset() {
               />
             </Block>
           </template>
-          <template #general>
-            <Block :title="$t('preferences.general')">
-              <General
-                v-model:app-ai-assistant="appAiAssistant"
-                v-model:app-dynamic-title="appDynamicTitle"
-                v-model:app-locale="appLocale"
-              />
-            </Block>
 
-            <Block :title="$t('preferences.animation')">
-              <Animation
-                v-model:transition-enable="transitionEnable"
-                v-model:transition-name="transitionName"
-                v-model:transition-progress="transitionProgress"
-              />
-            </Block>
-          </template>
           <template #shortcutKey>
             <Block :title="$t('preferences.shortcut-keys.global')">
               <GlobalShortcutKeys

+ 3 - 1
packages/business/universal-ui/src/preferences/trigger.vue

@@ -14,6 +14,8 @@ defineOptions({
     :title="$t('preferences.name')"
     class="bg-primary flex-col-center h-12 w-12 cursor-pointer rounded-l-lg rounded-r-none border-none"
   >
-    <IconSetting class="duration-3000 animate-spin text-2xl" />
+    <IconSetting
+      class="duration-3000 fill-primary-foreground animate-spin text-2xl"
+    />
   </VbenButton>
 </template>

+ 3 - 4
packages/business/universal-ui/src/theme-toggle/theme-toggle.vue

@@ -29,7 +29,7 @@ withDefaults(defineProps<{ shouldOnHover?: boolean }>(), {
 
 function handleChange(isDark: boolean) {
   updatePreferences({
-    app: { themeMode: isDark ? 'dark' : 'light' },
+    theme: { mode: isDark ? 'dark' : 'light' },
   });
 }
 
@@ -64,13 +64,12 @@ const PRESETS = [
         />
       </template>
       <ToggleGroup
-        :model-value="preferences.app.themeMode"
+        :model-value="preferences.theme.mode"
         class="gap-2"
         type="single"
         variant="outline"
         @update:model-value="
-          (val) =>
-            updatePreferences({ app: { themeMode: val as ThemeModeType } })
+          (val) => updatePreferences({ theme: { mode: val as ThemeModeType } })
         "
       >
         <ToggleGroupItem

+ 19 - 1
packages/locales/src/langs/en-US.yaml

@@ -121,7 +121,6 @@ preferences:
   name: Preferences
   subtitle: Customize Preferences & Preview in Real Time
   reset-tip: The data has changed, click to reset
-  theme: Theme
   ai-assistant: Ai Assistant
   appearance: Appearance
   theme-color: Theme Color
@@ -181,6 +180,25 @@ preferences:
   tabs-icon: Display Tabbar Icon
   mode: Mode
   logo-visible: Display Logo
+  theme:
+    name: Theme
+    builtin: Built-in
+    radius: Radius
+    default: Default
+    violet: Violet
+    pink: Pink
+    rose: Rose
+    sky-blue: Sky Blue
+    deep-blue: Deep Blue
+    green: Green
+    deep-green: Deep Green
+    orange: Orange
+    yellow: Yellow
+    zinc: Zinc
+    neutral: Neutral
+    slate: Slate
+    gray: Gray
+    custom: Custom
   header:
     name: Header
     visible: Display Header

+ 19 - 1
packages/locales/src/langs/zh-CN.yaml

@@ -120,7 +120,6 @@ preferences:
   name: 偏好设置
   subtitle: 自定义偏好设置 & 实时预览
   reset-tip: 数据有变化,点击可进行重置
-  theme: 主题
   appearance: 外观
   theme-color: 主题色
   layout: 布局
@@ -180,6 +179,25 @@ preferences:
   tabs-icon: 显示标签栏图标
   mode: 模式
   logo-visible: 显示 Logo
+  theme:
+    name: 主题
+    builtin: 内置主题
+    radius: 圆角
+    default: 默认
+    violet: 紫罗兰
+    pink: 樱花粉
+    rose: 玫瑰红
+    sky-blue: 天蓝色
+    deep-blue: 深蓝色
+    green: 浅绿色
+    deep-green: 深绿色
+    orange: 橙黄色
+    yellow: 柠檬黄
+    zinc: 锌色灰
+    neutral: 中性色
+    slate: 石板灰
+    gray: 中灰色
+    custom: 自定义
   header:
     name: 顶栏
     mode-static: 静止

+ 0 - 4
packages/styles/src/tokens/dark.scss

@@ -1,4 +0,0 @@
-:root.dark {
-  /* authentication */
-  --color-authentication: hsl(240deg 11% 2%);
-}

+ 0 - 1
packages/styles/src/tokens/light.scss

@@ -5,5 +5,4 @@
     'Segoe UI Symbol';
 
   /* authentication */
-  --color-authentication: hsl(231deg 61% 44%);
 }

File diff suppressed because it is too large
+ 444 - 44
pnpm-lock.yaml


+ 4 - 0
vben-admin.code-workspace

@@ -60,6 +60,10 @@
       "name": "@vben-core/cache",
       "path": "packages/@core/shared/cache",
     },
+    {
+      "name": "@vben-core/colorful",
+      "path": "packages/@core/shared/colorful",
+    },
     {
       "name": "@vben-core/design",
       "path": "packages/@core/shared/design",

Some files were not shown because too many files changed in this diff