mcpField
D
Design System Architect

Design System Architect

github.com/daffy0208设计与 UI38223 次安装44 周前更新
代码仓库

关于 Design System Architect

面向构建设计系统的团队,涵盖可复用且一致的 UI 基础设计。内容包括设计令牌、组件架构、文档、主题、治理与测试。

主要功能

  1. 01规划设计令牌结构
  2. 02设计可复用组件架构
  3. 03使用 Storybook 编写组件文档
  4. 04配置主题与品牌变体
  5. 05管理版本与治理流程
  6. 06测试组件和视觉变更

适用场景

  1. 01构建共享组件库
  2. 02支持浅色和深色主题
  3. 03为产品团队编写 UI 组件文档
  4. 04管理设计系统变更与发布

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/daffy0208/ai-dev-standards --skill design-system-architect

安装后在 Claude Code 中输入/design-system-architect确认已加载。

附带文件manifest.yamlREADME.md

详细介绍

设计系统架构师

设计系统是统一的事实来源,能够大规模实现一致性。

核心原则

设计系统不只是组件库,更是设计与工程之间的共通语言。优秀的设计系统能够:

  • 加快产品开发(可复用组件)
  • 确保产品间的一致性(统一品牌)
  • 提升无障碍性(融入组件)
  • 实现可扩展性(复合式增长,而非线性增长)
  • 减少技术债务(集中维护)

目标: 构建一次,随处使用。维护一次,全面改进。


阶段 1:设计令牌

什么是设计令牌?

设计令牌是设计系统的原子值。 它们是以数据形式存储、供所有平台使用的最小设计决策(颜色、间距、排版)。

令牌的重要性:

  • 单一事实来源(修改一次,随处更新)
  • 与平台无关(JSON → CSS、iOS、Android 等)
  • 可版本化且可追溯(通过 Git 历史记录设计决策)
  • 可换肤(浅色/深色模式、品牌变体)
令牌结构
// tokens/colors.json
{
  "color": {
    "brand": {
      "primary": {
        "50": { "value": "#eff6ff" },
        "100": { "value": "#dbeafe" },
        "200": { "value": "#bfdbfe" },
        "300": { "value": "#93c5fd" },
        "400": { "value": "#60a5fa" },
        "500": { "value": "#3b82f6" },
        "600": { "value": "#2563eb" },
        "700": { "value": "#1d4ed8" },
        "800": { "value": "#1e40af" },
        "900": { "value": "#1e3a8a" },
        "950": { "value": "#172554" }
      }
    },
    "semantic": {
      "background": {
        "primary": { "value": "{color.neutral.50}" },
        "secondary": { "value": "{color.neutral.100}" }
      },
      "text": {
        "primary": { "value": "{color.neutral.900}" },
        "secondary": { "value": "{color.neutral.600}" }
      },
      "feedback": {
        "success": { "value": "#10b981" },
        "warning": { "value": "#f59e0b" },
        "error": { "value": "#ef4444" },
        "info": { "value": "#3b82f6" }
      }
    }
  }
}
令牌类别

原始令牌(原始值):

{
  "color-blue-500": "#3b82f6",
  "space-4": "16px",
  "font-size-base": "16px"
}

语义令牌(按用途命名):

{
  "color-primary": "{color-blue-500}",
  "spacing-default": "{space-4}",
  "text-body": "{font-size-base}"
}

组件令牌(特定于组件):

{
  "button-padding-x": "{spacing-default}",
  "button-background": "{color-primary}",
  "button-text": "{color-white}"
}
令牌架构
tokens/
├── primitives/
│   ├── colors.json       # Raw color values
│   ├── spacing.json      # 4px, 8px, 16px...
│   ├── typography.json   # Font sizes, weights
│   ├── shadows.json      # Elevation system
│   └── radii.json        # Border radius values
├── semantic/
│   ├── colors.json       # background-primary, text-secondary
│   ├── spacing.json      # spacing-tight, spacing-comfortable
│   └── typography.json   # heading-xl, body-md
└── components/
    ├── button.json       # Button-specific tokens
    ├── input.json        # Input-specific tokens
    └── card.json         # Card-specific tokens
使用 Style Dictionary 转换令牌

安装 Style Dictionary:

npm install --save-dev style-dictionary

config.json:

{
  "source": ["tokens/**/*.json"],
  "platforms": {
    "css": {
      "transformGroup": "css",
      "buildPath": "dist/css/",
      "files": [
        {
          "destination": "variables.css",
          "format": "css/variables"
        }
      ]
    },
    "js": {
      "transformGroup": "js",
      "buildPath": "dist/js/",
      "files": [
        {
          "destination": "tokens.js",
          "format": "javascript/es6"
        }
      ]
    }
  }
}

构建令牌:

npx style-dictionary build

输出(variables.css):

:root {
  --color-brand-primary-500: #3b82f6;
  --color-semantic-background-primary: #fafafa;
  --space-4: 16px;
  --font-size-base: 16px;
  --button-padding-x: 16px;
}
使用令牌实现深色模式
// tokens/semantic/colors-light.json
{
  "background": {
    "primary": { "value": "#ffffff" },
    "secondary": { "value": "#f9fafb" }
  },
  "text": {
    "primary": { "value": "#111827" },
    "secondary": { "value": "#6b7280" }
  }
}

// tokens/semantic/colors-dark.json
{
  "background": {
    "primary": { "value": "#111827" },
    "secondary": { "value": "#1f2937" }
  },
  "text": {
    "primary": { "value": "#f9fafb" },
    "secondary": { "value": "#d1d5db" }
  }
}

CSS 输出:

:root {
  --background-primary: #ffffff;
  --text-primary: #111827;
}

[data-theme='dark'] {
  --background-primary: #111827;
  --text-primary: #f9fafb;
}

阶段 2:组件架构

原子设计方法论

原子 → 分子 → 有机体 → 模板 → 页面

atoms/
├── Button/
├── Input/
├── Label/
├── Icon/
└── Text/

molecules/
├── FormField/         # Label + Input + Error
├── SearchBar/         # Input + Icon + Button
└── Card/              # Container + Text + Button

organisms/
├── Header/            # Logo + Nav + SearchBar
├── LoginForm/         # FormFields + Button
└── ProductCard/       # Card + Image + Price + CTA

templates/
├── DashboardLayout/   # Header + Sidebar + Content
└── MarketingLayout/   # Header + Hero + Features + Footer

pages/
├── HomePage/          # MarketingLayout + specific content
└── DashboardPage/     # DashboardLayout + widgets
组件结构
components/
└── Button/
    ├── Button.tsx           # Component implementation
    ├── Button.module.css    # Scoped styles
    ├── Button.stories.tsx   # Storybook stories
    ├── Button.test.tsx      # Unit tests
    ├── Button.types.ts      # TypeScript types
    ├── index.ts             # Public exports
    └── README.md            # Component docs
Button 组件(原子示例)

Button.types.ts:

export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'danger'
export type ButtonSize = 'sm' | 'md' | 'lg'

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant
  size?: ButtonSize
  isLoading?: boolean
  leftIcon?: React.ReactNode
  rightIcon?: React.ReactNode
  fullWidth?: boolean
  children: React.ReactNode
}

Button.tsx:

import React from 'react'
import styles from './Button.module.css'
import { ButtonProps } from './Button.types'

export function Button({
  variant = 'primary',
  size = 'md',
  isLoading = false,
  leftIcon,
  rightIcon,
  fullWidth = false,
  disabled,
  children,
  className,
  ...props
}: ButtonProps) {
  const classes = [
    styles.button,
    styles[variant],
    styles[size],
    fullWidth && styles.fullWidth,
    isLoading && styles.loading,
    className
  ].filter(Boolean).join(' ')

  return (
    <button
      className={classes}
      disabled={disabled || isLoading}
      aria-busy={isLoading}
      {...props}
    >
      {leftIcon && <span className={styles.iconLeft}>{leftIcon}</span>}
      <span className={styles.content}>{children}</span>
      {rightIcon && <span className={styles.iconRight}>{rightIcon}</span>}
      {isLoading && (
        <span className={styles.spinner} aria-label="Loading">
          <svg className={styles.spinnerIcon} viewBox="0 0 24 24">
            <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" opacity="0.25" />
            <path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" strokeWidth="4" fill="none" strokeLinecap="round" />
          </svg>
        </span>
      )}
    </button>
  )
}

Button.module.css:

.button {
  /* Base */
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-family: var(--font-base);
  font-weight: 500;
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all 150ms ease;
  user-select: none;

  /* Accessibility */
  outline-offset: 2px;
}

.button:focus-visible {
  outline: 2px solid var(--color-focus);
}

.button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* Variants */
.primary {
  background: var(--button-primary-background);
  color: var(--button-primary-text);
}

.primary:hover:not(:disabled) {
  background: var(--button-primary-background-hover);
}

.secondary {
  background: transparent;
  color: var(--button-secondary-text);
  border: 1px solid var(--button-secondary-border);
}

.secondary:hover:not(:disabled) {
  background: var(--button-secondary-background-hover);
}

.tertiary {
  background: transparent;
  color: var(--button-tertiary-text);
}

.tertiary:hover:not(:disabled) {
  background: var(--button-tertiary-background-hover);
}

.danger {
  background: var(--color-error);
  color: var(--color-white);
}

.danger:hover:not(:disabled) {
  background: var(--color-error-dark);
}

/* Sizes */
.sm {
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-sm);
  height: 32px;
}

.md {
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-base);
  height: 40px;
}

.lg {
  padding: var(--space-3) var(--space-6);
  font-size: var(--text-lg);
  height: 48px;
}

/* Modifiers */
.fullWidth {
  width: 100%;
}

.loading {
  color: transparent;
}

.spinner {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  inset: 0;
}

.spinnerIcon {
  width: 20px;
  height: 20px;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

.iconLeft,
.iconRight {
  display: flex;
  align-items: center;
}

.content {
  display: flex;
  align-items: center;
}
FormField 组件(分子示例)

FormField.tsx:

import React from 'react'
import styles from './FormField.module.css'

interface FormFieldProps {
  label: string
  error?: string
  hint?: string
  required?: boolean
  children: React.ReactNode
}

export function FormField({
  label,
  error,
  hint,
  required = false,
  children
}: FormFieldProps) {
  const inputId = React.useId()
  const errorId = `${inputId}-error`
  const hintId = `${inputId}-hint`

  // Clone child to pass accessibility props
  const childWithProps = React.cloneElement(
    children as React.ReactElement,
    {
      id: inputId,
      'aria-invalid': !!error,
      'aria-describedby': [
        hint ? hintId : null,
        error ? errorId : null
      ].filter(Boolean).join(' ') || undefined
    }
  )

  return (
    <div className={styles.field}>
      <label htmlFor={inputId} className={styles.label}>
        {label}
        {required && (
          <span className={styles.required} aria-label="required">
            *
          </span>
        )}
      </label>

      {hint && (
        <div id={hintId} className={styles.hint}>
          {hint}
        </div>
      )}

      {childWithProps}

      {error && (
        <div id={errorId} className={styles.error} role="alert">
          {error}
        </div>
      )}
    </div>
  )
}
组件 API 设计原则

1. 合理的默认值:

// ✅ Good: Works with minimal props
<Button>Click me</Button>

// ✅ Good: Customizable when needed
<Button variant="secondary" size="lg" fullWidth>
  Click me
</Button>

2. 优先采用组合,而非配置:

// ❌ Bad: Too many props
<Modal
  title="Delete Account"
  body="Are you sure?"
  confirmText="Delete"
  cancelText="Cancel"
  onConfirm={handleDelete}
  onCancel={handleCancel}
/>

// ✅ Good: Composable
<Modal>
  <Modal.Header>Delete Account</Modal.Header>
  <Modal.Body>Are you sure?</Modal.Body>
  <Modal.Footer>
    <Button variant="danger" onClick={handleDelete}>Delete</Button>
    <Button variant="secondary" onClick={handleCancel}>Cancel</Button>
  </Modal.Footer>
</Modal>

3. 受控与非受控模式:

// Uncontrolled (internal state)
<Input defaultValue="hello" />

// Controlled (external state)
<Input value={value} onChange={setValue} />

4. 多态组件:

interface ButtonProps<T extends React.ElementType = 'button'> {
  as?: T
  // ... other props
}

// Render as button (default)
<Button onClick={handleClick}>Click</Button>

// Render as link
<Button as="a" href="/dashboard">Dashboard</Button>

// Render as Next.js Link
<Button as={Link} href="/about">About</Button>

阶段 3:Storybook 文档

Storybook 设置

安装 Storybook:

npx storybook@latest init

Button.stories.tsx:

import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'

const meta: Meta<typeof Button> = {
  title: 'Atoms/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'tertiary', 'danger'],
    },
    size: {
      control: 'select',
      options: ['sm', 'md', 'lg'],
    },
    isLoading: { control: 'boolean' },
    fullWidth: { control: 'boolean' },
    disabled: { control: 'boolean' },
  },
}

export default meta
type Story = StoryObj<typeof Button>

// Primary story
export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Button',
  },
}

// All variants
export const AllVariants: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '1rem' }}>
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="tertiary">Tertiary</Button>
      <Button variant="danger">Danger</Button>
    </div>
  ),
}

// All sizes
export const AllSizes: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
    </div>
  ),
}

// With icons
export const WithIcons: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '1rem' }}>
      <Button leftIcon={<Icon name="plus" />}>Add Item</Button>
      <Button rightIcon={<Icon name="arrow-right" />}>Next</Button>
    </div>
  ),
}

// Loading state
export const Loading: Story = {
  args: {
    isLoading: true,
    children: 'Loading...',
  },
}

// Disabled state
export const Disabled: Story = {
  args: {
    disabled: true,
    children: 'Disabled',
  },
}

// Full width
export const FullWidth: Story = {
  args: {
    fullWidth: true,
    children: 'Full Width Button',
  },
}
MDX 文档

Button.mdx:

import { Meta, Story, Canvas, Controls } from '@storybook/blocks'
import * as ButtonStories from './Button.stories'

<Meta of={ButtonStories} />

# Button

Buttons allow users to trigger actions and make choices with a single tap.

## When to Use

- Primary actions (submit forms, complete workflows)
- Secondary actions (cancel, go back)
- Tertiary actions (view details, learn more)
- Dangerous actions (delete, remove)

## Variants

<Canvas of={ButtonStories.AllVariants} />

### Primary

Use for the main action on a page. Limit to one per screen.

### Secondary

Use for less important actions. Can have multiple per screen.

### Tertiary

Use for the least important actions, like "Learn more" links.

### Danger

Use for destructive actions like deleting data.

## Sizes

<Canvas of={ButtonStories.AllSizes} />

- **Small**: Dense UIs, table actions
- **Medium**: Default for most use cases
- **Large**: Hero CTAs, mobile-first designs

## With Icons

<Canvas of={ButtonStories.WithIcons} />

Icons can clarify the button's purpose or indicate direction.

## States

### Loading

<Canvas of={ButtonStories.Loading} />

Show a loading spinner when an async action is in progress.

### Disabled

<Canvas of={ButtonStories.Disabled} />

Disable buttons when an action is not currently available.

## Accessibility

- ✅ Keyboard accessible (Tab, Enter/Space)
- ✅ Focus visible indicator
- ✅ ARIA attributes (aria-busy, aria-disabled)
- ✅ Loading state announced to screen readers

## Props

<Controls />

## Usage

```tsx
import { Button } from '@/components/Button'

function MyComponent() {
  return (
    <Button variant="primary" onClick={handleClick}>
      Click me
    </Button>
  )
}
```

### Storybook Addons

```bash
# Accessibility testing
npm install --save-dev @storybook/addon-a11y

# Component interactions
npm install --save-dev @storybook/addon-interactions

# Design tokens addon
npm install --save-dev storybook-addon-designs

.storybook/main.ts:

import type { StorybookConfig } from '@storybook/react-vite'

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
    'storybook-addon-designs'
  ],
  framework: '@storybook/react-vite'
}

export default config

阶段 4:主题与自定义

CSS 变量方案

tokens.css:

:root {
  /* Primitives */
  --color-blue-500: #3b82f6;
  --color-red-500: #ef4444;
  --space-4: 16px;

  /* Semantic (can be overridden) */
  --color-primary: var(--color-blue-500);
  --color-danger: var(--color-red-500);
  --spacing-default: var(--space-4);

  /* Component tokens */
  --button-primary-background: var(--color-primary);
  --button-primary-text: white;
  --button-padding: var(--spacing-default);
}

/* Theme override */
[data-theme='brand-red'] {
  --color-primary: #dc2626;
}

[data-theme='dark'] {
  --color-primary: #60a5fa;
  --button-primary-background: var(--color-primary);
}
主题提供程序(React)

ThemeProvider.tsx:

import React, { createContext, useContext, useEffect, useState } from 'react'

type Theme = 'light' | 'dark' | 'auto'

interface ThemeContextValue {
  theme: Theme
  setTheme: (theme: Theme) => void
  resolvedTheme: 'light' | 'dark'
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>('auto')
  const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light')

  useEffect(() => {
    // Load saved theme
    const saved = localStorage.getItem('theme') as Theme
    if (saved) setTheme(saved)
  }, [])

  useEffect(() => {
    // Save theme
    localStorage.setItem('theme', theme)

    // Resolve theme
    if (theme === 'auto') {
      const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
      setResolvedTheme(isDark ? 'dark' : 'light')
    } else {
      setResolvedTheme(theme)
    }
  }, [theme])

  useEffect(() => {
    // Apply theme to DOM
    document.documentElement.setAttribute('data-theme', resolvedTheme)
  }, [resolvedTheme])

  return (
    <ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider')
  }
  return context
}
多品牌主题
// themes/acme.ts
export const acmeTheme = {
  colors: {
    primary: '#3b82f6',
    secondary: '#8b5cf6'
  },
  typography: {
    fontFamily: 'Inter, sans-serif'
  },
  spacing: {
    unit: 8
  }
}

// themes/contoso.ts
export const contosoTheme = {
  colors: {
    primary: '#dc2626',
    secondary: '#f59e0b'
  },
  typography: {
    fontFamily: 'Roboto, sans-serif'
  },
  spacing: {
    unit: 4
  }
}

应用主题:

function applyTheme(theme: Theme) {
  Object.entries(theme.colors).forEach(([key, value]) => {
    document.documentElement.style.setProperty(`--color-${key}`, value)
  })

  document.documentElement.style.setProperty('--font-base', theme.typography.fontFamily)

  document.documentElement.style.setProperty('--space-unit', `${theme.spacing.unit}px`)
}

阶段 5:版本管理与治理

语义化版本控制

MAJOR.MINOR.PATCH

  • MAJOR: 不兼容的变更(v1.0.0 → v2.0.0)

    • 移除属性
    • 更改属性类型
    • 更改默认行为
  • MINOR: 新功能,向后兼容(v1.0.0 → v1.1.0)

    • 新属性
    • 新组件
    • 新变体
  • PATCH: 错误修复(v1.0.0 → v1.0.1)

    • CSS 修复
    • 无障碍性改进
    • TypeScript 修复
更新日志

CHANGELOG.md:

# Changelog

## [2.0.0] - 2024-01-15

### Breaking Changes

- **Button:** Renamed `type` prop to `variant`
- **Input:** Removed `error` prop (use FormField wrapper instead)

### Migration Guide

```typescript
// Before
<Button type="primary">Click</Button>

// After
<Button variant="primary">Click</Button>
```

[1.5.0] - 2024-01-10

新增
  • Button: 新增 isLoading 属性
  • Input: 新增 leftIcon 和 rightIcon 属性
  • Card: 新增组件
修复
  • Button: 现在所有浏览器中都能显示焦点轮廓
  • Input: 占位符颜色现在符合 WCAG 对比度要求

[1.4.1] - 2024-01-05

修复
  • Button: 加载旋转图标现在可以正确居中
  • Modal: 修复背景遮罩 z-index 问题

### Component Lifecycle

**P0 (Must Have):**
- Button, Input, Label, Text, Icon
- FormField, Card, Modal

**P1 (Should Have):**
- Select, Checkbox, Radio, Switch, Textarea
- Tabs, Accordion, Dropdown, Tooltip

**P2 (Nice to Have):**
- DatePicker, Combobox, Slider, Toggle
- Toast, Drawer, Popover

**P3 (Future):**
- DataTable, Calendar, FileUpload
- Charts, Timeline, Stepper

### Deprecation Strategy

**1. Announce deprecation:**
```typescript
/**
 * @deprecated Use `variant` prop instead. Will be removed in v3.0.0.
 */
export interface ButtonProps {
  type?: 'primary' | 'secondary' // Deprecated
  variant?: 'primary' | 'secondary' // New
}

2. 同时支持两者(并发出警告):

export function Button({ type, variant, ...props }: ButtonProps) {
  if (type) {
    console.warn('Button: `type` prop is deprecated. Use `variant` instead.')
  }

  const finalVariant = variant || type || 'primary'
  // ...
}

3. 在下一个主要版本中移除:

// v3.0.0 - type prop removed entirely
export interface ButtonProps {
  variant?: 'primary' | 'secondary'
}
设计系统治理

设计系统团队:

  • 负责人: 整体愿景与路线图
  • 设计师: 视觉设计、UX 模式
  • 工程师: 实现、工具
  • 贡献者: 构建组件的产品团队

贡献流程:

1. Proposal → GitHub issue
2. Design Review → Figma mockup
3. API Design → TypeScript interface
4. Implementation → PR with tests + stories
5. Documentation → README + Storybook
6. Release → Semantic versioning + changelog

提案模板:

## Component Name

**Problem:** What user need does this solve?

**Usage:** When should this be used?

**API Proposal:**

```typescript
interface ComponentProps {
  // ...
}
```

设计稿: [Figma 链接]

无障碍: 如何确保此功能具备无障碍性?

考虑过的替代方案: 我们还探索了哪些方案?


---

## Testing Design Systems

### Visual Regression Testing

**Chromatic (Storybook integration):**
```bash
npm install --save-dev chromatic

# Run visual tests
npx chromatic --project-token=YOUR_TOKEN

package.json:

{
  "scripts": {
    "chromatic": "chromatic --exit-zero-on-changes"
  }
}
组件测试
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Button } from './Button'

describe('Button', () => {
  it('renders children', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument()
  })

  it('calls onClick when clicked', async () => {
    const user = userEvent.setup()
    const handleClick = jest.fn()

    render(<Button onClick={handleClick}>Click me</Button>)
    await user.click(screen.getByRole('button'))

    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  it('disables button when loading', () => {
    render(<Button isLoading>Click me</Button>)
    expect(screen.getByRole('button')).toBeDisabled()
    expect(screen.getByLabelText('Loading')).toBeInTheDocument()
  })

  it('applies variant classes', () => {
    render(<Button variant="danger">Delete</Button>)
    expect(screen.getByRole('button')).toHaveClass('danger')
  })
})
无障碍测试
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)

it('has no accessibility violations', async () => {
  const { container } = render(<Button>Click me</Button>)
  const results = await axe(container)
  expect(results).toHaveNoViolations()
})

发布与分发

NPM 包设置

package.json:

{
  "name": "@company/design-system",
  "version": "1.0.0",
  "description": "Company design system",
  "main": "dist/index.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "files": ["dist", "README.md"],
  "scripts": {
    "build": "rollup -c",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "publishConfig": {
    "access": "public"
  }
}

rollup.config.js:

import typescript from '@rollup/plugin-typescript'
import postcss from 'rollup-plugin-postcss'

export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/index.js',
      format: 'cjs',
      sourcemap: true
    },
    {
      file: 'dist/index.esm.js',
      format: 'esm',
      sourcemap: true
    }
  ],
  plugins: [
    typescript({ tsconfig: './tsconfig.json' }),
    postcss({
      modules: true,
      extract: 'styles.css'
    })
  ],
  external: ['react', 'react-dom']
}

发布:

npm login
npm version patch  # or minor, major
npm publish
使用
npm install @company/design-system
import { Button, Input, Card } from '@company/design-system'
import '@company/design-system/dist/styles.css'

function App() {
  return (
    <Card>
      <Input placeholder="Email" />
      <Button variant="primary">Submit</Button>
    </Card>
  )
}

设计系统检查清单

基础
  • 设计令牌(颜色、间距、字体排印、阴影、圆角)
  • 令牌转换流程(Style Dictionary)
  • 深色模式支持
  • 文档站点(Storybook)
组件
  • 原子化设计结构(原子 → 有机体)
  • 组件 API 指南(合理的默认值、组合)
  • 所有组件均提供 TypeScript 类型
  • CSS modules 或 styled-components
  • 内置无障碍支持(WCAG 2.1 AA)
文档
  • 所有组件的 Storybook 故事
  • 包含使用示例的 MDX 文档
  • Props 表格(自动生成)
  • 设计指南(何时使用、何时不使用)
  • 代码示例
测试
  • 单元测试(覆盖率 70%+)
  • 无障碍测试(jest-axe)
  • 视觉回归测试(Chromatic)
  • 手动测试检查清单
治理
  • 语义化版本控制
  • 维护变更日志
  • 贡献指南
  • 组件生命周期(P0 → P3)
  • 弃用策略
分发
  • 已发布 NPM 包
  • 已部署 Storybook
  • 安装指南
  • 重大变更的迁移指南

工具与资源

令牌管理:

  • Style Dictionary(令牌转换)
  • Figma Tokens(同步 Figma → 代码)

组件开发:

  • Storybook(文档)
  • Chromatic(视觉测试)
  • React + TypeScript

测试:

  • Jest + React Testing Library
  • jest-axe(无障碍)
  • Playwright(端到端)

构建与分发:

  • Rollup 或 tsup(打包)
  • NPM(分发)
  • Changesets(版本控制)

灵感来源:


相关技能

  • visual-designer - 设计基础(颜色、字体排印、间距)
  • accessibility-engineer - WCAG 合规
  • frontend-builder - React 组件模式
  • testing-strategist - 组件测试策略

设计系统永无止境——它会随着产品不断演进。 🎨

评论

支持 Markdown