Преглед на файлове

feat: migrate to Tailwind CSS v4 (#7614)

* chore: update deps

* feat: use jsonc/x language

* chore: update eslint 10.0

* fix: no-useless-assignment

* feat: add CLAUDE.md

* chore: ignore

* feat: claude

* fix: lint

* chore: suppot eslint v10

* fix: lint

* fix: lint

* fix: type check

* fix: unit test

* fix: Suggested fix

* fix: unit test

* chore: update stylelint v17

* chore: update all major deps

* fix:  echarts console warn

* chore: update vitest v4

* feat: add skills ignores

* chore: update deps

* chore: update deps

* fix: cspell

* chore: update deps

* chore: update tailwindcss v4

* chore: remove postcss config

* fix: no use catalog

* chore: tailwind v4 config

* fix: tailwindcss v4 sort

* feat: use eslint-plugin-better-tailwindcss

* fix: Interference between enforce-consistent-line-wrapping, jsx-curly-brace-presence and Prettier

* fix: Interference between enforce-consistent-line-wrapping, jsx-curly-brace-presence and Prettier

* fix(lint): resolve prettier and better-tailwindcss formatting conflicts

* fix(tailwind): update theme references and lint sources

* style(format): normalize apps docs and playground vue files

* style(format): normalize core ui-kit components

* style(format): normalize effects ui and layout components
xingyu преди 3 месеца
родител
ревизия
a4736a49f8
променени са 100 файла, в които са добавени 956 реда и са изтрити 731 реда
  1. 7 0
      .gitignore
  2. 0 1
      .npmrc
  3. 5 0
      .prettierignore
  4. 4 0
      .stylelintignore
  5. 7 3
      .vscode/settings.json
  6. 148 0
      CLAUDE.md
  7. 1 1
      apps/backend-mock/api/table/list.ts
  8. 0 1
      apps/web-antd/postcss.config.mjs
  9. 1 2
      apps/web-antd/src/store/auth.ts
  10. 2 2
      apps/web-antd/src/views/dashboard/analytics/index.vue
  11. 0 1
      apps/web-antd/tailwind.config.mjs
  12. 0 1
      apps/web-antdv-next/postcss.config.mjs
  13. 1 2
      apps/web-antdv-next/src/store/auth.ts
  14. 2 2
      apps/web-antdv-next/src/views/dashboard/analytics/index.vue
  15. 0 1
      apps/web-antdv-next/tailwind.config.mjs
  16. 0 1
      apps/web-ele/postcss.config.mjs
  17. 1 2
      apps/web-ele/src/store/auth.ts
  18. 2 2
      apps/web-ele/src/views/dashboard/analytics/index.vue
  19. 1 3
      apps/web-ele/src/views/demos/element/index.vue
  20. 0 1
      apps/web-ele/tailwind.config.mjs
  21. 0 1
      apps/web-naive/postcss.config.mjs
  22. 1 2
      apps/web-naive/src/store/auth.ts
  23. 2 2
      apps/web-naive/src/views/dashboard/analytics/index.vue
  24. 0 1
      apps/web-naive/tailwind.config.mjs
  25. 0 1
      apps/web-tdesign/postcss.config.mjs
  26. 1 2
      apps/web-tdesign/src/store/auth.ts
  27. 2 2
      apps/web-tdesign/src/views/dashboard/analytics/index.vue
  28. 3 3
      apps/web-tdesign/src/views/demos/tdesign/index.vue
  29. 0 1
      apps/web-tdesign/tailwind.config.mjs
  30. 3 0
      cspell.json
  31. 1 1
      docs/.vitepress/components/demo-preview.vue
  32. 4 4
      docs/.vitepress/components/preview-group.vue
  33. 3 5
      docs/.vitepress/config/shared.mts
  34. 1 1
      docs/.vitepress/theme/components/vben-contributors.vue
  35. 1 0
      docs/package.json
  36. 0 11
      docs/tailwind.config.mjs
  37. 3 4
      internal/lint-configs/eslint-config/package.json
  38. 1 2
      internal/lint-configs/eslint-config/src/configs/comments.ts
  39. 6 0
      internal/lint-configs/eslint-config/src/configs/ignores.ts
  40. 1 0
      internal/lint-configs/eslint-config/src/configs/index.ts
  41. 2 7
      internal/lint-configs/eslint-config/src/configs/jsonc.ts
  42. 0 1
      internal/lint-configs/eslint-config/src/configs/node.ts
  43. 36 19
      internal/lint-configs/eslint-config/src/configs/perfectionist.ts
  44. 2 5
      internal/lint-configs/eslint-config/src/configs/pnpm.ts
  45. 49 0
      internal/lint-configs/eslint-config/src/configs/tailwindcss.ts
  46. 1 1
      internal/lint-configs/eslint-config/src/configs/test.ts
  47. 1 1
      internal/lint-configs/eslint-config/src/configs/yaml.ts
  48. 1 5
      internal/lint-configs/eslint-config/src/custom-config.ts
  49. 2 0
      internal/lint-configs/eslint-config/src/index.ts
  50. 0 1
      internal/lint-configs/prettier-config/index.mjs
  51. 1 2
      internal/lint-configs/prettier-config/package.json
  52. 13 1
      internal/lint-configs/stylelint-config/index.mjs
  53. 0 10
      internal/tailwind-config/build.config.ts
  54. 0 67
      internal/tailwind-config/package.json
  55. 0 266
      internal/tailwind-config/src/index.ts
  56. 0 3
      internal/tailwind-config/src/module.d.ts
  57. 0 53
      internal/tailwind-config/src/plugins/entry.ts
  58. 0 15
      internal/tailwind-config/src/postcss.config.ts
  59. 0 6
      internal/tailwind-config/tsconfig.json
  60. 1 0
      internal/vite-config/package.json
  61. 4 0
      internal/vite-config/src/plugins/index.ts
  62. 40 0
      internal/vite-config/src/plugins/tailwind-reference.ts
  63. 1 3
      package.json
  64. 9 0
      packages/@core/base/design/package.json
  65. 457 54
      packages/@core/base/design/src/css/global.css
  66. 5 3
      packages/@core/base/design/src/css/nprogress.css
  67. 26 28
      packages/@core/base/shared/src/utils/__tests__/resources.test.ts
  68. 1 1
      packages/@core/preferences/src/config.ts
  69. 0 1
      packages/@core/ui-kit/form-ui/postcss.config.mjs
  70. 10 13
      packages/@core/ui-kit/form-ui/src/form-api.ts
  71. 2 2
      packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
  72. 1 1
      packages/@core/ui-kit/form-ui/src/form-render/form.vue
  73. 0 1
      packages/@core/ui-kit/form-ui/tailwind.config.mjs
  74. 0 1
      packages/@core/ui-kit/layout-ui/postcss.config.mjs
  75. 2 2
      packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue
  76. 1 1
      packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-collapse-button.vue
  77. 1 1
      packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-fixed-button.vue
  78. 2 2
      packages/@core/ui-kit/layout-ui/src/vben-layout.vue
  79. 0 1
      packages/@core/ui-kit/layout-ui/tailwind.config.mjs
  80. 1 0
      packages/@core/ui-kit/menu-ui/package.json
  81. 0 1
      packages/@core/ui-kit/menu-ui/postcss.config.mjs
  82. 1 1
      packages/@core/ui-kit/menu-ui/src/components/menu-badge-dot.vue
  83. 3 1
      packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue
  84. 0 1
      packages/@core/ui-kit/menu-ui/tailwind.config.mjs
  85. 0 1
      packages/@core/ui-kit/popup-ui/postcss.config.mjs
  86. 1 1
      packages/@core/ui-kit/popup-ui/src/alert/alert.vue
  87. 8 9
      packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts
  88. 12 17
      packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts
  89. 6 6
      packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue
  90. 8 9
      packages/@core/ui-kit/popup-ui/src/modal/__tests__/modal-api.test.ts
  91. 13 19
      packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts
  92. 4 5
      packages/@core/ui-kit/popup-ui/src/modal/modal.vue
  93. 0 1
      packages/@core/ui-kit/popup-ui/tailwind.config.mjs
  94. 1 2
      packages/@core/ui-kit/shadcn-ui/components.json
  95. 1 0
      packages/@core/ui-kit/shadcn-ui/package.json
  96. 0 1
      packages/@core/ui-kit/shadcn-ui/postcss.config.mjs
  97. 2 2
      packages/@core/ui-kit/shadcn-ui/src/components/avatar/avatar.vue
  98. 1 1
      packages/@core/ui-kit/shadcn-ui/src/components/back-top/back-top.vue
  99. 7 5
      packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/breadcrumb-background.vue
  100. 1 1
      packages/@core/ui-kit/shadcn-ui/src/components/button/button.vue

+ 7 - 0
.gitignore

@@ -50,3 +50,10 @@ vite.config.ts.*
 *.sw?
 .history
 .cursor
+
+# AI
+.agent
+.agents
+.claude
+.codex
+skills-lock.json

+ 0 - 1
.npmrc

@@ -2,7 +2,6 @@ registry=https://registry.npmmirror.com
 public-hoist-pattern[]=lefthook
 public-hoist-pattern[]=eslint
 public-hoist-pattern[]=prettier
-public-hoist-pattern[]=prettier-plugin-tailwindcss
 public-hoist-pattern[]=stylelint
 public-hoist-pattern[]=*postcss*
 public-hoist-pattern[]=@commitlint/*

+ 5 - 0
.prettierignore

@@ -1,6 +1,10 @@
 dist
 dev-dist
 .local
+.claude
+.agent
+.agents
+.codex
 .output.js
 node_modules
 .nvmrc
@@ -16,3 +20,4 @@ CODEOWNERS
 public
 .npmrc
 *-lock.yaml
+skills-lock.json

+ 4 - 0
.stylelintignore

@@ -2,3 +2,7 @@ dist
 public
 __tests__
 coverage
+.codex
+.claude
+.agent
+.agents

+ 7 - 3
.vscode/settings.json

@@ -1,5 +1,6 @@
 {
-  "tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts",
+  "tailwindCSS.experimental.configFile": "packages/@core/base/design/src/css/global.css",
+  "tailwindCSS.lint.suggestCanonicalClasses": "ignore",
   // workbench
   "workbench.list.smoothScrolling": true,
   "workbench.startupEditor": "newUntitledFile",
@@ -31,6 +32,9 @@
   "editor.autoClosingOvertype": "always",
   "editor.autoClosingQuotes": "beforeWhitespace",
   "editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
+  "editor.quickSuggestions": {
+    "strings": "on"
+  },
   "editor.codeActionsOnSave": {
     "source.fixAll.eslint": "explicit",
     "source.fixAll.stylelint": "explicit",
@@ -79,6 +83,7 @@
   "files.insertFinalNewline": true,
   "files.simpleDialog.enable": true,
   "files.associations": {
+    "*.css": "tailwindcss",
     "*.ejs": "html",
     "*.art": "html",
     "**/tsconfig.json": "jsonc",
@@ -220,8 +225,7 @@
     "*.env": "$(capture).env.*",
     "README.md": "README*,CHANGELOG*,LICENSE,CNAME",
     "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
-    "eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
-    "tailwind.config.mjs": "postcss.*"
+    "eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml"
   },
   "commentTranslate.hover.enabled": false,
   "commentTranslate.multiLineMerge": true,

+ 148 - 0
CLAUDE.md

@@ -0,0 +1,148 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## 技术栈
+
+1. 基于 **pnpm workspaces** + **Turborepo** 的 Vue 3 + TypeScript + Vite monorepo 项目。
+2. 提供多个 UI 组件库版本(Ant Design Vue、Element Plus、Naive UI、TDesign),共享同一套使用tailwindcss+shadcn-vue的UI组件库核心框架。
+3. 要求 Node ≥ 20.19.0,pnpm ≥ 10。
+4. 使用 **prettier** + **eslint** + **stylelint** 进行代码检查和格式化。
+5. 使用 **vitest** 进行单元测试。
+6. 使用 **commitlint** 进行提交规范。
+7. 使用 **czg** 进行提交规范。
+8. 使用 **lefthook** 进行提交规范。
+9. 使用 **vsh** 进行代码检查和格式化。
+10. 使用 **turbo** 进行构建。
+11. 使用 **vite** 进行开发。
+12. 使用 **vue-tsc** 进行类型检查。
+
+```bash
+# 其他检查
+pnpm check:circular # 循环依赖扫描
+pnpm check:dep      # depcheck 依赖检查
+pnpm check:cspell   # 拼写检查
+
+# 清理
+pnpm clean          # 删除 dist、node_modules 等产物
+pnpm reinstall      # clean + 重新安装
+
+# 交互式规范提交
+pnpm commit         # czg 提交向导
+```
+
+Turbo 任务通过 `dependsOn: ["^build"]` 级联,构建某个应用时会自动先构建其所有依赖包。
+
+## Monorepo 目录结构
+
+```text
+apps/
+  backend-mock/     # 基于 Nitro 的 mock API 服务(h3 路由 + faker.js 数据)
+  web-antd/         # Ant Design Vue 应用
+  web-ele/          # Element Plus 应用
+  web-naive/        # Naive UI 应用
+  web-tdesign/      # TDesign Vue 应用
+
+packages/
+  @core/            # 框架核心(不依赖具体 UI 库)
+    base/           # 共享工具、缓存、颜色处理、类型定义
+    composables/    # 核心 Vue composable
+    preferences/    # PreferenceManager 类(响应式、持久化配置)
+    ui-kit/         # UI 组件片段:form-ui、layout-ui、menu-ui、popup-ui、shadcn-ui、tabs-ui
+  effects/          # 高层模块,可依赖 @core 和 UI 库
+    access/         # 路由/菜单生成与权限指令
+    common-ui/      # 通用 UI 组件(ApiComponent、IconPicker、VCropper、Tippy 等)
+    hooks/          # useAppConfig 等
+    layouts/        # BasicLayout、登录页、各类 widgets
+    plugins/        # Motion 等插件
+    request/        # RequestClient(axios 封装 + 拦截器体系)
+  constants/        # 全局常量(LOGIN_PATH 等)
+  icons/            # Iconify 图标封装
+  locales/          # vue-i18n 初始化、loadLocalesMap 工具
+  preferences/      # 对外暴露 @core/preferences 的公共 API
+  stores/           # Pinia 全局 store:useAccessStore、useUserStore、useTabbarStore
+  styles/           # 全局 CSS / TailwindCSS 基础样式
+  types/            # 共享 TypeScript 类型
+  utils/            # 共享工具函数(mergeRouteModules、mapTree 等)
+
+internal/
+  lint-configs/     # ESLint、Prettier、Stylelint、commitlint 配置包
+  node-utils/       # 构建时 Node 工具
+  tailwind-config/  # 共享 Tailwind 配置
+  tsconfig/         # 基础 tsconfig
+  vite-config/      # 共享 Vite 配置工厂 + 插件集合
+
+scripts/
+  vsh/              # CLI 工具(lint、check-dep、check-circular、publint)
+  turbo-run/        # 交互式 turbo 运行器
+
+playground/         # 组件演示场
+docs/               # VitePress 文档
+```
+
+## 核心架构说明
+
+### 应用启动流程
+
+每个应用的 `src/main.ts` 调用 `bootstrap(namespace)`(位于 `src/bootstrap.ts`),依次执行:
+
+1. 初始化**组件适配器**(`src/adapter/component/index.ts`)——将通用表单组件名映射到具体 UI 库的组件。
+2. 调用 `initSetupVbenForm()`(`src/adapter/form.ts`)配置通用表单系统。
+3. 依次初始化 i18n、Pinia stores、权限指令、Tippy、路由、MotionPlugin,最后挂载到 `#app`。
+
+### 偏好设置系统
+
+`@vben/preferences` 导出单例 `preferences`(`PreferenceManager`)。它是响应式的,自动持久化到 localStorage(以应用 namespace 为前缀),并驱动主题 CSS 变量的更新。各应用在 `src/preferences.ts` 中调用 `defineOverridesPreferences()` 覆盖默认值,无需修改核心代码。
+
+### 权限/访问系统
+
+`@vben/access`(`packages/effects/access`)支持三种访问模式:
+
+- **frontend**:根据用户角色过滤静态路由。
+- **backend**:从接口(`getAllMenusApi`)获取菜单并动态注册路由。
+- **mixed**:同时使用以上两种方式。
+
+路由守卫(`src/router/guard.ts`)在登录后首次导航时调用 `generateAccess()`,将结果存入 `useAccessStore`,再重定向到目标页。`v-access` 指令和 `<AccessControl>` 组件用于按权限码或角色控制 UI 元素显示。
+
+### 请求客户端
+
+`@vben/request` 将 Axios 封装为 `RequestClient`。每个应用在 `src/api/request.ts` 中创建自己的实例,挂载以下拦截器:
+
+- **请求拦截**:自动附加 Bearer Token 和 Accept-Language 头。
+- **`defaultResponseInterceptor`**:解包 `{ code, data, message }` 响应格式。
+- **`authenticateResponseInterceptor`**:处理 401,自动刷新 token 或跳转登录。
+- **`errorMessageResponseInterceptor`**:调用 `message.error()` 显示错误。
+
+在 API 文件中从 `#/api/request` 引入 `requestClient`(自动解包响应)或 `baseRequestClient`(原始响应)。
+
+### 路由组织
+
+- `src/router/routes/modules/*.ts`:需要权限验证的动态路由。
+- `src/router/routes/core/`:始终可访问的路由(登录页、404 等)。
+- `mergeRouteModules(import.meta.glob(...))` 用于聚合路由模块文件。
+- 动态路由在运行时由权限系统注册。
+
+### 适配器模式
+
+每个 UI 库应用在 `src/adapter/` 下提供适配器,将 `@vben/common-ui` 的通用 form/modal/drawer 组件桥接到具体组件库。这是 `web-antd`、`web-ele` 等应用之间的主要差异所在。
+
+### 全局 Pinia Store
+
+- `useAccessStore`:token、路由、菜单、锁屏、登录过期状态。
+- `useUserStore`:用户信息、角色、homePath。
+- `useTabbarStore`:已打开标签页管理。
+
+所有 store 通过 `@vben/stores` 的 `initStores(app, { namespace })` 统一初始化。
+
+### Mock 后端
+
+`apps/backend-mock` 是一个 Nitro 服务器,可单独启动:`pnpm -F @vben/backend-mock start`。Vite 开发服务器通过 `vite.config.ts`(位于 `internal/vite-config`)将 API 请求代理到该服务。
+
+## 开发约定
+
+- **路径别名**:`#/*` 指向各应用的 `./src/*`(在 `package.json#imports` 中定义)。
+- **依赖版本管理**:内部包使用 `workspace:*`,第三方包使用 `catalog:`(版本集中在 `pnpm-workspace.yaml#catalog` 中管理)。
+- **提交规范**:遵循 Conventional Commits(`feat`、`fix`、`chore`、`docs`、`refactor`、`perf`、`test`、`ci`、`style`、`types`、`revert`),由 lefthook + commitlint 强制执行。
+- **pre-commit 钩子**(lefthook):自动对暂存文件执行 prettier + eslint + stylelint,推荐使用 `pnpm commit`(czg)提交。
+- **新增页面**:在 `src/views/` 下创建 `.vue` 文件,在 `src/router/routes/modules/` 下添加路由模块;若使用 backend 模式,还需确保后端接口返回对应菜单数据。
+- **国际化**:统一使用 `$t('key')`,locale 文件位于 `packages/locales/`,项目级国际化文件位于 `src/locales/langs`。

+ 1 - 1
apps/backend-mock/api/table/list.ts

@@ -79,7 +79,7 @@ export default eventHandler(async (event) => {
       const aValue = a[sortKey] as unknown;
       const bValue = b[sortKey] as unknown;
 
-      let result = 0;
+      let result: number;
 
       if (typeof aValue === 'number' && typeof bValue === 'number') {
         result = aValue - bValue;

+ 0 - 1
apps/web-antd/postcss.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config/postcss';

+ 1 - 2
apps/web-antd/src/store/auth.ts

@@ -98,8 +98,7 @@ export const useAuthStore = defineStore('auth', () => {
   }
 
   async function fetchUserInfo() {
-    let userInfo: null | UserInfo = null;
-    userInfo = await getUserInfoApi();
+    const userInfo = await getUserInfoApi();
     userStore.setUserInfo(userInfo);
     return userInfo;
   }

+ 2 - 2
apps/web-antd/src/views/dashboard/analytics/index.vue

@@ -76,10 +76,10 @@ const chartTabs: TabOption[] = [
     </AnalysisChartsTabs>
 
     <div class="mt-5 w-full md:flex">
-      <AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
+      <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问数量">
         <AnalyticsVisitsData />
       </AnalysisChartCard>
-      <AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
+      <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问来源">
         <AnalyticsVisitsSource />
       </AnalysisChartCard>
       <AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">

+ 0 - 1
apps/web-antd/tailwind.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';

+ 0 - 1
apps/web-antdv-next/postcss.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config/postcss';

+ 1 - 2
apps/web-antdv-next/src/store/auth.ts

@@ -98,8 +98,7 @@ export const useAuthStore = defineStore('auth', () => {
   }
 
   async function fetchUserInfo() {
-    let userInfo: null | UserInfo = null;
-    userInfo = await getUserInfoApi();
+    const userInfo = await getUserInfoApi();
     userStore.setUserInfo(userInfo);
     return userInfo;
   }

+ 2 - 2
apps/web-antdv-next/src/views/dashboard/analytics/index.vue

@@ -76,10 +76,10 @@ const chartTabs: TabOption[] = [
     </AnalysisChartsTabs>
 
     <div class="mt-5 w-full md:flex">
-      <AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
+      <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问数量">
         <AnalyticsVisitsData />
       </AnalysisChartCard>
-      <AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
+      <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问来源">
         <AnalyticsVisitsSource />
       </AnalysisChartCard>
       <AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">

+ 0 - 1
apps/web-antdv-next/tailwind.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';

+ 0 - 1
apps/web-ele/postcss.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config/postcss';

+ 1 - 2
apps/web-ele/src/store/auth.ts

@@ -99,8 +99,7 @@ export const useAuthStore = defineStore('auth', () => {
   }
 
   async function fetchUserInfo() {
-    let userInfo: null | UserInfo = null;
-    userInfo = await getUserInfoApi();
+    const userInfo = await getUserInfoApi();
     userStore.setUserInfo(userInfo);
     return userInfo;
   }

+ 2 - 2
apps/web-ele/src/views/dashboard/analytics/index.vue

@@ -76,10 +76,10 @@ const chartTabs: TabOption[] = [
     </AnalysisChartsTabs>
 
     <div class="mt-5 w-full md:flex">
-      <AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
+      <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问数量">
         <AnalyticsVisitsData />
       </AnalysisChartCard>
-      <AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
+      <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问来源">
         <AnalyticsVisitsSource />
       </AnalysisChartCard>
       <AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">

+ 1 - 3
apps/web-ele/src/views/demos/element/index.vue

@@ -102,9 +102,7 @@ const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
       </ElCard>
       <ElCard class="mb-5 w-80">
         <template #header> V-Loading </template>
-        <div class="flex size-72 items-center justify-center" v-loading="true">
-          一些演示的内容
-        </div>
+        <div class="flex-center size-72" v-loading="true">一些演示的内容</div>
       </ElCard>
       <ElCard class="mb-5 w-80">
         <ElTable :data="tableData" stripe>

+ 0 - 1
apps/web-ele/tailwind.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';

+ 0 - 1
apps/web-naive/postcss.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config/postcss';

+ 1 - 2
apps/web-naive/src/store/auth.ts

@@ -99,8 +99,7 @@ export const useAuthStore = defineStore('auth', () => {
   }
 
   async function fetchUserInfo() {
-    let userInfo: null | UserInfo = null;
-    userInfo = await getUserInfoApi();
+    const userInfo = await getUserInfoApi();
     userStore.setUserInfo(userInfo);
     return userInfo;
   }

+ 2 - 2
apps/web-naive/src/views/dashboard/analytics/index.vue

@@ -76,10 +76,10 @@ const chartTabs: TabOption[] = [
     </AnalysisChartsTabs>
 
     <div class="mt-5 w-full md:flex">
-      <AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
+      <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问数量">
         <AnalyticsVisitsData />
       </AnalysisChartCard>
-      <AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
+      <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问来源">
         <AnalyticsVisitsSource />
       </AnalysisChartCard>
       <AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">

+ 0 - 1
apps/web-naive/tailwind.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';

+ 0 - 1
apps/web-tdesign/postcss.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config/postcss';

+ 1 - 2
apps/web-tdesign/src/store/auth.ts

@@ -97,8 +97,7 @@ export const useAuthStore = defineStore('auth', () => {
   }
 
   async function fetchUserInfo() {
-    let userInfo: null | UserInfo = null;
-    userInfo = await getUserInfoApi();
+    const userInfo = await getUserInfoApi();
     userStore.setUserInfo(userInfo);
     return userInfo;
   }

+ 2 - 2
apps/web-tdesign/src/views/dashboard/analytics/index.vue

@@ -76,10 +76,10 @@ const chartTabs: TabOption[] = [
     </AnalysisChartsTabs>
 
     <div class="mt-5 w-full md:flex">
-      <AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
+      <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问数量">
         <AnalyticsVisitsData />
       </AnalysisChartCard>
-      <AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
+      <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问来源">
         <AnalyticsVisitsSource />
       </AnalysisChartCard>
       <AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">

+ 3 - 3
apps/web-tdesign/src/views/demos/tdesign/index.vue

@@ -38,7 +38,7 @@ function notify(type: NotificationType) {
     description="支持多语言,主题功能集成切换等"
     title="TDesign Vue组件使用演示"
   >
-    <Card class="!mb-5" title="按钮">
+    <Card class="mb-5!" title="按钮">
       <Space>
         <Button>Default</Button>
         <Button theme="primary"> Primary </Button>
@@ -46,7 +46,7 @@ function notify(type: NotificationType) {
         <Button theme="danger"> Error </Button>
       </Space>
     </Card>
-    <Card class="!mb-5" title="Message">
+    <Card class="mb-5!" title="Message">
       <Space>
         <Button @click="info"> 信息 </Button>
         <Button theme="danger" @click="error"> 错误 </Button>
@@ -55,7 +55,7 @@ function notify(type: NotificationType) {
       </Space>
     </Card>
 
-    <Card class="!mb-5" title="Notification">
+    <Card class="mb-5!" title="Notification">
       <Space>
         <Button @click="notify('info')"> 信息 </Button>
         <Button theme="danger" @click="notify('error')"> 错误 </Button>

+ 0 - 1
apps/web-tdesign/tailwind.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';

+ 3 - 0
cspell.json

@@ -13,6 +13,7 @@
     "brotli",
     "cascader",
     "clsx",
+    "dedup",
     "defu",
     "demi",
     "dotenv",
@@ -41,6 +42,7 @@
     "noreferrer",
     "nprogress",
     "nuxt",
+    "organisation",
     "pinia",
     "prefixs",
     "publint",
@@ -51,6 +53,7 @@
     "sonner",
     "sortablejs",
     "styl",
+    "tabler",
     "taze",
     "tdesign",
     "ui-kit",

+ 1 - 1
docs/.vitepress/components/demo-preview.vue

@@ -27,7 +27,7 @@ const parsedFiles = computed(() => {
         <ClientOnly>
           <slot v-if="parsedFiles.length > 0"></slot>
           <div v-else class="text-sm text-destructive">
-            <span class="rounded-sm bg-destructive px-1 py-1 text-foreground">
+            <span class="rounded-sm bg-destructive p-1 text-foreground">
               ERROR:
             </span>
             The preview directory does not exist. Please check the 'dir'

+ 4 - 4
docs/.vitepress/components/preview-group.vue

@@ -56,15 +56,15 @@ const toggleOpen = () => {
         <TabsList class="relative flex">
           <template v-if="open">
             <TabsIndicator
-              class="absolute bottom-0 left-0 h-[2px] w-[--reka-tabs-indicator-size] translate-x-[--reka-tabs-indicator-position] rounded-full transition-[width,transform] duration-300"
+              class="absolute bottom-0 left-0 h-[2px] w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position) rounded-full transition-[width,transform] duration-300"
             >
-              <div class="size-full bg-[var(--vp-c-indigo-1)]"></div>
+              <div class="size-full bg-(--vp-c-indigo-1)"></div>
             </TabsIndicator>
             <TabsTrigger
               v-for="(tab, index) in tabs"
               :key="index"
               :value="tab.label"
-              class="border-box px-4 py-3 text-foreground data-[state=active]:text-[var(--vp-c-indigo-1)]"
+              class="border-box px-4 py-3 text-foreground data-[state=active]:text-(--vp-c-indigo-1)"
               tabindex="-1"
             >
               {{ tab.label }}
@@ -92,7 +92,7 @@ const toggleOpen = () => {
     </div>
     <div
       :class="`${open ? 'h-[unset] max-h-[80vh]' : 'h-0'}`"
-      class="block overflow-y-scroll bg-[var(--vp-code-block-bg)] transition-all duration-300"
+      class="block overflow-y-scroll bg-(--vp-code-block-bg) transition-all duration-300"
     >
       <TabsContent
         v-for="tab in tabs"

+ 3 - 5
docs/.vitepress/config/shared.mts

@@ -12,7 +12,7 @@ import {
   GitChangelog,
   GitChangelogMarkdownSection,
 } from '@nolebase/vitepress-plugin-git-changelog/vite';
-import tailwind from 'tailwindcss';
+import tailwindcss from '@tailwindcss/vite';
 import { defineConfig, postcssIsolateStyles } from 'vitepress';
 import {
   groupIconMdPlugin,
@@ -57,10 +57,7 @@ export const shared = defineConfig({
     },
     css: {
       postcss: {
-        plugins: [
-          tailwind(),
-          postcssIsolateStyles({ includeFiles: [/vp-doc\.css/] }),
-        ],
+        plugins: [postcssIsolateStyles({ includeFiles: [/vp-doc\.css/] })],
       },
       preprocessorOptions: {
         scss: {
@@ -72,6 +69,7 @@ export const shared = defineConfig({
       stringify: true,
     },
     plugins: [
+      tailwindcss(),
       GitChangelog({
         mapAuthors: [
           {

+ 1 - 1
docs/.vitepress/theme/components/vben-contributors.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts"></script>
 
 <template>
-  <div class="vp-doc vben-contributors">
+  <div class="vben-contributors vp-doc">
     <p>Contributors</p>
     <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
       <img

+ 1 - 0
docs/package.json

@@ -27,6 +27,7 @@
   },
   "devDependencies": {
     "@nolebase/vitepress-plugin-git-changelog": "catalog:",
+    "@tailwindcss/vite": "catalog:",
     "@vben/vite-config": "workspace:*",
     "@vite-pwa/vitepress": "catalog:",
     "vitepress": "catalog:",

+ 0 - 11
docs/tailwind.config.mjs

@@ -1,11 +0,0 @@
-import tailwindcssConfig from '@vben/tailwind-config';
-
-export default {
-  ...tailwindcssConfig,
-  content: [
-    ...tailwindcssConfig.content,
-    '.vitepress/**/*.{js,mts,ts,vue}',
-    'src/demos/**/*.{js,mts,ts,vue}',
-    'src/**/*.md',
-  ],
-};

+ 3 - 4
internal/lint-configs/eslint-config/package.json

@@ -32,12 +32,13 @@
     "eslint-plugin-import-x": "catalog:"
   },
   "devDependencies": {
+    "@eslint-community/eslint-plugin-eslint-comments": "catalog:",
     "@eslint/js": "catalog:",
-    "@types/eslint": "catalog:",
     "@typescript-eslint/eslint-plugin": "catalog:",
     "@typescript-eslint/parser": "catalog:",
+    "@vitest/eslint-plugin": "catalog:",
     "eslint": "catalog:",
-    "eslint-plugin-eslint-comments": "catalog:",
+    "eslint-plugin-better-tailwindcss": "catalog:",
     "eslint-plugin-jsdoc": "catalog:",
     "eslint-plugin-jsonc": "catalog:",
     "eslint-plugin-n": "catalog:",
@@ -48,11 +49,9 @@
     "eslint-plugin-regexp": "catalog:",
     "eslint-plugin-unicorn": "catalog:",
     "eslint-plugin-unused-imports": "catalog:",
-    "eslint-plugin-vitest": "catalog:",
     "eslint-plugin-vue": "catalog:",
     "eslint-plugin-yml": "catalog:",
     "globals": "catalog:",
-    "jsonc-eslint-parser": "catalog:",
     "vue-eslint-parser": "catalog:",
     "yaml-eslint-parser": "catalog:"
   }

+ 1 - 2
internal/lint-configs/eslint-config/src/configs/comments.ts

@@ -4,8 +4,7 @@ import { interopDefault } from '../util';
 
 export async function comments(): Promise<Linter.Config[]> {
   const [pluginComments] = await Promise.all([
-    // @ts-expect-error - no types
-    interopDefault(import('eslint-plugin-eslint-comments')),
+    interopDefault(import('@eslint-community/eslint-plugin-eslint-comments')),
   ] as const);
 
   return [

+ 6 - 0
internal/lint-configs/eslint-config/src/configs/ignores.ts

@@ -48,6 +48,12 @@ export async function ignores(): Promise<Linter.Config[]> {
         '**/*.woff',
         '**/.github',
         '**/lefthook.yml',
+
+        '**/.agent/**',
+        '**/.agents/**',
+        '**/.codex/**',
+        '**/.claude/**',
+        '**/.cursor/**',
       ],
     },
   ];

+ 1 - 0
internal/lint-configs/eslint-config/src/configs/index.ts

@@ -11,6 +11,7 @@ export * from './perfectionist';
 export * from './pnpm';
 export * from './prettier';
 export * from './regexp';
+export * from './tailwindcss';
 export * from './test';
 export * from './turbo';
 export * from './typescript';

+ 2 - 7
internal/lint-configs/eslint-config/src/configs/jsonc.ts

@@ -3,17 +3,12 @@ import type { Linter } from 'eslint';
 import { interopDefault } from '../util';
 
 export async function jsonc(): Promise<Linter.Config[]> {
-  const [pluginJsonc, parserJsonc] = await Promise.all([
-    interopDefault(import('eslint-plugin-jsonc')),
-    interopDefault(import('jsonc-eslint-parser')),
-  ] as const);
+  const pluginJsonc = await interopDefault(import('eslint-plugin-jsonc'));
 
   return [
     {
       files: ['**/*.json', '**/*.json5', '**/*.jsonc', '*.code-workspace'],
-      languageOptions: {
-        parser: parserJsonc as any,
-      },
+      language: 'jsonc/x',
       plugins: {
         jsonc: pluginJsonc as any,
       },

+ 0 - 1
internal/lint-configs/eslint-config/src/configs/node.ts

@@ -23,7 +23,6 @@ export async function node(): Promise<Linter.Config[]> {
               'vitest',
               'vite',
               '@vue/test-utils',
-              '@vben/tailwind-config',
               '@playwright/test',
             ],
           },

+ 36 - 19
internal/lint-configs/eslint-config/src/configs/perfectionist.ts

@@ -21,41 +21,58 @@ export async function perfectionist(): Promise<Linter.Config[]> {
         'perfectionist/sort-imports': [
           'error',
           {
-            customGroups: {
-              type: {
-                'vben-core-type': ['^@vben-core/.+'],
-                'vben-type': ['^@vben/.+'],
-                'vue-type': ['^vue$', '^vue-.+', '^@vue/.+'],
+            customGroups: [
+              {
+                selector: 'type',
+                groupName: 'vben-core-type',
+                elementNamePattern: '^@vben-core/.+',
               },
-              value: {
-                vben: ['^@vben/.+'],
-                'vben-core': ['^@vben-core/.+'],
-                vue: ['^vue$', '^vue-.+', '^@vue/.+'],
+              {
+                selector: 'type',
+                groupName: 'vben-type',
+                elementNamePattern: '^@vben/.+',
               },
-            },
+              {
+                selector: 'type',
+                groupName: 'vue-type',
+                elementNamePattern: ['^vue$', '^vue-.+', '^@vue/.+'],
+              },
+              {
+                groupName: 'vben',
+                elementNamePattern: '^@vben/.+',
+              },
+              {
+                groupName: 'vben-core',
+                elementNamePattern: '^@vben-core/.+',
+              },
+              {
+                groupName: 'vue',
+                elementNamePattern: ['^vue$', '^vue-.+', '^@vue/.+'],
+              },
+            ],
             environment: 'node',
             groups: [
-              ['external-type', 'builtin-type', 'type'],
+              ['type-external', 'type-builtin', 'type-import'],
               'vue-type',
               'vben-type',
               'vben-core-type',
-              ['parent-type', 'sibling-type', 'index-type'],
-              ['internal-type'],
-              'builtin',
+              ['type-parent', 'type-sibling', 'type-index'],
+              ['type-internal'],
+              'value-builtin',
               'vue',
               'vben',
               'vben-core',
-              'external',
-              'internal',
-              ['parent', 'sibling', 'index'],
+              'value-external',
+              'value-internal',
+              ['value-parent', 'value-sibling', 'value-index'],
               'side-effect',
               'side-effect-style',
               'style',
-              'object',
+              'ts-equals-import',
               'unknown',
             ],
             internalPattern: ['^#/.+'],
-            newlinesBetween: 'always',
+            newlinesBetween: 1,
             order: 'asc',
             type: 'natural',
           },

+ 2 - 5
internal/lint-configs/eslint-config/src/configs/pnpm.ts

@@ -3,18 +3,15 @@ import type { Linter } from 'eslint';
 import { interopDefault } from '../util';
 
 export async function pnpm(): Promise<Linter.Config[]> {
-  const [pluginPnpm, parserPnpm, parserJsonc] = await Promise.all([
+  const [pluginPnpm, parserPnpm] = await Promise.all([
     interopDefault(import('eslint-plugin-pnpm')),
     interopDefault(import('yaml-eslint-parser')),
-    interopDefault(import('jsonc-eslint-parser')),
   ] as const);
 
   return [
     {
       files: ['package.json', '**/package.json'],
-      languageOptions: {
-        parser: parserJsonc,
-      },
+      language: 'jsonc/x',
       plugins: {
         pnpm: pluginPnpm,
       },

+ 49 - 0
internal/lint-configs/eslint-config/src/configs/tailwindcss.ts

@@ -0,0 +1,49 @@
+import type { Linter } from 'eslint';
+
+import { getDefaultSelectors } from 'eslint-plugin-better-tailwindcss/defaults';
+import { SelectorKind } from 'eslint-plugin-better-tailwindcss/types';
+
+import { interopDefault } from '../util';
+
+export async function tailwindcss(): Promise<Linter.Config[]> {
+  const [pluginBetterTailwindcss] = await Promise.all([
+    interopDefault(import('eslint-plugin-better-tailwindcss')),
+  ] as const);
+
+  return [
+    {
+      plugins: {
+        'better-tailwindcss': pluginBetterTailwindcss,
+      },
+      // shadcn-ui 内部组件是自动生成的,不做太多限制
+      ignores: ['packages/@core/ui-kit/shadcn-ui/**/**'],
+      settings: {
+        'better-tailwindcss': {
+          entryPoint: 'packages/@core/base/design/src/css/global.css',
+          selectors: [
+            ...getDefaultSelectors(), // preserve default selectors
+            {
+              kind: SelectorKind.Attribute,
+              match: [{ type: 'objectValues' }],
+              name: '^classNames$',
+            },
+          ],
+        },
+      },
+      rules: {
+        ...pluginBetterTailwindcss.configs.recommended.rules,
+        'better-tailwindcss/enforce-consistent-class-order': [
+          'error',
+          {
+            detectComponentClasses: true,
+            unknownClassOrder: 'asc',
+            unknownClassPosition: 'start',
+          },
+        ],
+        // Let Prettier own wrapping decisions to avoid ping-pong formatting.
+        'better-tailwindcss/enforce-consistent-line-wrapping': 'off',
+        'better-tailwindcss/no-unknown-classes': 'off',
+      },
+    },
+  ];
+}

+ 1 - 1
internal/lint-configs/eslint-config/src/configs/test.ts

@@ -4,7 +4,7 @@ import { interopDefault } from '../util';
 
 export async function test(): Promise<Linter.Config[]> {
   const [pluginTest, pluginNoOnlyTests] = await Promise.all([
-    interopDefault(import('eslint-plugin-vitest')),
+    interopDefault(import('@vitest/eslint-plugin')),
     // @ts-expect-error - no types
     interopDefault(import('eslint-plugin-no-only-tests')),
   ] as const);

+ 1 - 1
internal/lint-configs/eslint-config/src/configs/yaml.ts

@@ -12,7 +12,7 @@ export async function yaml(): Promise<Linter.Config[]> {
     {
       files: ['**/*.y?(a)ml'],
       plugins: {
-        yaml: pluginYaml as any,
+        yaml: pluginYaml,
       },
       languageOptions: {
         parser: parserYaml,

+ 1 - 5
internal/lint-configs/eslint-config/src/custom-config.ts

@@ -1,10 +1,6 @@
 import type { Linter } from 'eslint';
 
-const restrictedImportIgnores = [
-  '**/vite.config.mts',
-  '**/tailwind.config.mjs',
-  '**/postcss.config.mjs',
-];
+const restrictedImportIgnores = ['**/vite.config.mts'];
 
 const customConfig: Linter.Config[] = [
   // shadcn-ui 内部组件是自动生成的,不做太多限制

+ 2 - 0
internal/lint-configs/eslint-config/src/index.ts

@@ -14,6 +14,7 @@ import {
   pnpm,
   prettier,
   regexp,
+  tailwindcss,
   test,
   turbo,
   typescript,
@@ -45,6 +46,7 @@ async function defineConfig(config: FlatConfig[] = []) {
     perfectionist(),
     comments(),
     jsdoc(),
+    tailwindcss(),
     unicorn(),
     test(),
     regexp(),

+ 0 - 1
internal/lint-configs/prettier-config/index.mjs

@@ -9,7 +9,6 @@ export default {
       },
     },
   ],
-  plugins: ['prettier-plugin-tailwindcss'],
   printWidth: 80,
   proseWrap: 'never',
   semi: true,

+ 1 - 2
internal/lint-configs/prettier-config/package.json

@@ -22,7 +22,6 @@
     }
   },
   "dependencies": {
-    "prettier": "catalog:",
-    "prettier-plugin-tailwindcss": "catalog:"
+    "prettier": "catalog:"
   }
 }

+ 13 - 1
internal/lint-configs/stylelint-config/index.mjs

@@ -67,6 +67,12 @@ export default {
           'use',
           'forward',
           'return',
+          'reference',
+          'plugin',
+          'source',
+          'theme',
+          'utility',
+          'custom-variant',
         ],
       },
     ],
@@ -130,12 +136,18 @@ export default {
           'use',
           'forward',
           'return',
+          'reference',
+          'plugin',
+          'source',
+          'theme',
+          'utility',
+          'custom-variant',
         ],
       },
     ],
     'scss/operator-no-newline-after': null,
     'selector-class-pattern':
-      '^(?:(?:o|c|u|t|s|is|has|_|js|qa)-)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*(?:__[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:--[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:[.+])?$',
+      '^-?(?:(?:o|c|u|t|s|is|has|_|js|qa)-)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*(?:__[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:--[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:[.+])?$',
 
     'selector-not-notation': null,
   },

+ 0 - 10
internal/tailwind-config/build.config.ts

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

+ 0 - 67
internal/tailwind-config/package.json

@@ -1,67 +0,0 @@
-{
-  "name": "@vben/tailwind-config",
-  "version": "5.6.0",
-  "private": true,
-  "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": "internal/tailwind-config"
-  },
-  "license": "MIT",
-  "type": "module",
-  "scripts": {
-    "stub": "pnpm unbuild --stub"
-  },
-  "files": [
-    "dist"
-  ],
-  "main": "./dist/index.mjs",
-  "module": "./dist/index.mjs",
-  "types": "./dist/index.d.ts",
-  "typesVersions": {
-    "*": {
-      "*": [
-        "./dist/*",
-        "./*"
-      ]
-    }
-  },
-  "exports": {
-    ".": {
-      "types": "./src/index.ts",
-      "import": "./dist/index.mjs",
-      "require": "./dist/index.cjs"
-    },
-    "./postcss": {
-      "types": "./src/postcss.config.ts",
-      "import": "./dist/postcss.config.mjs",
-      "require": "./dist/postcss.config.cjs",
-      "default": "./dist/postcss.config.mjs"
-    },
-    "./*": "./*"
-  },
-  "peerDependencies": {
-    "tailwindcss": "^3.4.3"
-  },
-  "dependencies": {
-    "@iconify/json": "catalog:",
-    "@iconify/tailwind": "catalog:",
-    "@manypkg/get-packages": "catalog:",
-    "@tailwindcss/nesting": "catalog:",
-    "@tailwindcss/typography": "catalog:",
-    "autoprefixer": "catalog:",
-    "cssnano": "catalog:",
-    "jiti": "catalog:",
-    "postcss": "catalog:",
-    "postcss-antd-fixes": "catalog:",
-    "postcss-import": "catalog:",
-    "postcss-preset-env": "catalog:",
-    "tailwindcss": "catalog:",
-    "tailwindcss-animate": "catalog:"
-  },
-  "devDependencies": {
-    "@types/postcss-import": "catalog:"
-  }
-}

+ 0 - 266
internal/tailwind-config/src/index.ts

@@ -1,266 +0,0 @@
-import type { Config } from 'tailwindcss';
-
-import path from 'node:path';
-
-import { addDynamicIconSelectors } from '@iconify/tailwind';
-import { getPackagesSync } from '@manypkg/get-packages';
-import typographyPlugin from '@tailwindcss/typography';
-import animate from 'tailwindcss-animate';
-
-import { enterAnimationPlugin } from './plugins/entry';
-
-// import defaultTheme from 'tailwindcss/defaultTheme';
-
-const { packages } = getPackagesSync(process.cwd());
-
-const tailwindPackages: string[] = [];
-
-packages.forEach((pkg) => {
-  // apps目录下和 @vben-core/tailwind-ui 包需要使用到 tailwindcss ui
-  // if (fs.existsSync(path.join(pkg.dir, 'tailwind.config.mjs'))) {
-  tailwindPackages.push(pkg.dir);
-  // }
-});
-
-const shadcnUiColors = {
-  accent: {
-    DEFAULT: 'hsl(var(--accent))',
-    foreground: 'hsl(var(--accent-foreground))',
-    hover: 'hsl(var(--accent-hover))',
-    lighter: 'has(val(--accent-lighter))',
-  },
-  background: {
-    deep: 'hsl(var(--background-deep))',
-    DEFAULT: 'hsl(var(--background))',
-  },
-  border: {
-    DEFAULT: 'hsl(var(--border))',
-  },
-  card: {
-    DEFAULT: 'hsl(var(--card))',
-    foreground: 'hsl(var(--card-foreground))',
-  },
-  destructive: {
-    ...createColorsPalette('destructive'),
-    DEFAULT: 'hsl(var(--destructive))',
-  },
-
-  foreground: {
-    DEFAULT: 'hsl(var(--foreground))',
-  },
-
-  input: {
-    background: 'hsl(var(--input-background))',
-    DEFAULT: 'hsl(var(--input))',
-  },
-  muted: {
-    DEFAULT: 'hsl(var(--muted))',
-    foreground: 'hsl(var(--muted-foreground))',
-  },
-  popover: {
-    DEFAULT: 'hsl(var(--popover))',
-    foreground: 'hsl(var(--popover-foreground))',
-  },
-  primary: {
-    ...createColorsPalette('primary'),
-    DEFAULT: 'hsl(var(--primary))',
-  },
-
-  ring: 'hsl(var(--ring))',
-  secondary: {
-    DEFAULT: 'hsl(var(--secondary))',
-    desc: 'hsl(var(--secondary-desc))',
-    foreground: 'hsl(var(--secondary-foreground))',
-  },
-};
-
-const customColors = {
-  green: {
-    ...createColorsPalette('green'),
-    foreground: 'hsl(var(--success-foreground))',
-  },
-  header: {
-    DEFAULT: 'hsl(var(--header))',
-  },
-  heavy: {
-    DEFAULT: 'hsl(var(--heavy))',
-    foreground: 'hsl(var(--heavy-foreground))',
-  },
-  main: {
-    DEFAULT: 'hsl(var(--main))',
-  },
-  overlay: {
-    content: 'hsl(var(--overlay-content))',
-    DEFAULT: 'hsl(var(--overlay))',
-  },
-  red: {
-    ...createColorsPalette('red'),
-    foreground: 'hsl(var(--destructive-foreground))',
-  },
-  sidebar: {
-    deep: 'hsl(var(--sidebar-deep))',
-    DEFAULT: 'hsl(var(--sidebar))',
-  },
-  success: {
-    ...createColorsPalette('success'),
-    DEFAULT: 'hsl(var(--success))',
-  },
-  warning: {
-    ...createColorsPalette('warning'),
-    DEFAULT: 'hsl(var(--warning))',
-  },
-  yellow: {
-    ...createColorsPalette('yellow'),
-    foreground: 'hsl(var(--warning-foreground))',
-  },
-};
-
-export default {
-  content: [
-    './index.html',
-    ...tailwindPackages.map((item) =>
-      path.join(item, 'src/**/*.{vue,js,ts,jsx,tsx,svelte,astro,html}'),
-    ),
-  ],
-  darkMode: 'selector',
-  plugins: [
-    animate,
-    typographyPlugin,
-    addDynamicIconSelectors(),
-    enterAnimationPlugin,
-  ],
-  prefix: '',
-  theme: {
-    container: {
-      center: true,
-      padding: '2rem',
-      screens: {
-        '2xl': '1400px',
-      },
-    },
-    extend: {
-      animation: {
-        'accordion-down': 'accordion-down 0.2s ease-out',
-        'accordion-up': 'accordion-up 0.2s ease-out',
-        'collapsible-down': 'collapsible-down 0.2s ease-in-out',
-        'collapsible-up': 'collapsible-up 0.2s ease-in-out',
-        float: 'float 5s linear 0ms infinite',
-      },
-
-      animationDuration: {
-        '2000': '2000ms',
-        '3000': '3000ms',
-      },
-      borderRadius: {
-        lg: 'var(--radius)',
-        md: 'calc(var(--radius) - 2px)',
-        sm: 'calc(var(--radius) - 4px)',
-        xl: 'calc(var(--radius) + 4px)',
-      },
-      boxShadow: {
-        float: `0 6px 16px 0 rgb(0 0 0 / 8%),
-          0 3px 6px -4px rgb(0 0 0 / 12%),
-          0 9px 28px 8px rgb(0 0 0 / 5%)`,
-      },
-      colors: {
-        ...customColors,
-        ...shadcnUiColors,
-      },
-      fontFamily: {
-        sans: [
-          'var(--font-family)',
-          //  ...defaultTheme.fontFamily.sans
-        ],
-      },
-      keyframes: {
-        'accordion-down': {
-          from: { height: '0' },
-          to: { height: 'var(--reka-accordion-content-height)' },
-        },
-        'accordion-up': {
-          from: { height: 'var(--reka-accordion-content-height)' },
-          to: { height: '0' },
-        },
-        'collapsible-down': {
-          from: { height: '0' },
-          to: { height: 'var(--reka-collapsible-content-height)' },
-        },
-        'collapsible-up': {
-          from: { height: 'var(--reka-collapsible-content-height)' },
-          to: { height: '0' },
-        },
-        float: {
-          '0%': { transform: 'translateY(0)' },
-          '50%': { transform: 'translateY(-20px)' },
-          '100%': { transform: 'translateY(0)' },
-        },
-      },
-      zIndex: {
-        '100': '100',
-        '1000': '1000',
-      },
-    },
-  },
-  safelist: ['dark'],
-} as Config;
-
-function createColorsPalette(name: string) {
-  // backgroundLightest: '#EFF6FF', // Tailwind CSS 默认的 `blue-50`
-  //         backgroundLighter: '#DBEAFE',  // Tailwind CSS 默认的 `blue-100`
-  //         backgroundLight: '#BFDBFE',    // Tailwind CSS 默认的 `blue-200`
-  //         borderLight: '#93C5FD',        // Tailwind CSS 默认的 `blue-300`
-  //         border: '#60A5FA',             // Tailwind CSS 默认的 `blue-400`
-  //         main: '#3B82F6',               // Tailwind CSS 默认的 `blue-500`
-  //         hover: '#2563EB',              // Tailwind CSS 默认的 `blue-600`
-  //         active: '#1D4ED8',             // Tailwind CSS 默认的 `blue-700`
-  //         backgroundDark: '#1E40AF',     // Tailwind CSS 默认的 `blue-800`
-  //         backgroundDarker: '#1E3A8A',   // Tailwind CSS 默认的 `blue-900`
-  //         backgroundDarkest: '#172554',  // Tailwind CSS 默认的 `blue-950`
-
-  // •	backgroundLightest (#EFF6FF): 适用于最浅的背景色,可能用于非常轻微的阴影或卡片的背景。
-  // •	backgroundLighter (#DBEAFE): 适用于略浅的背景色,通常用于次要背景或略浅的区域。
-  // •	backgroundLight (#BFDBFE): 适用于浅色背景,可能用于输入框或表单区域的背景。
-  // •	borderLight (#93C5FD): 适用于浅色边框,可能用于输入框或卡片的边框。
-  // •	border (#60A5FA): 适用于普通边框,可能用于按钮或卡片的边框。
-  // •	main (#3B82F6): 适用于主要的主题色,通常用于按钮、链接或主要的强调色。
-  // •	hover (#2563EB): 适用于鼠标悬停状态下的颜色,例如按钮悬停时的背景色或边框色。
-  // •	active (#1D4ED8): 适用于激活状态下的颜色,例如按钮按下时的背景色或边框色。
-  // •	backgroundDark (#1E40AF): 适用于深色背景,可能用于主要按钮或深色卡片背景。
-  // •	backgroundDarker (#1E3A8A): 适用于更深的背景,通常用于头部导航栏或页脚。
-  // •	backgroundDarkest (#172554): 适用于最深的背景,可能用于非常深色的区域或极端对比色。
-
-  return {
-    50: `hsl(var(--${name}-50))`,
-    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))`,
-    // 950: `hsl(var(--${name}-950))`,
-    // 激活状态下的颜色,适用于按钮按下时的背景色或边框色。
-    active: `hsl(var(--${name}-700))`,
-    // 浅色背景,适用于输入框或表单区域的背景。
-    'background-light': `hsl(var(--${name}-200))`,
-    // 适用于略浅的背景色,通常用于次要背景或略浅的区域。
-    'background-lighter': `hsl(var(--${name}-100))`,
-    // 最浅的背景色,适用于非常轻微的阴影或卡片的背景。
-    'background-lightest': `hsl(var(--${name}-50))`,
-    // 适用于普通边框,可能用于按钮或卡片的边框。
-    border: `hsl(var(--${name}-400))`,
-    // 浅色边框,适用于输入框或卡片的边框。
-    'border-light': `hsl(var(--${name}-300))`,
-    foreground: `hsl(var(--${name}-foreground))`,
-    // 鼠标悬停状态下的颜色,适用于按钮悬停时的背景色或边框色。
-    hover: `hsl(var(--${name}-600))`,
-    // 主色文本
-    text: `hsl(var(--${name}-500))`,
-    // 主色文本激活态
-    'text-active': `hsl(var(--${name}-700))`,
-    // 主色文本悬浮态
-    'text-hover': `hsl(var(--${name}-600))`,
-  };
-}

+ 0 - 3
internal/tailwind-config/src/module.d.ts

@@ -1,3 +0,0 @@
-declare module '@tailwindcss/nesting' {
-  export default any;
-}

+ 0 - 53
internal/tailwind-config/src/plugins/entry.ts

@@ -1,53 +0,0 @@
-import plugin from 'tailwindcss/plugin.js';
-
-const enterAnimationPlugin = plugin(({ addUtilities }) => {
-  const maxChild = 5;
-  const utilities: Record<string, any> = {};
-  for (let i = 1; i <= maxChild; i++) {
-    const baseDelay = 0.1;
-    const delay = `${baseDelay * i}s`;
-
-    utilities[`.enter-x:nth-child(${i})`] = {
-      animation: `enter-x-animation 0.3s ease-in-out ${delay} forwards`,
-      opacity: '0',
-      transform: `translateX(50px)`,
-    };
-
-    utilities[`.enter-y:nth-child(${i})`] = {
-      animation: `enter-y-animation 0.3s ease-in-out ${delay} forwards`,
-      opacity: '0',
-      transform: `translateY(50px)`,
-    };
-
-    utilities[`.-enter-x:nth-child(${i})`] = {
-      animation: `enter-x-animation 0.3s ease-in-out ${delay} forwards`,
-      opacity: '0',
-      transform: `translateX(-50px)`,
-    };
-
-    utilities[`.-enter-y:nth-child(${i})`] = {
-      animation: `enter-y-animation 0.3s ease-in-out ${delay} forwards`,
-      opacity: '0',
-      transform: `translateY(-50px)`,
-    };
-  }
-
-  // 添加动画关键帧
-  addUtilities(utilities);
-  addUtilities({
-    '@keyframes enter-x-animation': {
-      to: {
-        opacity: '1',
-        transform: 'translateX(0)',
-      },
-    },
-    '@keyframes enter-y-animation': {
-      to: {
-        opacity: '1',
-        transform: 'translateY(0)',
-      },
-    },
-  });
-});
-
-export { enterAnimationPlugin };

+ 0 - 15
internal/tailwind-config/src/postcss.config.ts

@@ -1,15 +0,0 @@
-import config from '.';
-
-export default {
-  plugins: {
-    ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
-    // Specifying the config is not necessary in most cases, but it is included
-    autoprefixer: {},
-    // 修复 element-plus 和 ant-design-vue 的样式和tailwindcss冲突问题
-    'postcss-antd-fixes': { prefixes: ['ant', 'el'] },
-    'postcss-import': {},
-    'postcss-preset-env': {},
-    tailwindcss: { config },
-    'tailwindcss/nesting': {},
-  },
-};

+ 0 - 6
internal/tailwind-config/tsconfig.json

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

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

@@ -29,6 +29,7 @@
   "dependencies": {
     "@intlify/unplugin-vue-i18n": "catalog:",
     "@jspm/generator": "catalog:",
+    "@tailwindcss/vite": "catalog:",
     "archiver": "catalog:",
     "cheerio": "catalog:",
     "get-port": "catalog:",

+ 4 - 0
internal/vite-config/src/plugins/index.ts

@@ -8,6 +8,7 @@ import type {
 } from '../typing';
 
 import viteVueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
+import tailwindcss from '@tailwindcss/vite';
 import viteVue from '@vitejs/plugin-vue';
 import viteVueJsx from '@vitejs/plugin-vue-jsx';
 import { visualizer as viteVisualizerPlugin } from 'rollup-plugin-visualizer';
@@ -25,6 +26,7 @@ import { viteMetadataPlugin } from './inject-metadata';
 import { viteLicensePlugin } from './license';
 import { viteNitroMockPlugin } from './nitro-mock';
 import { vitePrintPlugin } from './print';
+import { viteTailwindReferencePlugin } from './tailwind-reference';
 import { viteVxeTableImportsPlugin } from './vxe-table';
 
 /**
@@ -60,6 +62,8 @@ async function loadCommonPlugins(
           },
         }),
         viteVueJsx(),
+        viteTailwindReferencePlugin(),
+        tailwindcss(),
       ],
     },
 

+ 40 - 0
internal/vite-config/src/plugins/tailwind-reference.ts

@@ -0,0 +1,40 @@
+import type { Plugin } from 'vite';
+
+const REFERENCE_LINE = '@reference "@vben-core/design/theme";\n';
+
+/**
+ * Auto-inject @reference into Vue SFC <style> blocks that use @apply.
+ *
+ * In Tailwind CSS v4, each Vue SFC <style scoped> block is processed as an
+ * independent CSS module. If a style block uses @apply with custom theme
+ * utilities (e.g. bg-primary, text-foreground), it needs access to the
+ * @theme definition via @reference. This plugin auto-injects it so
+ * individual components don't need to add it manually.
+ */
+export function viteTailwindReferencePlugin(): Plugin {
+  return {
+    enforce: 'pre',
+    name: 'vite:tailwind-reference',
+    transform(code, id) {
+      // Only process Vue SFC style blocks
+      if (!id.includes('.vue')) {
+        return null;
+      }
+      if (!id.includes('type=style')) {
+        return null;
+      }
+      // Skip if already has @reference
+      if (code.includes('@reference')) {
+        return null;
+      }
+      // Only inject if the style block uses @apply
+      if (!code.includes('@apply')) {
+        return null;
+      }
+      return {
+        code: REFERENCE_LINE + code,
+        map: null,
+      };
+    },
+  };
+}

+ 1 - 3
package.json

@@ -72,7 +72,6 @@
     "@vben/eslint-config": "workspace:*",
     "@vben/prettier-config": "workspace:*",
     "@vben/stylelint-config": "workspace:*",
-    "@vben/tailwind-config": "workspace:*",
     "@vben/tsconfig": "workspace:*",
     "@vben/turbo-run": "workspace:*",
     "@vben/vite-config": "workspace:*",
@@ -80,7 +79,6 @@
     "@vitejs/plugin-vue": "catalog:",
     "@vitejs/plugin-vue-jsx": "catalog:",
     "@vue/test-utils": "catalog:",
-    "autoprefixer": "catalog:",
     "cross-env": "catalog:",
     "cspell": "catalog:",
     "happy-dom": "catalog:",
@@ -101,5 +99,5 @@
     "node": ">=20.19.0",
     "pnpm": ">=10.0.0"
   },
-  "packageManager": "pnpm@10.28.2"
+  "packageManager": "pnpm@10.30.3"
 }

+ 9 - 0
packages/@core/base/design/package.json

@@ -25,6 +25,9 @@
       "development": "./src/scss-bem/bem.scss",
       "default": "./dist/bem.scss"
     },
+    "./theme": {
+      "default": "./src/css/global.css"
+    },
     ".": {
       "types": "./src/index.ts",
       "development": "./src/index.ts",
@@ -37,5 +40,11 @@
         "default": "./dist/index.mjs"
       }
     }
+  },
+  "dependencies": {
+    "@iconify/json": "catalog:",
+    "@iconify/tailwind4": "catalog:",
+    "@tailwindcss/typography": "catalog:",
+    "tw-animate-css": "catalog:"
   }
 }

+ 457 - 54
packages/@core/base/design/src/css/global.css

@@ -1,12 +1,294 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
+@import 'tailwindcss';
+@import 'tw-animate-css';
+
+@plugin '@tailwindcss/typography';
+@plugin '@iconify/tailwind4';
+
+/* Monorepo source detection: scan all packages and apps for utility classes */
+@source '../../../../../../packages/';
+@source '../../../../../../apps/';
+@source '../../../../../../docs/';
+@source '../../../../../../playground/';
+
+/* Dark mode uses .dark class selector, not prefers-color-scheme */
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+  /* Font */
+  --font-sans: var(--font-family);
+
+  /* Border Radius */
+  --radius-sm: calc(var(--radius) - 4px);
+  --radius-md: calc(var(--radius) - 2px);
+  --radius-lg: var(--radius);
+  --radius-xl: calc(var(--radius) + 4px);
+
+  /* Box Shadow */
+  --shadow-float:
+    0 6px 16px 0 rgb(0 0 0 / 8%), 0 3px 6px -4px rgb(0 0 0 / 12%),
+    0 9px 28px 8px rgb(0 0 0 / 5%);
+
+  /* Animations */
+  --animate-accordion-down: accordion-down 0.2s ease-out;
+  --animate-accordion-up: accordion-up 0.2s ease-out;
+  --animate-collapsible-down: collapsible-down 0.2s ease-in-out;
+  --animate-collapsible-up: collapsible-up 0.2s ease-in-out;
+  --animate-float: float 5s linear 0ms infinite;
+
+  /* ===== Semantic Colors (shadcn-ui) ===== */
+
+  --color-background: hsl(var(--background));
+  --color-background-deep: hsl(var(--background-deep));
+  --color-foreground: hsl(var(--foreground));
+  --color-card: hsl(var(--card));
+  --color-card-foreground: hsl(var(--card-foreground));
+  --color-popover: hsl(var(--popover));
+  --color-popover-foreground: hsl(var(--popover-foreground));
+  --color-muted: hsl(var(--muted));
+  --color-muted-foreground: hsl(var(--muted-foreground));
+  --color-accent: hsl(var(--accent));
+  --color-accent-foreground: hsl(var(--accent-foreground));
+  --color-accent-hover: hsl(var(--accent-hover));
+  --color-accent-lighter: hsl(var(--accent-lighter));
+  --color-border: hsl(var(--border));
+  --color-input: hsl(var(--input));
+  --color-input-background: hsl(var(--input-background));
+  --color-ring: hsl(var(--ring));
+  --color-secondary: hsl(var(--secondary));
+  --color-secondary-desc: hsl(var(--secondary-desc));
+  --color-secondary-foreground: hsl(var(--secondary-foreground));
+
+  /* ===== Custom Semantic Colors ===== */
+
+  --color-header: hsl(var(--header));
+  --color-heavy: hsl(var(--heavy));
+  --color-heavy-foreground: hsl(var(--heavy-foreground));
+  --color-main: hsl(var(--main));
+  --color-overlay: hsl(var(--overlay));
+  --color-overlay-content: hsl(var(--overlay-content));
+  --color-sidebar: hsl(var(--sidebar));
+  --color-sidebar-deep: hsl(var(--sidebar-deep));
+
+  /* ===== Primary Palette ===== */
+
+  --color-primary: hsl(var(--primary));
+  --color-primary-foreground: hsl(var(--primary-foreground));
+  --color-primary-50: hsl(var(--primary-50));
+  --color-primary-100: hsl(var(--primary-100));
+  --color-primary-200: hsl(var(--primary-200));
+  --color-primary-300: hsl(var(--primary-300));
+  --color-primary-400: hsl(var(--primary-400));
+  --color-primary-500: hsl(var(--primary-500));
+  --color-primary-600: hsl(var(--primary-600));
+  --color-primary-700: hsl(var(--primary-700));
+  --color-primary-active: hsl(var(--primary-700));
+  --color-primary-background-light: hsl(var(--primary-200));
+  --color-primary-background-lighter: hsl(var(--primary-100));
+  --color-primary-background-lightest: hsl(var(--primary-50));
+  --color-primary-border: hsl(var(--primary-400));
+  --color-primary-border-light: hsl(var(--primary-300));
+  --color-primary-hover: hsl(var(--primary-600));
+  --color-primary-text: hsl(var(--primary-500));
+  --color-primary-text-active: hsl(var(--primary-700));
+  --color-primary-text-hover: hsl(var(--primary-600));
+
+  /* ===== Destructive Palette ===== */
+
+  --color-destructive: hsl(var(--destructive));
+  --color-destructive-foreground: hsl(var(--destructive-foreground));
+  --color-destructive-50: hsl(var(--destructive-50));
+  --color-destructive-100: hsl(var(--destructive-100));
+  --color-destructive-200: hsl(var(--destructive-200));
+  --color-destructive-300: hsl(var(--destructive-300));
+  --color-destructive-400: hsl(var(--destructive-400));
+  --color-destructive-500: hsl(var(--destructive-500));
+  --color-destructive-600: hsl(var(--destructive-600));
+  --color-destructive-700: hsl(var(--destructive-700));
+  --color-destructive-active: hsl(var(--destructive-700));
+  --color-destructive-background-light: hsl(var(--destructive-200));
+  --color-destructive-background-lighter: hsl(var(--destructive-100));
+  --color-destructive-background-lightest: hsl(var(--destructive-50));
+  --color-destructive-border: hsl(var(--destructive-400));
+  --color-destructive-border-light: hsl(var(--destructive-300));
+  --color-destructive-hover: hsl(var(--destructive-600));
+  --color-destructive-text: hsl(var(--destructive-500));
+  --color-destructive-text-active: hsl(var(--destructive-700));
+  --color-destructive-text-hover: hsl(var(--destructive-600));
+
+  /* ===== Success Palette ===== */
+
+  --color-success: hsl(var(--success));
+  --color-success-foreground: hsl(var(--success-foreground));
+  --color-success-50: hsl(var(--success-50));
+  --color-success-100: hsl(var(--success-100));
+  --color-success-200: hsl(var(--success-200));
+  --color-success-300: hsl(var(--success-300));
+  --color-success-400: hsl(var(--success-400));
+  --color-success-500: hsl(var(--success-500));
+  --color-success-600: hsl(var(--success-600));
+  --color-success-700: hsl(var(--success-700));
+  --color-success-active: hsl(var(--success-700));
+  --color-success-background-light: hsl(var(--success-200));
+  --color-success-background-lighter: hsl(var(--success-100));
+  --color-success-background-lightest: hsl(var(--success-50));
+  --color-success-border: hsl(var(--success-400));
+  --color-success-border-light: hsl(var(--success-300));
+  --color-success-hover: hsl(var(--success-600));
+  --color-success-text: hsl(var(--success-500));
+  --color-success-text-active: hsl(var(--success-700));
+  --color-success-text-hover: hsl(var(--success-600));
+
+  /* ===== Warning Palette ===== */
+
+  --color-warning: hsl(var(--warning));
+  --color-warning-foreground: hsl(var(--warning-foreground));
+  --color-warning-50: hsl(var(--warning-50));
+  --color-warning-100: hsl(var(--warning-100));
+  --color-warning-200: hsl(var(--warning-200));
+  --color-warning-300: hsl(var(--warning-300));
+  --color-warning-400: hsl(var(--warning-400));
+  --color-warning-500: hsl(var(--warning-500));
+  --color-warning-600: hsl(var(--warning-600));
+  --color-warning-700: hsl(var(--warning-700));
+  --color-warning-active: hsl(var(--warning-700));
+  --color-warning-background-light: hsl(var(--warning-200));
+  --color-warning-background-lighter: hsl(var(--warning-100));
+  --color-warning-background-lightest: hsl(var(--warning-50));
+  --color-warning-border: hsl(var(--warning-400));
+  --color-warning-border-light: hsl(var(--warning-300));
+  --color-warning-hover: hsl(var(--warning-600));
+  --color-warning-text: hsl(var(--warning-500));
+  --color-warning-text-active: hsl(var(--warning-700));
+  --color-warning-text-hover: hsl(var(--warning-600));
+
+  /* ===== Green Palette (alias for success shades) ===== */
+
+  --color-green-50: hsl(var(--green-50));
+  --color-green-100: hsl(var(--green-100));
+  --color-green-200: hsl(var(--green-200));
+  --color-green-300: hsl(var(--green-300));
+  --color-green-400: hsl(var(--green-400));
+  --color-green-500: hsl(var(--green-500));
+  --color-green-600: hsl(var(--green-600));
+  --color-green-700: hsl(var(--green-700));
+  --color-green-active: hsl(var(--green-700));
+  --color-green-background-light: hsl(var(--green-200));
+  --color-green-background-lighter: hsl(var(--green-100));
+  --color-green-background-lightest: hsl(var(--green-50));
+  --color-green-border: hsl(var(--green-400));
+  --color-green-border-light: hsl(var(--green-300));
+  --color-green-foreground: hsl(var(--success-foreground));
+  --color-green-hover: hsl(var(--green-600));
+  --color-green-text: hsl(var(--green-500));
+  --color-green-text-active: hsl(var(--green-700));
+  --color-green-text-hover: hsl(var(--green-600));
+
+  /* ===== Red Palette (alias for destructive shades) ===== */
+
+  --color-red-50: hsl(var(--red-50));
+  --color-red-100: hsl(var(--red-100));
+  --color-red-200: hsl(var(--red-200));
+  --color-red-300: hsl(var(--red-300));
+  --color-red-400: hsl(var(--red-400));
+  --color-red-500: hsl(var(--red-500));
+  --color-red-600: hsl(var(--red-600));
+  --color-red-700: hsl(var(--red-700));
+  --color-red-active: hsl(var(--red-700));
+  --color-red-background-light: hsl(var(--red-200));
+  --color-red-background-lighter: hsl(var(--red-100));
+  --color-red-background-lightest: hsl(var(--red-50));
+  --color-red-border: hsl(var(--red-400));
+  --color-red-border-light: hsl(var(--red-300));
+  --color-red-foreground: hsl(var(--destructive-foreground));
+  --color-red-hover: hsl(var(--red-600));
+  --color-red-text: hsl(var(--red-500));
+  --color-red-text-active: hsl(var(--red-700));
+  --color-red-text-hover: hsl(var(--red-600));
+
+  /* ===== Yellow Palette (alias for warning shades) ===== */
+
+  --color-yellow-50: hsl(var(--yellow-50));
+  --color-yellow-100: hsl(var(--yellow-100));
+  --color-yellow-200: hsl(var(--yellow-200));
+  --color-yellow-300: hsl(var(--yellow-300));
+  --color-yellow-400: hsl(var(--yellow-400));
+  --color-yellow-500: hsl(var(--yellow-500));
+  --color-yellow-600: hsl(var(--yellow-600));
+  --color-yellow-700: hsl(var(--yellow-700));
+  --color-yellow-active: hsl(var(--yellow-700));
+  --color-yellow-background-light: hsl(var(--yellow-200));
+  --color-yellow-background-lighter: hsl(var(--yellow-100));
+  --color-yellow-background-lightest: hsl(var(--yellow-50));
+  --color-yellow-border: hsl(var(--yellow-400));
+  --color-yellow-border-light: hsl(var(--yellow-300));
+  --color-yellow-foreground: hsl(var(--warning-foreground));
+  --color-yellow-hover: hsl(var(--yellow-600));
+  --color-yellow-text: hsl(var(--yellow-500));
+  --color-yellow-text-active: hsl(var(--yellow-700));
+  --color-yellow-text-hover: hsl(var(--yellow-600));
+}
+
+/* Keyframes */
+@keyframes accordion-down {
+  from {
+    height: 0;
+  }
+
+  to {
+    height: var(--reka-accordion-content-height);
+  }
+}
+
+@keyframes accordion-up {
+  from {
+    height: var(--reka-accordion-content-height);
+  }
+
+  to {
+    height: 0;
+  }
+}
+
+@keyframes collapsible-down {
+  from {
+    height: 0;
+  }
+
+  to {
+    height: var(--reka-collapsible-content-height);
+  }
+}
+
+@keyframes collapsible-up {
+  from {
+    height: var(--reka-collapsible-content-height);
+  }
+
+  to {
+    height: 0;
+  }
+}
+
+@keyframes float {
+  0% {
+    transform: translateY(0);
+  }
+
+  50% {
+    transform: translateY(-20px);
+  }
+
+  100% {
+    transform: translateY(0);
+  }
+}
 
+/* Base styles */
 @layer base {
   *,
   ::after,
   ::before {
-    @apply border-border;
+    @apply border-border outline-ring/50;
 
     box-sizing: border-box;
     border-style: solid;
@@ -24,29 +306,16 @@
     text-rendering: optimizelegibility;
     text-size-adjust: 100%;
     -webkit-tap-highlight-color: transparent;
-
-    /* -webkit-font-smoothing: antialiased;
-    -moz-osx-font-smoothing: grayscale; */
   }
 
   #app,
   body,
   html {
     @apply size-full;
-
-    /* scrollbar-gutter: stable; */
   }
 
   body {
     min-height: 100vh;
-
-    /* pointer-events: auto !important; */
-
-    /* overflow: overlay; */
-
-    /* -webkit-font-smoothing: antialiased; */
-
-    /* -moz-osx-font-smoothing: grayscale; */
   }
 
   a,
@@ -63,19 +332,19 @@
   }
 
   ::view-transition-old(root) {
-    @apply z-[1];
+    @apply z-1;
   }
 
   ::view-transition-new(root) {
-    @apply z-[2147483646];
+    @apply z-2147483646;
   }
 
   html.dark::view-transition-old(root) {
-    @apply z-[2147483646];
+    @apply z-2147483646;
   }
 
   html.dark::view-transition-new(root) {
-    @apply z-[1];
+    @apply z-1;
   }
 
   input::placeholder,
@@ -83,18 +352,12 @@
     @apply opacity-100;
   }
 
-  /* input:-webkit-autofill {
-    @apply border-none;
-
-    box-shadow: 0 0 0 1000px transparent inset;
-  } */
-
   input[type='number']::-webkit-inner-spin-button,
   input[type='number']::-webkit-outer-spin-button {
     @apply m-0 appearance-none;
   }
 
-  /* 只有非mac下才进行调整,mac下使用默认滚动条 */
+  /* Only adjust scrollbar for non-macOS */
   html:not([data-platform='macOs']) {
     ::-webkit-scrollbar {
       @apply h-[10px] w-[10px];
@@ -114,44 +377,184 @@
   }
 }
 
-@layer components {
-  .flex-center {
-    @apply flex items-center justify-center;
-  }
+/* Custom utilities (v4 @utility syntax) */
+@utility flex-center {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
 
-  .flex-col-center {
-    @apply flex flex-col items-center justify-center;
-  }
+@utility flex-col-center {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
 
-  .outline-box {
-    @apply outline-border relative cursor-pointer rounded-md p-1 outline outline-1;
-  }
+/* Component styles (complex selectors, not convertible to @utility) */
+.outline-box {
+  @apply outline-border relative cursor-pointer rounded-md p-1 outline-1;
+}
 
-  .outline-box::after {
-    @apply absolute left-1/2 top-1/2 z-20 h-0 w-[1px] rounded-sm opacity-0 outline outline-2 outline-transparent transition-all duration-300 content-[""];
-  }
+.outline-box::after {
+  @apply absolute top-1/2 left-1/2 z-20 h-0 w-px rounded-sm opacity-0 outline-2 outline-transparent transition-all duration-300 content-[""];
+}
 
-  .outline-box.outline-box-active {
-    @apply outline-primary outline outline-2;
-  }
+.outline-box.outline-box-active {
+  @apply outline-primary outline-2;
+}
 
-  .outline-box.outline-box-active::after {
-    display: none;
-  }
+.outline-box.outline-box-active::after {
+  display: none;
+}
 
-  .outline-box:not(.outline-box-active):hover::after {
-    @apply outline-primary left-0 top-0 h-full w-full p-1 opacity-100;
-  }
+.outline-box:not(.outline-box-active):hover::after {
+  @apply outline-primary top-0 left-0 h-full w-full p-1 opacity-100;
+}
+
+.vben-link {
+  @apply text-primary hover:text-primary-hover active:text-primary-active cursor-pointer;
+}
+
+.card-box {
+  @apply bg-card text-card-foreground border-border rounded-xl border;
+}
 
-  .vben-link {
-    @apply text-primary hover:text-primary-hover active:text-primary-active cursor-pointer;
+/* Enter animations (converted from enterAnimationPlugin) */
+@keyframes enter-x-animation {
+  to {
+    opacity: 1;
+    transform: translateX(0);
   }
+}
 
-  .card-box {
-    @apply bg-card text-card-foreground border-border rounded-xl border;
+@keyframes enter-y-animation {
+  to {
+    opacity: 1;
+    transform: translateY(0);
   }
 }
 
+.enter-x:nth-child(1) {
+  opacity: 0;
+  transform: translateX(50px);
+  animation: enter-x-animation 0.3s ease-in-out 0.1s forwards;
+}
+
+.enter-x:nth-child(2) {
+  opacity: 0;
+  transform: translateX(50px);
+  animation: enter-x-animation 0.3s ease-in-out 0.2s forwards;
+}
+
+.enter-x:nth-child(3) {
+  opacity: 0;
+  transform: translateX(50px);
+  animation: enter-x-animation 0.3s ease-in-out 0.3s forwards;
+}
+
+.enter-x:nth-child(4) {
+  opacity: 0;
+  transform: translateX(50px);
+  animation: enter-x-animation 0.3s ease-in-out 0.4s forwards;
+}
+
+.enter-x:nth-child(5) {
+  opacity: 0;
+  transform: translateX(50px);
+  animation: enter-x-animation 0.3s ease-in-out 0.5s forwards;
+}
+
+.enter-y:nth-child(1) {
+  opacity: 0;
+  transform: translateY(50px);
+  animation: enter-y-animation 0.3s ease-in-out 0.1s forwards;
+}
+
+.enter-y:nth-child(2) {
+  opacity: 0;
+  transform: translateY(50px);
+  animation: enter-y-animation 0.3s ease-in-out 0.2s forwards;
+}
+
+.enter-y:nth-child(3) {
+  opacity: 0;
+  transform: translateY(50px);
+  animation: enter-y-animation 0.3s ease-in-out 0.3s forwards;
+}
+
+.enter-y:nth-child(4) {
+  opacity: 0;
+  transform: translateY(50px);
+  animation: enter-y-animation 0.3s ease-in-out 0.4s forwards;
+}
+
+.enter-y:nth-child(5) {
+  opacity: 0;
+  transform: translateY(50px);
+  animation: enter-y-animation 0.3s ease-in-out 0.5s forwards;
+}
+
+.-enter-x:nth-child(1) {
+  opacity: 0;
+  transform: translateX(-50px);
+  animation: enter-x-animation 0.3s ease-in-out 0.1s forwards;
+}
+
+.-enter-x:nth-child(2) {
+  opacity: 0;
+  transform: translateX(-50px);
+  animation: enter-x-animation 0.3s ease-in-out 0.2s forwards;
+}
+
+.-enter-x:nth-child(3) {
+  opacity: 0;
+  transform: translateX(-50px);
+  animation: enter-x-animation 0.3s ease-in-out 0.3s forwards;
+}
+
+.-enter-x:nth-child(4) {
+  opacity: 0;
+  transform: translateX(-50px);
+  animation: enter-x-animation 0.3s ease-in-out 0.4s forwards;
+}
+
+.-enter-x:nth-child(5) {
+  opacity: 0;
+  transform: translateX(-50px);
+  animation: enter-x-animation 0.3s ease-in-out 0.5s forwards;
+}
+
+.-enter-y:nth-child(1) {
+  opacity: 0;
+  transform: translateY(-50px);
+  animation: enter-y-animation 0.3s ease-in-out 0.1s forwards;
+}
+
+.-enter-y:nth-child(2) {
+  opacity: 0;
+  transform: translateY(-50px);
+  animation: enter-y-animation 0.3s ease-in-out 0.2s forwards;
+}
+
+.-enter-y:nth-child(3) {
+  opacity: 0;
+  transform: translateY(-50px);
+  animation: enter-y-animation 0.3s ease-in-out 0.3s forwards;
+}
+
+.-enter-y:nth-child(4) {
+  opacity: 0;
+  transform: translateY(-50px);
+  animation: enter-y-animation 0.3s ease-in-out 0.4s forwards;
+}
+
+.-enter-y:nth-child(5) {
+  opacity: 0;
+  transform: translateY(-50px);
+  animation: enter-y-animation 0.3s ease-in-out 0.5s forwards;
+}
+
 html.invert-mode {
   @apply invert;
 }

+ 5 - 3
packages/@core/base/design/src/css/nprogress.css

@@ -1,10 +1,12 @@
+@reference "./global.css";
+
 /* Make clicks pass-through */
 #nprogress {
   @apply pointer-events-none;
 }
 
 #nprogress .bar {
-  @apply bg-primary fixed left-0 top-0 z-[1031] h-[2px] w-full;
+  @apply bg-primary fixed top-0 left-0 z-1031 h-[2px] w-full;
 }
 
 /* Fancy blur effect */
@@ -20,11 +22,11 @@
 
 /* Remove these to get rid of the spinner */
 #nprogress .spinner {
-  @apply fixed right-4 top-4 z-[1031] block;
+  @apply fixed top-4 right-4 z-1031 block;
 }
 
 #nprogress .spinner-icon {
-  @apply border-t-primary border-l-primary size-4 rounded-full border-[2px] border-solid border-transparent;
+  @apply border-t-primary border-l-primary size-4 rounded-full border-2 border-solid border-transparent;
 
   animation: nprogress-spinner 400ms linear infinite;
 }

+ 26 - 28
packages/@core/base/shared/src/utils/__tests__/resources.test.ts

@@ -1,10 +1,7 @@
-import { beforeEach, describe, expect, it } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
 
 import { loadScript } from '../resources';
 
-const testJsPath =
-  'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js';
-
 describe('loadScript', () => {
   beforeEach(() => {
     // 每个测试前清空 head,保证环境干净
@@ -12,18 +9,14 @@ describe('loadScript', () => {
   });
 
   it('should resolve when the script loads successfully', async () => {
-    const promise = loadScript(testJsPath);
+    // happy-dom v20+ auto-fires 'load' via handleDisabledFileLoadingAsSuccess
+    const promise = loadScript('/test-script.js');
 
-    // 此时脚本元素已被创建并插入
     const script = document.querySelector(
-      `script[src="${testJsPath}"]`,
+      'script[src="/test-script.js"]',
     ) as HTMLScriptElement;
     expect(script).toBeTruthy();
 
-    // 模拟加载成功
-    script.dispatchEvent(new Event('load'));
-
-    // 等待 promise resolve
     await expect(promise).resolves.toBeUndefined();
   });
 
@@ -45,37 +38,42 @@ describe('loadScript', () => {
   });
 
   it('should reject when the script fails to load', async () => {
+    let capturedScript: HTMLScriptElement | null = null;
+
+    // 拦截 append,捕获 script 元素但不插入 DOM,
+    // 防止 happy-dom v20+ 自动触发 load 事件
+    const appendSpy = vi
+      .spyOn(document.head, 'append')
+      .mockImplementation((...nodes) => {
+        for (const node of nodes) {
+          if (node instanceof HTMLScriptElement) {
+            capturedScript = node;
+          }
+        }
+      });
+
     const promise = loadScript('error.js');
 
-    const script = document.querySelector(
-      'script[src="error.js"]',
-    ) as HTMLScriptElement;
-    expect(script).toBeTruthy();
+    appendSpy.mockRestore();
 
-    // 模拟加载失败
-    script.dispatchEvent(new Event('error'));
+    expect(capturedScript).toBeTruthy();
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    capturedScript!.dispatchEvent(new Event('error'));
 
     await expect(promise).rejects.toThrow('Failed to load script: error.js');
   });
 
   it('should handle multiple concurrent calls and only insert one script tag', async () => {
-    const p1 = loadScript(testJsPath);
-    const p2 = loadScript(testJsPath);
-
-    const script = document.querySelector(
-      `script[src="${testJsPath}"]`,
-    ) as HTMLScriptElement;
-    expect(script).toBeTruthy();
-
-    // 触发一次 load,两个 promise 都应该 resolve
-    script.dispatchEvent(new Event('load'));
+    const p1 = loadScript('/test-script.js');
+    const p2 = loadScript('/test-script.js');
 
+    // happy-dom v20+ auto-fires 'load',两个 promise 都应该 resolve
     await expect(p1).resolves.toBeUndefined();
     await expect(p2).resolves.toBeUndefined();
 
     // 只插入一次
     const scripts = document.head.querySelectorAll(
-      `script[src="${testJsPath}"]`,
+      'script[src="/test-script.js"]',
     );
     expect(scripts).toHaveLength(1);
   });

+ 1 - 1
packages/@core/preferences/src/config.ts

@@ -20,8 +20,8 @@ const defaultPreferences: Preferences = {
     defaultHomePath: '/analytics',
     dynamicTitle: true,
     enableCheckUpdates: true,
-    enablePreferences: true,
     enableCopyPreferences: true,
+    enablePreferences: true,
     enableRefreshToken: false,
     enableStickyPreferencesNavigationBar: true,
     isMobile: false,

+ 0 - 1
packages/@core/ui-kit/form-ui/postcss.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config/postcss';

+ 10 - 13
packages/@core/ui-kit/form-ui/src/form-api.ts

@@ -75,19 +75,16 @@ export class FormApi {
 
     const defaultState = getDefaultState();
 
-    this.store = new Store<VbenFormProps>(
-      {
-        ...defaultState,
-        ...storeState,
-      },
-      {
-        onUpdate: () => {
-          this.prevState = this.state;
-          this.state = this.store.state;
-          this.updateState();
-        },
-      },
-    );
+    this.store = new Store<VbenFormProps>({
+      ...defaultState,
+      ...storeState,
+    });
+
+    this.store.subscribe((state) => {
+      this.prevState = this.state;
+      this.state = state;
+      this.updateState();
+    });
 
     this.state = this.store.state;
     this.stateHandler = new StateHandler();

+ 2 - 2
packages/@core/ui-kit/form-ui/src/form-render/form-field.vue

@@ -308,7 +308,7 @@ onUnmounted(() => {
           cn(
             'flex leading-6',
             {
-              'mr-2 flex-shrink-0 justify-end': !isVertical,
+              'mr-2 shrink-0 justify-end': !isVertical,
               'mb-1 flex-row': isVertical,
             },
             labelClass,
@@ -324,7 +324,7 @@ onUnmounted(() => {
           <VbenRenderContent :content="label" />
         </template>
       </FormLabel>
-      <div class="flex-auto overflow-hidden p-[1px]">
+      <div class="flex-auto overflow-hidden p-px">
         <div :class="cn('relative flex w-full items-center', wrapperClass)">
           <FormControl :class="cn(controlClass)">
             <slot

+ 1 - 1
packages/@core/ui-kit/form-ui/src/form-render/form.vue

@@ -157,7 +157,7 @@ const computedSchema = computed(
           ...schema.formFieldProps,
         },
         formItemClass: cn(
-          'flex-shrink-0',
+          'shrink-0',
           { hidden },
           formItemClass,
           resolvedSchemaFormItemClass,

+ 0 - 1
packages/@core/ui-kit/form-ui/tailwind.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';

+ 0 - 1
packages/@core/ui-kit/layout-ui/postcss.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config/postcss';

+ 2 - 2
packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue

@@ -307,7 +307,7 @@ onUnmounted(() => {
   <aside
     ref="asideRef"
     :style="style"
-    class="fixed left-0 top-0 h-full transition-all duration-150"
+    class="fixed top-0 left-0 h-full transition-all duration-150"
     @mouseenter="handleMouseenter"
     @mouseleave="handleMouseleave"
   >
@@ -374,7 +374,7 @@ onUnmounted(() => {
     <div
       v-if="draggable"
       ref="dragBarRef"
-      class="absolute inset-y-0 -right-[1px] z-1000 w-[2px] cursor-col-resize hover:bg-primary"
+      class="absolute inset-y-0 -right-px z-1000 w-[2px] cursor-col-resize hover:bg-primary"
       @mousedown="handleDragSidebar"
     ></div>
   </aside>

+ 1 - 1
packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-collapse-button.vue

@@ -10,7 +10,7 @@ function handleCollapsed() {
 
 <template>
   <div
-    class="flex-center absolute bottom-2 left-3 z-10 cursor-pointer rounded-sm bg-accent p-1 text-foreground/60 hover:bg-accent-hover hover:text-foreground"
+    class="absolute bottom-2 left-3 z-10 flex-center cursor-pointer rounded-sm bg-accent p-1 text-foreground/60 hover:bg-accent-hover hover:text-foreground"
     @click.stop="handleCollapsed"
   >
     <ChevronsRight v-if="collapsed" class="size-4" />

+ 1 - 1
packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-fixed-button.vue

@@ -10,7 +10,7 @@ function toggleFixed() {
 
 <template>
   <div
-    class="flex-center absolute bottom-2 right-3 z-10 cursor-pointer rounded-sm bg-accent p-[5px] text-foreground/60 transition-all duration-300 hover:bg-accent-hover hover:text-foreground"
+    class="absolute right-3 bottom-2 z-10 flex-center cursor-pointer rounded-sm bg-accent p-[5px] text-foreground/60 transition-all duration-300 hover:bg-accent-hover hover:text-foreground"
     @click="toggleFixed"
   >
     <PinOff v-if="!expandOnHover" class="size-3.5" />

+ 2 - 2
packages/@core/ui-kit/layout-ui/src/vben-layout.vue

@@ -275,7 +275,7 @@ const mainStyle = computed(() => {
 
 // 计算 tabbar 的样式
 const tabbarStyle = computed((): CSSProperties => {
-  let width = '';
+  let width: string;
   let marginLeft = 0;
 
   // 如果不是混合导航,tabbar 的宽度为 100%
@@ -627,7 +627,7 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
     <div
       v-if="maskVisible"
       :style="maskStyle"
-      class="fixed left-0 top-0 h-full w-full bg-overlay transition-[background-color] duration-200"
+      class="fixed top-0 left-0 size-full bg-overlay transition-[background-color] duration-200"
       @click="handleClickMask"
     ></div>
   </div>

+ 0 - 1
packages/@core/ui-kit/layout-ui/tailwind.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';

+ 1 - 0
packages/@core/ui-kit/menu-ui/package.json

@@ -38,6 +38,7 @@
   },
   "dependencies": {
     "@vben-core/composables": "workspace:*",
+    "@vben-core/design": "workspace:*",
     "@vben-core/icons": "workspace:*",
     "@vben-core/shadcn-ui": "workspace:*",
     "@vben-core/shared": "workspace:*",

+ 0 - 1
packages/@core/ui-kit/menu-ui/postcss.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config/postcss';

+ 1 - 1
packages/@core/ui-kit/menu-ui/src/components/menu-badge-dot.vue

@@ -16,7 +16,7 @@ withDefaults(defineProps<Props>(), {
     <span
       :class="dotClass"
       :style="dotStyle"
-      class="absolute inline-flex h-full w-full animate-ping rounded-full opacity-75"
+      class="absolute inline-flex size-full animate-ping rounded-full opacity-75"
     >
     </span>
     <span

+ 3 - 1
packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue

@@ -60,6 +60,8 @@ function menuIcon(menu: MenuRecordRaw) {
 <style lang="scss" scoped>
 $namespace: vben;
 
+@reference "@vben-core/design/theme";
+
 .#{$namespace}-normal-menu {
   --menu-item-margin-y: 4px;
   --menu-item-margin-x: 0px;
@@ -129,7 +131,7 @@ $namespace: vben;
 
       .#{$namespace}-normal-menu__name,
       .#{$namespace}-normal-menu__icon {
-        @apply font-semibold text-primary-foreground;
+        @apply text-primary-foreground font-semibold;
       }
     }
 

+ 0 - 1
packages/@core/ui-kit/menu-ui/tailwind.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';

+ 0 - 1
packages/@core/ui-kit/popup-ui/postcss.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config/postcss';

+ 1 - 1
packages/@core/ui-kit/popup-ui/src/alert/alert.vue

@@ -147,7 +147,7 @@ async function handleOpenChange(val: boolean) {
       :class="
         cn(
           containerClass,
-          'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:w-[520px] sm:max-w-[80%] sm:rounded-[var(--radius)]',
+          'inset-x-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:w-[520px] sm:max-w-[80%] sm:rounded-(--radius)',
           {
             'border border-border': bordered,
             'shadow-3xl': !bordered,

+ 8 - 9
packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts

@@ -13,21 +13,20 @@ vi.mock('@vben-core/shared/store', () => {
         return this._state;
       }
       private _state: DrawerState;
+      private subscribers: Array<(state: DrawerState) => void> = [];
 
-      private options: any;
-
-      constructor(initialState: DrawerState, options: any) {
+      constructor(initialState: DrawerState) {
         this._state = initialState;
-        this.options = options;
-      }
-
-      batch(cb: () => void) {
-        cb();
       }
 
       setState(fn: (prev: DrawerState) => DrawerState) {
         this._state = fn(this._state);
-        this.options.onUpdate();
+        this.subscribers.forEach((sub) => sub(this._state));
+      }
+
+      subscribe(fn: (state: DrawerState) => void) {
+        this.subscribers.push(fn);
+        return { unsubscribe: () => {} };
       }
     },
   };

+ 12 - 17
packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts

@@ -56,23 +56,18 @@ export class DrawerApi {
       title: '',
     };
 
-    this.store = new Store<DrawerState>(
-      {
-        ...defaultState,
-        ...storeState,
-      },
-      {
-        onUpdate: () => {
-          const state = this.store.state;
-          if (state?.isOpen === this.state?.isOpen) {
-            this.state = state;
-          } else {
-            this.state = state;
-            this.api.onOpenChange?.(!!state?.isOpen);
-          }
-        },
-      },
-    );
+    this.store = new Store<DrawerState>({
+      ...defaultState,
+      ...storeState,
+    });
+
+    this.store.subscribe((state) => {
+      const prevIsOpen = this.state?.isOpen;
+      this.state = state;
+      if (state?.isOpen !== prevIsOpen) {
+        this.api.onOpenChange?.(!!state?.isOpen);
+      }
+    });
     this.state = this.store.state;
     this.api = {
       onBeforeClose,

+ 6 - 6
packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue

@@ -186,8 +186,8 @@ const getForceMount = computed(() => {
       :append-to="getAppendTo"
       :class="
         cn('flex w-[520px] flex-col', drawerClass, {
-          '!w-full': isMobile || placement === 'bottom' || placement === 'top',
-          'max-h-[100vh]': placement === 'bottom' || placement === 'top',
+          'w-full!': isMobile || placement === 'bottom' || placement === 'top',
+          'max-h-screen': placement === 'bottom' || placement === 'top',
           hidden: isClosed,
         })
       "
@@ -210,7 +210,7 @@ const getForceMount = computed(() => {
         v-if="showHeader"
         :class="
           cn(
-            '!flex flex-row items-center justify-between border-b px-6 py-5',
+            'flex! flex-row items-center justify-between border-b px-6 py-5',
             headerClass,
             {
               'px-4 py-3': closable,
@@ -224,7 +224,7 @@ const getForceMount = computed(() => {
             v-if="closable && closeIconPlacement === 'left'"
             as-child
             :disabled="submitting"
-            class="ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-secondary"
+            class="ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary"
           >
             <slot name="close-icon">
               <VbenIconButton>
@@ -234,7 +234,7 @@ const getForceMount = computed(() => {
           </SheetClose>
           <Separator
             v-if="closable && closeIconPlacement === 'left'"
-            class="ml-1 mr-2 h-8"
+            class="mr-2 ml-1 h-8"
             decorative
             orientation="vertical"
           />
@@ -265,7 +265,7 @@ const getForceMount = computed(() => {
             v-if="closable && closeIconPlacement === 'right'"
             as-child
             :disabled="submitting"
-            class="ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-secondary"
+            class="ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary"
           >
             <slot name="close-icon">
               <VbenIconButton>

+ 8 - 9
packages/@core/ui-kit/popup-ui/src/modal/__tests__/modal-api.test.ts

@@ -12,21 +12,20 @@ vi.mock('@vben-core/shared/store', () => {
         return this._state;
       }
       private _state: ModalState;
+      private subscribers: Array<(state: ModalState) => void> = [];
 
-      private options: any;
-
-      constructor(initialState: ModalState, options: any) {
+      constructor(initialState: ModalState) {
         this._state = initialState;
-        this.options = options;
-      }
-
-      batch(cb: () => void) {
-        cb();
       }
 
       setState(fn: (prev: ModalState) => ModalState) {
         this._state = fn(this._state);
-        this.options.onUpdate();
+        this.subscribers.forEach((sub) => sub(this._state));
+      }
+
+      subscribe(fn: (state: ModalState) => void) {
+        this.subscribers.push(fn);
+        return { unsubscribe: () => {} };
       }
     },
   };

+ 13 - 19
packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts

@@ -62,25 +62,19 @@ export class ModalApi {
       animationType: 'slide',
     };
 
-    this.store = new Store<ModalState>(
-      {
-        ...defaultState,
-        ...storeState,
-      },
-      {
-        onUpdate: () => {
-          const state = this.store.state;
-
-          // 每次更新状态时,都会调用 onOpenChange 回调函数
-          if (state?.isOpen === this.state?.isOpen) {
-            this.state = state;
-          } else {
-            this.state = state;
-            this.api.onOpenChange?.(!!state?.isOpen);
-          }
-        },
-      },
-    );
+    this.store = new Store<ModalState>({
+      ...defaultState,
+      ...storeState,
+    });
+
+    this.store.subscribe((state) => {
+      // 每次更新状态时,都会调用 onOpenChange 回调函数
+      const prevIsOpen = this.state?.isOpen;
+      this.state = state;
+      if (state?.isOpen !== prevIsOpen) {
+        this.api.onOpenChange?.(!!state?.isOpen);
+      }
+    });
 
     this.state = this.store.state;
 

+ 4 - 5
packages/@core/ui-kit/popup-ui/src/modal/modal.vue

@@ -240,14 +240,13 @@ function handleClosed() {
       :append-to="getAppendTo"
       :class="
         cn(
-          'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0',
-          shouldFullscreen ? 'sm:rounded-none' : 'sm:rounded-[var(--radius)]',
+          'inset-x-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0',
+          shouldFullscreen ? 'sm:rounded-none' : 'sm:rounded-(--radius)',
           modalClass,
           {
             'border border-border': bordered,
             'shadow-3xl': !bordered,
-            'left-0 top-0 size-full max-h-full !translate-x-0 !translate-y-0':
-              shouldFullscreen,
+            'top-0 left-0 size-full max-h-full translate-0!': shouldFullscreen,
             'top-1/2': centered && !shouldFullscreen,
             'duration-300': !dragging,
             hidden: isClosed,
@@ -320,7 +319,7 @@ function handleClosed() {
       <VbenLoading v-if="showLoading || submitting" spinning />
       <VbenIconButton
         v-if="fullscreenButton"
-        class="flex-center absolute right-10 top-3 hidden size-6 rounded-full px-1 text-lg text-foreground/80 opacity-70 transition-opacity hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus:outline-none disabled:pointer-events-none sm:block"
+        class="absolute top-3 right-10 flex-center hidden size-6 rounded-full px-1 text-lg text-foreground/80 opacity-70 transition-opacity hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus:outline-hidden disabled:pointer-events-none sm:block"
         @click="handleFullscreen"
       >
         <Shrink v-if="fullscreen" class="size-3.5" />

+ 0 - 1
packages/@core/ui-kit/popup-ui/tailwind.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';

+ 1 - 2
packages/@core/ui-kit/shadcn-ui/components.json

@@ -3,12 +3,11 @@
   "style": "new-york",
   "typescript": true,
   "tailwind": {
-    "config": "tailwind.config.mjs",
+    "config": "",
     "css": "src/assets/index.css",
     "baseColor": "slate",
     "cssVariables": true
   },
-  "framework": "vite",
   "aliases": {
     "components": "@vben-core/shadcn-ui/components",
     "utils": "@vben-core/shared/utils"

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

@@ -41,6 +41,7 @@
   },
   "dependencies": {
     "@vben-core/composables": "workspace:*",
+    "@vben-core/design": "workspace:*",
     "@vben-core/icons": "workspace:*",
     "@vben-core/shared": "workspace:*",
     "@vben-core/typings": "workspace:*",

+ 0 - 1
packages/@core/ui-kit/shadcn-ui/postcss.config.mjs

@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config/postcss';

+ 2 - 2
packages/@core/ui-kit/shadcn-ui/src/components/avatar/avatar.vue

@@ -60,7 +60,7 @@ const rootStyle = computed(() => {
   <div
     :class="props.class"
     :style="rootStyle"
-    class="relative flex flex-shrink-0 items-center"
+    class="relative flex shrink-0 items-center"
   >
     <Avatar :class="props.class" class="size-full">
       <AvatarImage :alt="alt" :src="src" :style="imageStyle" />
@@ -69,7 +69,7 @@ const rootStyle = computed(() => {
     <span
       v-if="dot"
       :class="dotClass"
-      class="absolute bottom-0 right-0 size-3 rounded-full border-2 border-background"
+      class="border-background absolute right-0 bottom-0 size-3 rounded-full border-2"
     >
     </span>
   </div>

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/components/back-top/back-top.vue

@@ -32,7 +32,7 @@ const { handleClick, visible } = useBackTop(props);
     <VbenButton
       v-if="visible"
       :style="backTopStyle"
-      class="data z-popup fixed bottom-10 size-10 rounded-full bg-background shadow-float duration-500 hover:bg-heavy dark:bg-accent dark:hover:bg-heavy"
+      class="data z-popup bg-background shadow-float hover:bg-heavy dark:bg-accent dark:hover:bg-heavy fixed bottom-10 size-10 rounded-full duration-500"
       size="icon"
       variant="icon"
       @click="handleClick"

+ 7 - 5
packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/breadcrumb-background.vue

@@ -33,11 +33,11 @@ function handleClick(index: number, path?: string) {
               <VbenIcon
                 v-if="showIcon"
                 :icon="item.icon"
-                class="mr-1 size-4 flex-shrink-0"
+                class="mr-1 size-4 shrink-0"
               />
               <span
                 :class="{
-                  'font-normal text-foreground':
+                  'text-foreground font-normal':
                     index === breadcrumbs.length - 1,
                 }"
                 >{{ item.title }}
@@ -50,12 +50,14 @@ function handleClick(index: number, path?: string) {
   </ul>
 </template>
 <style scoped>
+@reference "@vben-core/design/theme";
+
 li {
   @apply h-7;
 }
 
 li a {
-  @apply relative mr-9 flex h-7 items-center bg-accent py-0 pl-[5px] pr-2 text-[13px] text-muted-foreground;
+  @apply bg-accent text-muted-foreground relative mr-9 flex h-7 items-center py-0 pr-2 pl-[5px] text-[13px];
 }
 
 li a > span {
@@ -84,7 +86,7 @@ li:last-child a::after {
 
 li a::before,
 li a::after {
-  @apply absolute top-0 h-0 w-0 border-[.875rem] border-solid border-accent content-[''];
+  @apply border-accent absolute top-0 h-0 w-0 border-[.875rem] border-solid content-[''];
 }
 
 li a::before {
@@ -92,7 +94,7 @@ li a::before {
 }
 
 li a::after {
-  @apply left-full border-transparent border-l-accent;
+  @apply border-l-accent left-full border-transparent;
 }
 
 li:not(:last-child) a:hover {

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/components/button/button.vue

@@ -35,7 +35,7 @@ const isDisabled = computed(() => {
   >
     <LoaderCircle
       v-if="loading"
-      class="text-md mr-2 size-4 flex-shrink-0 animate-spin"
+      class="text-md mr-2 size-4 shrink-0 animate-spin"
     />
     <slot></slot>
   </Primitive>

Някои файлове не бяха показани, защото твърде много файлове са промени