Files
ai-proj-helper/skills-dev/frontend-design-plugin/skills/SKILL.md

15 KiB
Raw Permalink Blame History

name, description, arguments
name description arguments
frontend-design Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics. [component|page|storybook] <description>

Frontend Design 前端设计技能

创建高质量、有设计感的前端界面和组件,支持 Storybook 组件开发。


命令格式

命令 功能 示例
/frontend-design component <描述> 创建 React/Vue 组件 /frontend-design component 产品卡片
/frontend-design page <描述> 创建完整页面 /frontend-design page 登录页
/frontend-design storybook <描述> 创建带 Storybook 的组件 /frontend-design storybook 按钮组件

设计原则

1. 设计思维先行

在编码前,明确以下问题:

  • 目的:这个界面解决什么问题?谁在使用?
  • 调性:选择一个明确的美学方向
  • 差异化:什么让这个设计令人难忘?

2. 美学方向选择

风格 特点 适用场景
极简主义 大量留白、精炼元素 工具类、专业平台
现代商务 清晰层次、专业配色 企业官网、B2B
活力年轻 鲜艳色彩、动感动画 消费品、社交
奢华精致 深色调、金属质感 高端品牌、金融
自然有机 柔和曲线、自然色系 健康、环保
复古怀旧 经典字体、做旧质感 文化、艺术
未来科技 渐变、玻璃拟态 科技、创新

3. 避免的设计陷阱

禁止使用

  • 过度使用的字体:Inter、Roboto、Arial
  • 陈词滥调的配色:紫色渐变白底
  • 千篇一律的布局
  • 缺乏个性的通用组件

应该追求

  • 独特的字体组合
  • 有意图的配色方案
  • 打破常规的布局
  • 有记忆点的细节

Storybook 组件开发

项目结构

src/
├── components/
│   ├── Button/
│   │   ├── Button.tsx
│   │   ├── Button.stories.tsx
│   │   ├── Button.module.css
│   │   └── index.ts
│   ├── Card/
│   │   ├── Card.tsx
│   │   ├── Card.stories.tsx
│   │   ├── Card.module.css
│   │   └── index.ts
│   └── index.ts
├── styles/
│   ├── variables.css
│   ├── typography.css
│   └── animations.css
└── .storybook/
    ├── main.ts
    └── preview.ts

组件模板

1. 组件文件 (Component.tsx)

import React from 'react';
import styles from './Component.module.css';

export interface ComponentProps {
  /** 组件变体 */
  variant?: 'primary' | 'secondary' | 'outline';
  /** 尺寸 */
  size?: 'sm' | 'md' | 'lg';
  /** 是否禁用 */
  disabled?: boolean;
  /** 子元素 */
  children: React.ReactNode;
  /** 点击事件 */
  onClick?: () => void;
}

export const Component: React.FC<ComponentProps> = ({
  variant = 'primary',
  size = 'md',
  disabled = false,
  children,
  onClick,
}) => {
  return (
    <div
      className={`${styles.component} ${styles[variant]} ${styles[size]}`}
      data-disabled={disabled}
      onClick={disabled ? undefined : onClick}
    >
      {children}
    </div>
  );
};

2. Storybook Stories (Component.stories.tsx)

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

const meta: Meta<typeof Component> = {
  title: 'Components/Component',
  component: Component,
  tags: ['autodocs'],
  parameters: {
    layout: 'centered',
    docs: {
      description: {
        component: '组件描述文档',
      },
    },
  },
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'outline'],
      description: '组件变体样式',
    },
    size: {
      control: 'radio',
      options: ['sm', 'md', 'lg'],
      description: '组件尺寸',
    },
    disabled: {
      control: 'boolean',
      description: '是否禁用',
    },
  },
};

export default meta;
type Story = StoryObj<typeof Component>;

/** 默认状态 */
export const Default: Story = {
  args: {
    children: '默认组件',
  },
};

/** 主要变体 */
export const Primary: Story = {
  args: {
    variant: 'primary',
    children: '主要按钮',
  },
};

/** 次要变体 */
export const Secondary: Story = {
  args: {
    variant: 'secondary',
    children: '次要按钮',
  },
};

/** 不同尺寸 */
export const Sizes: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
      <Component size="sm">小号</Component>
      <Component size="md">中号</Component>
      <Component size="lg">大号</Component>
    </div>
  ),
};

/** 禁用状态 */
export const Disabled: Story = {
  args: {
    disabled: true,
    children: '禁用状态',
  },
};

3. 样式文件 (Component.module.css)

.component {
  /* 基础样式 */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  font-family: var(--font-sans);
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
}

/* 变体 */
.primary {
  background: var(--color-primary);
  color: white;
}

.primary:hover {
  background: var(--color-primary-dark);
  transform: translateY(-1px);
  box-shadow: 0 4px 12px var(--color-primary-shadow);
}

.secondary {
  background: var(--color-secondary);
  color: var(--color-text);
}

.outline {
  background: transparent;
  border: 2px solid var(--color-border);
  color: var(--color-text);
}

/* 尺寸 */
.sm {
  padding: 0.5rem 1rem;
  font-size: 0.875rem;
}

.md {
  padding: 0.75rem 1.5rem;
  font-size: 1rem;
}

.lg {
  padding: 1rem 2rem;
  font-size: 1.125rem;
}

/* 状态 */
[data-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

设计系统变量

CSS 变量模板

:root {
  /* 颜色 */
  --color-primary: #0066ff;
  --color-primary-dark: #0052cc;
  --color-primary-light: #4d94ff;
  --color-primary-shadow: rgba(0, 102, 255, 0.25);

  --color-secondary: #f0f4f8;
  --color-accent: #ff6b35;

  --color-text: #1a1a2e;
  --color-text-muted: #64748b;
  --color-text-inverse: #ffffff;

  --color-background: #ffffff;
  --color-surface: #f8fafc;
  --color-border: #e2e8f0;

  --color-success: #10b981;
  --color-warning: #f59e0b;
  --color-error: #ef4444;

  /* 字体 */
  --font-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-display: 'Clash Display', var(--font-sans);
  --font-mono: 'JetBrains Mono', monospace;

  /* 字号 */
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.25rem;
  --text-2xl: 1.5rem;
  --text-3xl: 2rem;
  --text-4xl: 2.5rem;

  /* 间距 */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-12: 3rem;
  --space-16: 4rem;

  /* 圆角 */
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 1rem;
  --radius-xl: 1.5rem;
  --radius-full: 9999px;

  /* 阴影 */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
  --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);

  /* 动画 */
  --duration-fast: 150ms;
  --duration-normal: 300ms;
  --duration-slow: 500ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* 暗色主题 */
[data-theme="dark"] {
  --color-text: #f1f5f9;
  --color-text-muted: #94a3b8;
  --color-background: #0f172a;
  --color-surface: #1e293b;
  --color-border: #334155;
}

常用组件示例

1. 产品卡片 (ProductCard)

// ProductCard.tsx
import React from 'react';
import styles from './ProductCard.module.css';

export interface ProductCardProps {
  image: string;
  title: string;
  location: string;
  rating: number;
  reviewCount: number;
  price: number;
  originalPrice?: number;
  tags?: string[];
  onAddToCart?: () => void;
}

export const ProductCard: React.FC<ProductCardProps> = ({
  image,
  title,
  location,
  rating,
  reviewCount,
  price,
  originalPrice,
  tags = [],
  onAddToCart,
}) => {
  return (
    <article className={styles.card}>
      <div className={styles.imageWrapper}>
        <img src={image} alt={title} className={styles.image} />
        {tags.length > 0 && (
          <div className={styles.tags}>
            {tags.map((tag) => (
              <span key={tag} className={styles.tag} data-tag={tag}>
                {tag}
              </span>
            ))}
          </div>
        )}
      </div>

      <div className={styles.content}>
        <h3 className={styles.title}>{title}</h3>
        <p className={styles.location}>📍 {location}</p>

        <div className={styles.rating}>
          <span className={styles.stars}> {rating.toFixed(1)}</span>
          <span className={styles.reviewCount}>({reviewCount}条评价)</span>
        </div>

        <div className={styles.priceRow}>
          <div className={styles.price}>
            <span className={styles.currency}>¥</span>
            <span className={styles.amount}>{price}</span>
            <span className={styles.suffix}></span>
          </div>
          {originalPrice && (
            <span className={styles.originalPrice}>¥{originalPrice}</span>
          )}
        </div>

        <button className={styles.addButton} onClick={onAddToCart}>
          加入购物车
        </button>
      </div>
    </article>
  );
};
// ProductCard.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ProductCard } from './ProductCard';

const meta: Meta<typeof ProductCard> = {
  title: 'Components/ProductCard',
  component: ProductCard,
  tags: ['autodocs'],
  parameters: {
    layout: 'centered',
    backgrounds: {
      default: 'light',
    },
  },
};

export default meta;
type Story = StoryObj<typeof ProductCard>;

export const Default: Story = {
  args: {
    image: 'https://images.unsplash.com/photo-1494947665470-20322015e3a8',
    title: '袋鼠岛一日游',
    location: '阿德莱德出发',
    rating: 4.8,
    reviewCount: 126,
    price: 389,
    tags: ['热卖', '含午餐'],
  },
};

export const WithDiscount: Story = {
  args: {
    ...Default.args,
    originalPrice: 499,
    tags: ['特惠', '限时'],
  },
};

export const Grid: Story = {
  render: () => (
    <div style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(3, 300px)',
      gap: '1.5rem'
    }}>
      <ProductCard
        image="https://images.unsplash.com/photo-1494947665470-20322015e3a8"
        title="袋鼠岛一日游"
        location="阿德莱德出发"
        rating={4.8}
        reviewCount={126}
        price={389}
        tags={['热卖']}
      />
      <ProductCard
        image="https://images.unsplash.com/photo-1506905925346-21bda4d32df4"
        title="巴罗莎谷酒庄之旅"
        location="阿德莱德出发"
        rating={4.9}
        reviewCount={89}
        price={299}
        originalPrice={399}
        tags={['特惠', '含品酒']}
      />
      <ProductCard
        image="https://images.unsplash.com/photo-1540202403-b7abd6747a18"
        title="海豚巡航体验"
        location="格雷尔海滩"
        rating={4.7}
        reviewCount={234}
        price={159}
        tags={['亲子']}
      />
    </div>
  ),
};

2. 按钮组件 (Button)

// Button.tsx
import React from 'react';
import styles from './Button.module.css';

export interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  fullWidth?: boolean;
  loading?: boolean;
  disabled?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  children: React.ReactNode;
  onClick?: () => void;
}

export const Button: React.FC<ButtonProps> = ({
  variant = 'primary',
  size = 'md',
  fullWidth = false,
  loading = false,
  disabled = false,
  leftIcon,
  rightIcon,
  children,
  onClick,
}) => {
  return (
    <button
      className={`
        ${styles.button}
        ${styles[variant]}
        ${styles[size]}
        ${fullWidth ? styles.fullWidth : ''}
      `}
      disabled={disabled || loading}
      onClick={onClick}
    >
      {loading ? (
        <span className={styles.spinner} />
      ) : (
        <>
          {leftIcon && <span className={styles.icon}>{leftIcon}</span>}
          <span>{children}</span>
          {rightIcon && <span className={styles.icon}>{rightIcon}</span>}
        </>
      )}
    </button>
  );
};

Storybook 配置

.storybook/main.ts

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

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

export default config;

.storybook/preview.ts

import type { Preview } from '@storybook/react';
import '../src/styles/variables.css';
import '../src/styles/typography.css';

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
    backgrounds: {
      default: 'light',
      values: [
        { name: 'light', value: '#ffffff' },
        { name: 'gray', value: '#f8fafc' },
        { name: 'dark', value: '#0f172a' },
      ],
    },
  },
  globalTypes: {
    theme: {
      description: 'Global theme for components',
      defaultValue: 'light',
      toolbar: {
        title: 'Theme',
        icon: 'circlehollow',
        items: ['light', 'dark'],
        dynamicTitle: true,
      },
    },
  },
};

export default preview;

快速启动命令

创建新组件

# 创建组件目录
mkdir -p src/components/ComponentName

# 创建文件
touch src/components/ComponentName/{ComponentName.tsx,ComponentName.stories.tsx,ComponentName.module.css,index.ts}

安装 Storybook

# 初始化 Storybook
npx storybook@latest init

# 安装额外插件
npm install -D @storybook/addon-a11y @storybook/addon-interactions

# 启动 Storybook
npm run storybook

设计检查清单

组件质量检查

  • Props 接口定义完整,带 JSDoc 注释
  • 支持必要的变体(variant)和尺寸(size
  • 处理禁用和加载状态
  • 支持自定义 className
  • 键盘可访问性
  • 屏幕阅读器友好

Storybook 质量检查

  • 所有变体都有对应 Story
  • argTypes 配置完整
  • 包含组件文档描述
  • 交互状态可测试
  • 响应式展示

视觉质量检查

  • 字体选择有特色
  • 配色方案协调
  • 动画流畅自然
  • 间距一致
  • 暗色主题支持