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 componentspull/336/head
parent
aa7d8630b5
commit
a4736a49f8
|
|
@ -50,3 +50,10 @@ vite.config.ts.*
|
|||
*.sw?
|
||||
.history
|
||||
.cursor
|
||||
|
||||
# AI
|
||||
.agent
|
||||
.agents
|
||||
.claude
|
||||
.codex
|
||||
skills-lock.json
|
||||
|
|
|
|||
1
.npmrc
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/*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,3 +2,7 @@ dist
|
|||
public
|
||||
__tests__
|
||||
coverage
|
||||
.codex
|
||||
.claude
|
||||
.agent
|
||||
.agents
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`。
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config/postcss';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config/postcss';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config/postcss';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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="访问来源">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config/postcss';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config/postcss';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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="访问来源">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config';
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,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
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@nolebase/vitepress-plugin-git-changelog": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@vben/vite-config": "workspace:*",
|
||||
"@vite-pwa/vitepress": "catalog:",
|
||||
"vitepress": "catalog:",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
],
|
||||
};
|
||||
|
|
@ -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:"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ export async function ignores(): Promise<Linter.Config[]> {
|
|||
'**/*.woff',
|
||||
'**/.github',
|
||||
'**/lefthook.yml',
|
||||
|
||||
'**/.agent/**',
|
||||
'**/.agents/**',
|
||||
'**/.codex/**',
|
||||
'**/.claude/**',
|
||||
'**/.cursor/**',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ export async function node(): Promise<Linter.Config[]> {
|
|||
'vitest',
|
||||
'vite',
|
||||
'@vue/test-utils',
|
||||
'@vben/tailwind-config',
|
||||
'@playwright/test',
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,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 内部组件是自动生成的,不做太多限制
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ export default {
|
|||
},
|
||||
},
|
||||
],
|
||||
plugins: ['prettier-plugin-tailwindcss'],
|
||||
printWidth: 80,
|
||||
proseWrap: 'never',
|
||||
semi: true,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"prettier": "catalog:",
|
||||
"prettier-plugin-tailwindcss": "catalog:"
|
||||
"prettier": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: ['src/index', './src/postcss.config'],
|
||||
rollup: {
|
||||
emitCJS: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -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:"
|
||||
}
|
||||
}
|
||||
|
|
@ -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))`,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
declare module '@tailwindcss/nesting' {
|
||||
export default any;
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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': {},
|
||||
},
|
||||
};
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/node.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
"dependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "catalog:",
|
||||
"@jspm/generator": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"archiver": "catalog:",
|
||||
"cheerio": "catalog:",
|
||||
"get-port": "catalog:",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,25 +377,31 @@
|
|||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
/* Component styles (complex selectors, not convertible to @utility) */
|
||||
.outline-box {
|
||||
@apply outline-border relative cursor-pointer rounded-md p-1 outline outline-1;
|
||||
@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-[""];
|
||||
@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;
|
||||
@apply outline-primary outline-2;
|
||||
}
|
||||
|
||||
.outline-box.outline-box-active::after {
|
||||
|
|
@ -140,7 +409,7 @@
|
|||
}
|
||||
|
||||
.outline-box:not(.outline-box-active):hover::after {
|
||||
@apply outline-primary left-0 top-0 h-full w-full p-1 opacity-100;
|
||||
@apply outline-primary top-0 left-0 h-full w-full p-1 opacity-100;
|
||||
}
|
||||
|
||||
.vben-link {
|
||||
|
|
@ -150,6 +419,140 @@
|
|||
.card-box {
|
||||
@apply bg-card text-card-foreground border-border rounded-xl border;
|
||||
}
|
||||
|
||||
/* Enter animations (converted from enterAnimationPlugin) */
|
||||
@keyframes enter-x-animation {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config/postcss';
|
||||
|
|
@ -75,19 +75,16 @@ export class FormApi {
|
|||
|
||||
const defaultState = getDefaultState();
|
||||
|
||||
this.store = new Store<VbenFormProps>(
|
||||
{
|
||||
this.store = new Store<VbenFormProps>({
|
||||
...defaultState,
|
||||
...storeState,
|
||||
},
|
||||
{
|
||||
onUpdate: () => {
|
||||
});
|
||||
|
||||
this.store.subscribe((state) => {
|
||||
this.prevState = this.state;
|
||||
this.state = this.store.state;
|
||||
this.state = state;
|
||||
this.updateState();
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
this.state = this.store.state;
|
||||
this.stateHandler = new StateHandler();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ const computedSchema = computed(
|
|||
...schema.formFieldProps,
|
||||
},
|
||||
formItemClass: cn(
|
||||
'flex-shrink-0',
|
||||
'shrink-0',
|
||||
{ hidden },
|
||||
formItemClass,
|
||||
resolvedSchemaFormItemClass,
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config/postcss';
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config';
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config/postcss';
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config/postcss';
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: () => {} };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -56,23 +56,18 @@ export class DrawerApi {
|
|||
title: '',
|
||||
};
|
||||
|
||||
this.store = new Store<DrawerState>(
|
||||
{
|
||||
this.store = new Store<DrawerState>({
|
||||
...defaultState,
|
||||
...storeState,
|
||||
},
|
||||
{
|
||||
onUpdate: () => {
|
||||
const state = this.store.state;
|
||||
if (state?.isOpen === this.state?.isOpen) {
|
||||
this.state = state;
|
||||
} else {
|
||||
});
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: () => {} };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -62,25 +62,19 @@ export class ModalApi {
|
|||
animationType: 'slide',
|
||||
};
|
||||
|
||||
this.store = new Store<ModalState>(
|
||||
{
|
||||
this.store = new Store<ModalState>({
|
||||
...defaultState,
|
||||
...storeState,
|
||||
},
|
||||
{
|
||||
onUpdate: () => {
|
||||
const state = this.store.state;
|
||||
});
|
||||
|
||||
this.store.subscribe((state) => {
|
||||
// 每次更新状态时,都会调用 onOpenChange 回调函数
|
||||
if (state?.isOpen === this.state?.isOpen) {
|
||||
this.state = state;
|
||||
} else {
|
||||
const prevIsOpen = this.state?.isOpen;
|
||||
this.state = state;
|
||||
if (state?.isOpen !== prevIsOpen) {
|
||||
this.api.onOpenChange?.(!!state?.isOpen);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
this.state = this.store.state;
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config';
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@vben/tailwind-config/postcss';
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue