From 859acc7b71f5b87001a965d86b5ecb6b679ab3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E6=B5=B7=E6=B4=8B?= Date: Thu, 28 May 2026 14:18:56 +0800 Subject: [PATCH] first commit --- CONVENTIONS.md | 615 +++++++++++++++++++++++++++++++++++++++++++++++++ DESIGN_SPEC.md | 464 +++++++++++++++++++++++++++++++++++++ README.md | 0 opencode.json | 24 ++ 4 files changed, 1103 insertions(+) create mode 100644 CONVENTIONS.md create mode 100644 DESIGN_SPEC.md create mode 100644 README.md create mode 100644 opencode.json diff --git a/CONVENTIONS.md b/CONVENTIONS.md new file mode 100644 index 0000000..0ce53db --- /dev/null +++ b/CONVENTIONS.md @@ -0,0 +1,615 @@ +# Nova 项目约定 (Project Conventions) + +> dumi + father 技术栈的 AI-Native 组件库工程规范。 +> 所有配置在此预先约定,AI 生成代码时按此执行。 + +--- + +## 一、技术栈确认 + +| 领域 | 方案 | 版本 | +|------|------|------| +| 包管理 | pnpm | >= 9 | +| Monorepo | pnpm workspace | — | +| 文档站点 | dumi | 2.x | +| 组件构建 | father | 4.x | +| 测试 | Vitest + @testing-library/react | — | +| Lint | ESLint + Prettier | — | +| Git Hooks | husky + lint-staged | — | +| 版本管理 | Changesets | — | +| 样式方案 | **CSS Modules** | — | +| 语言 | TypeScript (strict) | 5.x | + +### 为什么选 CSS Modules? + +| 方案 | dumi 兼容 | father 兼容 | AI 友好度 | 运行时开销 | +|------|:---------:|:-----------:|:---------:|:---------:| +| CSS Modules | ✅ 原生 | ✅ 原生 | ★★★ (import styles 清晰) | 0 | +| vanilla-extract | ⚠️ 需插件 | ⚠️ 需插件 | ★★ (编译时,调试困难) | 0 | +| styled-components | ✅ | ⚠️ 需配置 | ★ (模板字符串 AI 常出错) | 有 | +| Tailwind | ✅ | ✅ | ★ (类名太长 AI 难控制) | 0 | + +**结论:CSS Modules 零配置、零运行时、AI 最可控。** + +--- + +## 二、项目结构 + +``` +novaui/ +├── .husky/ +│ ├── pre-commit # lint-staged +│ └── commit-msg # commitlint +├── .github/ +│ └── workflows/ +│ ├── ci.yml # lint + test + build +│ └── release.yml # 发布 npm +├── docs/ # dumi 站点文档 +│ ├── index.md # 首页 +│ ├── guide/ +│ │ ├── getting-started.md +│ │ ├── theme.md +│ │ └── faq.md +│ └── components/ # 组件概览页 +│ └── index.md +├── src/ +│ ├── components/ +│ │ ├── Button/ +│ │ │ ├── index.ts +│ │ │ ├── Button.tsx +│ │ │ ├── Button.module.css # ← CSS Modules +│ │ │ ├── Button.test.tsx +│ │ │ └── index.md # ← dumi 文档(关键) +│ │ └── ... +│ ├── hooks/ +│ ├── theme/ +│ │ ├── tokens.ts +│ │ ├── dark.css +│ │ └── global.css +│ ├── charts/ # 可视化组件 +│ │ ├── LineChart/ +│ │ └── ... +│ └── index.ts # 按需导出不做 barrel export +├── .dumirc.ts # dumi 配置 +├── .fatherrc.ts # father 配置 +├── vitest.config.ts +├── tsconfig.json +├── .eslintrc.cjs +├── .prettierrc +├── .commitlintrc.cjs +├── package.json +├── pnpm-workspace.yaml +├── .npmrc +└── README.md +``` + +--- + +## 三、dumi 配置约定 + +### 3.1 `.dumirc.ts` + +```ts +import { defineConfig } from 'dumi' + +export default defineConfig({ + themeConfig: { + name: 'Nova', + logo: '/logo.png', + nav: [ + { title: '指南', link: '/guide' }, + { title: '组件', link: '/components' }, + { title: '图表', link: '/charts' }, + ], + socialLinks: { + github: 'https://github.com/user/novaui', + }, + }, + // 自动 API 表格(必须,AI 生成组件时直接能看到类型) + apiParser: {}, + resolve: { + // doc 目录下的 .md 作为文档路由 + docDirs: ['docs'], + // 组件目录下的 index.md 作为组件文档 + atomDirs: [ + { type: 'component', dir: 'src/components' }, + { type: 'chart', dir: 'src/charts' }, + ], + }, + // 约定输出 + outputPath: 'dist-docs', +}) +``` + +### 3.2 组件文档 (`index.md`) 模板 + +```md +--- +nav: 组件 +group: 通用 +title: Button 按钮 +description: 用于触发操作的按钮组件。 +--- + +## 代码演示 + +### 基础用法 + +```tsx | pure +/** + * title: 基础按钮 + * description: 使用 `variant` 切换按钮风格。 + */ +import { Button } from '@nova/ui/button' + +export default () => ( + +) +``` + +### 变体 + +```tsx | pure + + + + +``` + +## API + + +``` + +### 3.3 dumi 文档规则 + +``` +1. 每个组件目录必须包含 index.md +2. 每个 demo 必须用 ```tsx | pure 声明(禁止用 react,避免 dumi 额外包裹) +3. demo 必须有 title,可选 description +4. 必须用 生成 API 表格(依赖 apiParser 插件) +5. 组件分类:通用 / 录入 / 展示 / 反馈 / 导航 / 图表 +``` + +--- + +## 四、father 配置约定 + +### 4.1 `.fatherrc.ts` + +```ts +import { defineConfig } from 'father' + +export default defineConfig({ + // Bundless 模式:保持源码目录结构,每个组件独立产物(Tree-shaking 最佳) + cjs: { + output: 'dist/cjs', + platform: 'browser', + }, + esm: { + output: 'dist/esm', + platform: 'browser', + // 保留 .module.css 作为 CSS Modules + extraBabelPlugins: [ + // 确保 .module.css 被正确处理 + ], + }, + // 自动生成 .d.ts + typescript: {}, + // UMD 构建(可选) + umd: { + output: 'dist/umd', + name: 'NovaUI', + }, +}) +``` + +### 4.2 father 构建规则 + +``` +1. 使用 Bundless 模式(非 Bundle),保持每个组件独立文件 + → 用户可 import { Button } from '@nova/ui/button' +2. 产物格式:esm + cjs(双格式) +3. CSS Modules 文件 (.module.css) 自动保留,build 后生成对应的 .css +4. TypeScript 生成 .d.ts 类型定义 +5. 外部依赖:react、react-dom 标记为 peerDependencies,不打包 +``` + +### 4.3 package.json 导出配置 + +```json +{ + "name": "@nova/ui", + "type": "module", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + }, + "./*": { + "import": "./dist/esm/*/index.js", + "require": "./dist/cjs/*/index.js" + } + }, + "sideEffects": [ + "**/*.css" + ], + "files": [ + "dist" + ], + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } +} +``` + +--- + +## 五、CSS Modules 约定 + +### 5.1 命名规则 + +```css +/* Button.module.css */ + +/* ✅ 用 camelCase,在 JS 中 import 后直接 styles.xxx */ +.wrapper { } +.iconLeft { } +.iconRight { } + +/* ✅ 全局覆盖用 :global */ +:global(.nv-theme-dark) .wrapper { + background: #1a1a3e; +} + +/* ✅ data-* 状态样式 */ +.wrapper[data-variant='solid'] { } +.wrapper[data-variant='outline'] { } +.wrapper[data-size='lg'] { } +.wrapper[data-loading] { } +``` + +### 5.2 组件引用 + +```tsx +// ✅ AI 正确用法 +import styles from './Button.module.css' + +
+ {icon} +
+ +// ❌ 禁止:styles['wrapper'](AI 容易拼错字符串) +``` + +### 5.3 全局 CSS + +```ts +// theme/global.css — 引入 normalize + CSS 变量 +// 在 dumi 的 .dumirc.ts 中通过 extraBabelPlugins 或手动 import + +// theme/dark.css — 暗色模式变量 +// 通过 html[data-theme='dark'] 选择器作用 +``` + +--- + +## 六、Monorepo 约定 + +### 6.1 pnpm-workspace.yaml + +```yaml +packages: + - 'packages/*' + - 'apps/*' +``` + +### 6.2 当前是单包,但结构保留扩展性 + +``` +novaui/ +├── packages/ +│ ├── core/ # UI 组件(@nova/ui) +│ ├── charts/ # 图表组件(@nova/charts) +│ ├── theme/ # 主题包(@nova/theme) +│ └── utils/ # 工具(@nova/utils) +└── apps/ + └── docs/ # dumi 文档站点 +``` + +> 初期可扁平,但目录名和 npm scope 预先约定好。 + +--- + +## 七、代码质量工具约定 + +### 7.1 ESLint + Prettier + +```jsonc +// .eslintrc.cjs +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'prettier', // 关闭冲突规则 + ], + rules: { + 'react/react-in-jsx-scope': 'off', // React 18+ + 'react/prop-types': 'off', // TypeScript 已覆盖 + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], + }, +} +``` + +```jsonc +// .prettierrc +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "arrowParens": "avoid" +} +``` + +### 7.2 commitlint + +``` +// .commitlintrc.cjs +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [2, 'always', [ + 'feat', 'fix', 'docs', 'style', 'refactor', + 'test', 'chore', 'ci', + ]], + }, +} + +// 格式: (): +// 示例: feat(Button): add glass variant +``` + +### 7.3 lint-staged + +```jsonc +// package.json +{ + "lint-staged": { + "*.{ts,tsx}": ["eslint --fix", "prettier --write"], + "*.{css,json,md}": ["prettier --write"] + } +} +``` + +--- + +## 八、测试约定 + +### 8.1 vitest.config.ts + +```ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./test-setup.ts'], + include: ['src/**/*.test.{ts,tsx}'], + coverage: { + provider: 'v8', + include: ['src/components/**/*.tsx', 'src/charts/**/*.tsx'], + thresholds: { + functions: 80, + }, + }, + }, +}) +``` + +### 8.2 测试规范 + +```tsx +// 每个组件至少覆盖: +// 1. 默认渲染 +import { render, screen } from '@testing-library/react' + +it('renders with default props', () => { + render() + expect(screen.getByRole('button')).toHaveTextContent('Click') +}) + +// 2. Props 组合 +it.each(['solid', 'outline', 'ghost'] as const)( + 'renders variant="%s" correctly', + variant => { + const { container } = render(