465 lines
12 KiB
Markdown
465 lines
12 KiB
Markdown
# Nova 设计规范
|
||
|
||
> AI-Native 组件库:专为大模型代码生成优化的 AntV 类可视化 + UI 组件库。
|
||
> 核心目标:**让 AI 第一次生成的代码就能跑、就能看、就能用。**
|
||
|
||
---
|
||
|
||
## 一、核心理念
|
||
|
||
### 1.1 AI 优先 (AI-First)
|
||
|
||
传统组件库(antd)面向人类开发者编写,Nova 面向 AI 编写。这意味着:
|
||
|
||
| 原则 | 说明 | 反例 (antd 风格) | 正例 (Nova 风格) |
|
||
|------|------|-------------------|------------------|
|
||
| **零歧义** | 每个 prop 名称自解释,无简称、无隐式行为 | `allowClear` | `showClear` |
|
||
| **零默认假设** | 所有视觉行为显式声明,不靠"常识" | `status` 默认 `undefined` 表示正常 | `status: 'default' \| 'error' \| 'warning' \| 'success'` |
|
||
| **一致性** | 相同概念用相同 prop 名,跨组件统一 | Table `dataSource` / Select `options` | 统一 `data` |
|
||
| **扁平化** | 最多一层嵌套,禁止深层 renderProps | `columns[].render()` | `column.renderCell` 函数 prop |
|
||
| **确定性** | 相同输入始终产生相同输出,无随机行为 | 部分组件默认不带 `key` 警告 | 所有组件强制 `key` 或无状态 |
|
||
|
||
### 1.2 可视化 + UI 一体化
|
||
|
||
Nova 融合 AntV 的数据可视化能力与 antd 的 UI 能力,两者共享同一套设计 Token、交互模式、主题系统。
|
||
|
||
---
|
||
|
||
## 二、AI 最友好的 API 设计规则
|
||
|
||
### 规则 1: 统一数据模型
|
||
|
||
所有列表/表格/图表组件遵循同一数据接口:
|
||
|
||
```tsx
|
||
// ✅ Nova 方式 — 所有数据组件统一
|
||
<Table data={items} />
|
||
<List data={items} />
|
||
<Chart data={items} />
|
||
<Timeline data={items} />
|
||
|
||
// ❌ antd 方式 — 不同名称增加 AI 混淆
|
||
<Table dataSource={items} /> // dataSource
|
||
<Select options={items} /> // options
|
||
<Tree treeData={items} /> // treeData
|
||
```
|
||
|
||
### 规则 2: 统一尺寸体系
|
||
|
||
```tsx
|
||
// 所有组件共用 size,无例外
|
||
size: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||
|
||
// ❌ antd 不一致的尺寸名
|
||
// Button: large / middle / small
|
||
// Input: large / middle / small
|
||
// DatePicker: large / middle / small
|
||
// Tag: 无尺寸 prop
|
||
```
|
||
|
||
### 规则 3: 统一事件命名
|
||
|
||
```
|
||
// 所有事件回调 = on + 动词 + 名词
|
||
onClick // ✅
|
||
onChange // ✅
|
||
onSelect // ✅
|
||
onClose // ✅
|
||
|
||
// ❌ 避免 on + 名词 + 动词 (增加 AI 推理负担)
|
||
onMenuClick // ❌ → onClick 即可
|
||
onPanelChange // ❌ → onChange
|
||
```
|
||
|
||
### 规则 4: 统一布尔 prop 命名
|
||
|
||
```tsx
|
||
// ✅ 统一用动词/形容词 + 肯定形式
|
||
disabled: boolean // 禁用
|
||
loading: boolean // 加载中
|
||
readOnly: boolean // 只读
|
||
showSearch: boolean // 显示搜索
|
||
showClear: boolean // 显示清除
|
||
allowMultiple: boolean // 允许多选
|
||
|
||
// ❌ 避免 antd 风格的不一致
|
||
// allowClear / showSearch / bordered / dropdownMatchSelectWidth
|
||
```
|
||
|
||
### 规则 5: 自包含组件 + 确定导入路径
|
||
|
||
```tsx
|
||
// ✅ 每个组件独立导入,路径即名字
|
||
import { Button } from '@nova/ui/button'
|
||
import { Modal } from '@nova/ui/modal'
|
||
import { LineChart } from '@nova/ui/line-chart'
|
||
|
||
// ❌ 避免 AI 不确定路径
|
||
// import { Button } from '@nova/ui' // 可能 tree-shake 失败
|
||
// import { LineChart } from '@nova/charts' // 需要 AI 猜测包名
|
||
```
|
||
|
||
### 规则 6: 类型优先
|
||
|
||
所有 prop 使用联合类型字面量而非 boolean,让 AI 看到所有选项:
|
||
|
||
```tsx
|
||
// ✅ 明确枚举,AI 能看到所有可能性
|
||
type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'glass'
|
||
type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||
type Status = 'default' | 'success' | 'warning' | 'error' | 'info'
|
||
|
||
// ❌ boolean 隐藏了其他可能性
|
||
<Button ghost /> // 为什么不是 outline?
|
||
<Button primary /> // primary 和 type="primary" 是什么关系?
|
||
```
|
||
|
||
```tsx
|
||
// 每个组件导出完整类型
|
||
export interface ButtonProps {
|
||
variant: ButtonVariant
|
||
size: ButtonSize
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 规则 7: 远离魔法字符串
|
||
|
||
```tsx
|
||
// ✅ 枚举/常量,AI 不会拼错
|
||
<Table scrollBar={{ placement: 'overlay' }} />
|
||
|
||
// ❌ 魔法字符串
|
||
<Input status="error" /> // "error" 还是 "Error"?还有哪些值?
|
||
```
|
||
|
||
### 规则 8: 组件原子化
|
||
|
||
一个组件只做一件事,不做"大而全"的上帝组件:
|
||
|
||
```tsx
|
||
// ✅ 拆分
|
||
<Table /> // 纯表格
|
||
<TableFilter /> // 表格筛选
|
||
<TableSort /> // 表格排序
|
||
|
||
// ❌ 不拆分
|
||
<Table filter sort pagination editable /> // 30+ props 的巨无霸
|
||
```
|
||
|
||
---
|
||
|
||
## 三、结合 AntV 的可视化组件规范
|
||
|
||
### 3.1 图表组件统一接口
|
||
|
||
所有图表组件共享以下 props 契约:
|
||
|
||
```tsx
|
||
interface ChartBaseProps<T> {
|
||
data: T[] // 统一数据源
|
||
xField: keyof T // X 轴字段名
|
||
yField: keyof T // Y 轴字段名
|
||
seriesField?: keyof T // 分组/系列字段
|
||
color?: string | string[] | ((d: T) => string)
|
||
size?: number | ((d: T) => number)
|
||
tooltip?: boolean | TooltipConfig
|
||
legend?: boolean | LegendConfig
|
||
animation?: boolean | AnimationConfig
|
||
}
|
||
```
|
||
|
||
### 3.2 支持的图表类型
|
||
|
||
```tsx
|
||
// 基础图表
|
||
<LineChart data={data} xField="date" yField="value" />
|
||
<BarChart data={data} xField="category" yField="count" />
|
||
<PieChart data={data} angleField="value" colorField="name" />
|
||
<AreaChart data={data} xField="date" yField="value" />
|
||
<ScatterChart data={data} xField="age" yField="income" />
|
||
|
||
// 组合图表
|
||
<DualAxesChart data={data} left={{ yField: 'revenue' }} right={{ yField: 'rate' }} />
|
||
|
||
// 统计图表
|
||
<BoxPlot data={data} />
|
||
<Heatmap data={data} />
|
||
<RadarChart data={data} />
|
||
<Treemap data={data} />
|
||
|
||
// 地图
|
||
<MapChart data={geoData} />
|
||
```
|
||
|
||
### 3.3 可视化交互
|
||
|
||
```tsx
|
||
// 所有图表支持标准交互
|
||
interface ChartInteraction {
|
||
tooltip: boolean | { shared?: boolean; crosshairs?: boolean }
|
||
zoom: boolean | { type: 'x' | 'y' | 'both' }
|
||
brush: boolean | { type: 'rect' | 'polygon' }
|
||
slider: boolean | { start?: number; end?: number }
|
||
legendFilter: boolean
|
||
}
|
||
```
|
||
|
||
### 3.4 Stat 组件(数值 + 迷你图)
|
||
|
||
```tsx
|
||
// AI 生成看板时最常用的组件
|
||
<Stat
|
||
title="本月收入"
|
||
value={128000}
|
||
prefix="¥"
|
||
trend={12.5} // 正数上升,负数下降
|
||
trendDirection="up"
|
||
chart={<MiniLine data={trendData} />}
|
||
/>
|
||
```
|
||
|
||
---
|
||
|
||
## 四、可落地的 Token 体系
|
||
|
||
所有值用 TypeScript 常量 + CSS 自定义属性双重暴露:
|
||
|
||
```ts
|
||
// theme/tokens.ts
|
||
export const tokens = {
|
||
color: {
|
||
primary: '#6C5CE7',
|
||
primaryHover: '#A29BFE',
|
||
primaryActive: '#4A3DBF',
|
||
success: '#00B894',
|
||
warning: '#FDCB6E',
|
||
error: '#FF7675',
|
||
info: '#74B9FF',
|
||
text: '#1a1a2e',
|
||
bg: '#ffffff',
|
||
bgElevated: '#f8f9fa',
|
||
border: '#e2e8f0',
|
||
},
|
||
radius: {
|
||
xs: '4px',
|
||
sm: '8px',
|
||
md: '12px',
|
||
lg: '16px',
|
||
xl: '24px',
|
||
round: '9999px',
|
||
},
|
||
space: {
|
||
xs: '4px',
|
||
sm: '8px',
|
||
md: '12px',
|
||
lg: '16px',
|
||
xl: '24px',
|
||
'2xl': '32px',
|
||
'3xl': '48px',
|
||
},
|
||
font: {
|
||
family: '"Inter", "SF Pro Display", -apple-system, sans-serif',
|
||
familyMono: '"JetBrains Mono", "SF Mono", monospace',
|
||
size: { xs: 12, sm: 14, md: 16, lg: 20, xl: 24 },
|
||
weight: { regular: 400, medium: 500, semibold: 600, bold: 700 },
|
||
},
|
||
shadow: {
|
||
sm: '0 1px 3px rgba(0,0,0,0.08)',
|
||
md: '0 4px 12px rgba(0,0,0,0.1)',
|
||
lg: '0 8px 30px rgba(0,0,0,0.12)',
|
||
xl: '0 20px 60px rgba(0,0,0,0.15)',
|
||
},
|
||
motion: {
|
||
fast: '100ms',
|
||
normal: '200ms',
|
||
slow: '350ms',
|
||
easeOut: 'cubic-bezier(0.23, 1, 0.32, 1)',
|
||
easeInOut: 'cubic-bezier(0.65, 0, 0.35, 1)',
|
||
spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||
},
|
||
} as const
|
||
```
|
||
|
||
CSS 变量注入:
|
||
|
||
```css
|
||
:root {
|
||
--nv-color-primary: #6C5CE7;
|
||
--nv-radius-sm: 8px;
|
||
--nv-space-md: 12px;
|
||
/* ... 每个 token 对应一个 --nv-* 变量 */
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 五、组件文件脚手架(AI 生成模板)
|
||
|
||
每个新组件创建时,AI 按此模板生成:
|
||
|
||
```tsx
|
||
// Button.tsx — AI 生成的模板示例
|
||
import React from 'react'
|
||
|
||
export interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
|
||
variant?: 'solid' | 'outline' | 'ghost' | 'glass'
|
||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||
status?: 'default' | 'success' | 'warning' | 'error'
|
||
loading?: boolean
|
||
icon?: React.ReactNode
|
||
iconPosition?: 'left' | 'right'
|
||
fullWidth?: boolean
|
||
}
|
||
|
||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||
({
|
||
variant = 'solid',
|
||
size = 'md',
|
||
status = 'default',
|
||
loading = false,
|
||
icon,
|
||
iconPosition = 'left',
|
||
fullWidth = false,
|
||
children,
|
||
...rest
|
||
}, ref) => {
|
||
return (
|
||
<button
|
||
ref={ref}
|
||
data-variant={variant}
|
||
data-size={size}
|
||
data-status={status}
|
||
data-loading={loading || undefined}
|
||
data-full-width={fullWidth || undefined}
|
||
{...rest}
|
||
>
|
||
{loading ? <Spinner /> : icon && iconPosition === 'left' && icon}
|
||
{children}
|
||
{icon && iconPosition === 'right' && icon}
|
||
</button>
|
||
)
|
||
}
|
||
)
|
||
|
||
Button.displayName = 'Button'
|
||
```
|
||
|
||
### 每个组件必须包含:
|
||
|
||
```
|
||
Button/
|
||
├── index.ts # export { Button } from './Button'
|
||
├── Button.tsx # 组件实现 + Props 类型导出
|
||
├── Button.style.ts # 样式(vanilla-extract / CSS modules)
|
||
├── Button.test.tsx # 测试(至少 3 个用例:渲染、交互、边界)
|
||
└── Button.stories.tsx # Storybook stories(至少展示所有 variant + size 组合)
|
||
```
|
||
|
||
---
|
||
|
||
## 六、AI 友好的代码约束
|
||
|
||
### 6.1 Props 声明规范
|
||
|
||
```tsx
|
||
// ✅ 正确做法
|
||
interface ButtonProps {
|
||
/** 按钮风格变体 */
|
||
variant?: 'solid' | 'outline' | 'ghost' | 'glass'
|
||
/** 按钮尺寸 */
|
||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||
}
|
||
|
||
// ❌ 禁止做法
|
||
interface BadProps {
|
||
type?: string // string 类型 — AI 不知道可选值
|
||
mode?: any // any 类型 — 无类型提示
|
||
[key: string]: any // 透传导致 AI 无法推理
|
||
}
|
||
```
|
||
|
||
### 6.2 状态映射(禁止 switch-case 硬编码)
|
||
|
||
```tsx
|
||
// ✅ 用对象映射,AI 容易理解
|
||
const STATUS_MAP = {
|
||
default: { bg: '#f5f5f5', color: '#333' },
|
||
success: { bg: '#f0fff4', color: '#00b894' },
|
||
warning: { bg: '#fffbeb', color: '#fdcb6e' },
|
||
error: { bg: '#fff5f5', color: '#ff7675' },
|
||
} as const
|
||
|
||
// ❌ switch-case 层层嵌套
|
||
switch (status) {
|
||
case 'success': return { bg: '#f0fff4', color: '#00b894' }
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 6.3 条件渲染
|
||
|
||
```tsx
|
||
// ✅ 提前 return / 三元表达式
|
||
if (!data.length) return <Empty />
|
||
return <div>{data.map(render)}</div>
|
||
|
||
// ❌ 复杂 && 链
|
||
{!!data && data.length > 0 && data.map(item => item.active && <Item />)}
|
||
```
|
||
|
||
### 6.4 样式绑定
|
||
|
||
```tsx
|
||
// ✅ data-* 属性 + CSS 属性选择器(AI 删改时不易出错)
|
||
<button data-variant="solid" data-size="lg" />
|
||
|
||
// ❌ classnames 动态拼接(AI 难以追踪所有组合)
|
||
<button className={cn('btn', `btn-${variant}`, `btn-${size}`)} />
|
||
```
|
||
|
||
---
|
||
|
||
## 七、项目结构
|
||
|
||
```
|
||
novaui/
|
||
├── packages/
|
||
│ ├── core/ # 核心组件
|
||
│ │ ├── Button/
|
||
│ │ ├── Modal/
|
||
│ │ └── ...
|
||
│ ├── charts/ # 可视化组件(基于 AntV/G2)
|
||
│ │ ├── LineChart/
|
||
│ │ ├── BarChart/
|
||
│ │ └── ...
|
||
│ ├── icons/ # 图标库
|
||
│ ├── theme/ # 主题 Token + 生成器
|
||
│ └── utils/ # 工具函数
|
||
├── apps/
|
||
│ └── docs/ # Storybook
|
||
├── packages.json # monorepo root
|
||
└── tsconfig.json
|
||
```
|
||
|
||
---
|
||
|
||
## 八、总结:Nova 给 AI 的"提示工程"
|
||
|
||
当 AI 生成 Nova 组件代码时,应当遵循的心智模型:
|
||
|
||
```
|
||
1. 导入路径 = @nova/ui/组件名(全小写 kebab-case)
|
||
2. 数据 prop 始终叫 data,没有例外
|
||
3. 尺寸 prop 始终叫 size,值从 xs-xl 选
|
||
4. 布尔 prop 用动词开头,如 showSearch、allowMultiple
|
||
5. 所有 prop 类型用联合类型字面量定义
|
||
6. 状态样式用 data-* 属性,不用 class 拼接
|
||
7. 回调命名 = on + 动词 + 名词
|
||
8. 每个组件独立文件,组件 = 文件夹
|
||
9. 可视化组件统一 xField / yField / data
|
||
10. 拒绝魔法字符串、拒绝 any、拒绝深层嵌套
|
||
```
|
||
|
||
> **Nova 的核心竞争力:AI 写的代码,跟人写的一样规范。**
|