UI Design by constellos/claude-code-plugins
npx skills add https://github.com/constellos/claude-code-plugins --skill 'UI Design'采用 TypeScript 接口和复合组件的契约优先静态 UI 方法论。
UI 设计是 UI 开发工作流程中的第二步,紧随线框图之后。首先定义 TypeScript 接口(即“契约”),然后使用 Tailwind CSS 样式实现复合组件。默认使用服务器组件。
在实现之前先定义 TypeScript 接口:
// 1. Define the contract first
interface FeatureCardProps {
title: string;
description: string;
icon: IconName;
href?: string;
}
// 2. Then implement the component
function FeatureCard({ title, description, icon, href }: FeatureCardProps) {
// Implementation
}
所有组件都是 React 服务器组件,除非它们需要:
事件处理器(onClick、onChange)
浏览器 API(localStorage、window)
React 钩子(useState、useEffect)
// Server Component (default) - no "use client" directive export function FeatureCard({ title, description }: FeatureCardProps) { return ( <article className="rounded-lg border p-6"> <h3 className="text-lg font-semibold">{title}</h3> <p className="text-muted-foreground">{description}</p> </article> ); }
使用可组合的部分构建复杂组件:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// Root component provides context
function Card({ children, className }: CardProps) {
return (
<div className={cn("rounded-lg border bg-card", className)}>
{children}
</div>
);
}
// Sub-components for each section
function CardHeader({ children, className }: CardHeaderProps) {
return (
<div className={cn("flex flex-col space-y-1.5 p-6", className)}>
{children}
</div>
);
}
function CardTitle({ children, className }: CardTitleProps) {
return (
<h3 className={cn("text-2xl font-semibold", className)}>
{children}
</h3>
);
}
function CardContent({ children, className }: CardContentProps) {
return (
<div className={cn("p-6 pt-0", className)}>
{children}
</div>
);
}
function CardFooter({ children, className }: CardFooterProps) {
return (
<div className={cn("flex items-center p-6 pt-0", className)}>
{children}
</div>
);
}
// Export compound component
export { Card, CardHeader, CardTitle, CardContent, CardFooter };
从组件契约开始:
// types.ts
import type { ReactNode } from 'react';
export interface CardProps {
children: ReactNode;
className?: string;
variant?: 'default' | 'outlined' | 'elevated';
}
export interface CardHeaderProps {
children: ReactNode;
className?: string;
}
export interface CardTitleProps {
children: ReactNode;
className?: string;
as?: 'h1' | 'h2' | 'h3' | 'h4';
}
export interface CardDescriptionProps {
children: ReactNode;
className?: string;
}
export interface CardContentProps {
children: ReactNode;
className?: string;
}
export interface CardFooterProps {
children: ReactNode;
className?: string;
align?: 'start' | 'center' | 'end' | 'between';
}
实现复合组件的每个部分:
// card.tsx
import { cn } from '@/lib/utils';
import type {
CardProps,
CardHeaderProps,
CardTitleProps,
CardDescriptionProps,
CardContentProps,
CardFooterProps,
} from './types';
const variantStyles = {
default: 'bg-card text-card-foreground',
outlined: 'border-2 bg-transparent',
elevated: 'bg-card shadow-lg',
} as const;
export function Card({ children, className, variant = 'default' }: CardProps) {
return (
<div
className={cn(
'rounded-lg border',
variantStyles[variant],
className
)}
>
{children}
</div>
);
}
export function CardHeader({ children, className }: CardHeaderProps) {
return (
<div className={cn('flex flex-col space-y-1.5 p-6', className)}>
{children}
</div>
);
}
export function CardTitle({
children,
className,
as: Tag = 'h3',
}: CardTitleProps) {
return (
<Tag className={cn('text-2xl font-semibold leading-none tracking-tight', className)}>
{children}
</Tag>
);
}
export function CardDescription({ children, className }: CardDescriptionProps) {
return (
<p className={cn('text-sm text-muted-foreground', className)}>
{children}
</p>
);
}
export function CardContent({ children, className }: CardContentProps) {
return (
<div className={cn('p-6 pt-0', className)}>
{children}
</div>
);
}
const alignStyles = {
start: 'justify-start',
center: 'justify-center',
end: 'justify-end',
between: 'justify-between',
} as const;
export function CardFooter({
children,
className,
align = 'start',
}: CardFooterProps) {
return (
<div className={cn('flex items-center p-6 pt-0', alignStyles[align], className)}>
{children}
</div>
);
}
在 index.ts 中组织导出:
// index.ts
export {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from './card';
export type {
CardProps,
CardHeaderProps,
CardTitleProps,
CardDescriptionProps,
CardContentProps,
CardFooterProps,
} from './types';
应用移动优先的响应式样式:
export function FeatureGrid({ features }: FeatureGridProps) {
return (
<div className={cn(
// Mobile: single column
'grid grid-cols-1 gap-4',
// Tablet: two columns
'md:grid-cols-2 md:gap-6',
// Desktop: three columns
'lg:grid-cols-3 lg:gap-8'
)}>
{features.map((feature) => (
<Card key={feature.id}>
<CardHeader>
<CardTitle>{feature.title}</CardTitle>
<CardDescription>{feature.description}</CardDescription>
</CardHeader>
</Card>
))}
</div>
);
}
安装并使用 Shadcn 组件作为基础:
npx shadcn@latest add button card input
需要时扩展 Shadcn 组件:
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
interface LoadingButtonProps extends React.ComponentProps<typeof Button> {
loading?: boolean;
}
export function LoadingButton({
loading,
children,
disabled,
className,
...props
}: LoadingButtonProps) {
return (
<Button
disabled={disabled || loading}
className={cn(loading && 'cursor-wait', className)}
{...props}
>
{loading ? <Spinner className="mr-2 h-4 w-4" /> : null}
{children}
</Button>
);
}
使用 Radix 获取无障碍、无样式的原始组件:
import * as Dialog from '@radix-ui/react-dialog';
export function Modal({ open, onOpenChange, children }: ModalProps) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6">
{children}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
使用 AI 元素处理 AI 特定的 UI 模式:
import { Chat, Message, MessageInput } from '@ai-elements/react';
export function ChatInterface({ messages, onSend }: ChatInterfaceProps) {
return (
<Chat>
{messages.map((msg) => (
<Message key={msg.id} role={msg.role}>
{msg.content}
</Message>
))}
<MessageInput onSubmit={onSend} />
</Chat>
);
}
一致地组织组件文件:
components/
└── feature-card/
├── index.ts # Barrel exports
├── types.ts # TypeScript interfaces
├── feature-card.tsx # Main component
├── feature-card.test.tsx # Tests
└── WIREFRAME.md # Layout documentation
对于复合组件:
components/
└── card/
├── index.ts # Exports all parts
├── types.ts # All interfaces
├── card.tsx # Root component
├── card-header.tsx # Sub-component
├── card-content.tsx # Sub-component
├── card-footer.tsx # Sub-component
└── card.test.tsx # Tests
优先使用可组合的组件,而非属性繁多的组件:
// Avoid: Too many props
<Card
title="Feature"
description="Description"
footer={<Button>Action</Button>}
headerIcon={<Icon />}
/>
// Prefer: Composable structure
<Card>
<CardHeader>
<Icon />
<CardTitle>Feature</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
使用可辨识联合类型处理变体:
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps {
variant?: ButtonVariant;
size?: ButtonSize;
children: ReactNode;
}
const variantStyles: Record<ButtonVariant, string> = {
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
};
const sizeStyles: Record<ButtonSize, string> = {
sm: 'h-8 px-3 text-xs',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base',
};
支持引用转发以访问 DOM:
import { forwardRef } from 'react';
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border bg-background px-3 py-2',
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = 'Input';
any,对于泛型输入优先使用 unknowntext-foreground、bg-background,而非原始颜色在进入交互实现之前:
静态 UI 完成后,继续学习 ui-interaction 技能,以添加客户端事件、本地状态和表单验证。
每周安装量
0
代码仓库
GitHub 星标数
5
首次出现
1970年1月1日
安全审计
Contract-first static UI methodology using TypeScript interfaces and compound components.
UI Design is the second step in the UI development workflow, following wireframing. Define TypeScript interfaces first (the "contract"), then implement compound components with Tailwind CSS styling. Server Components are the default.
Define TypeScript interfaces before implementation:
// 1. Define the contract first
interface FeatureCardProps {
title: string;
description: string;
icon: IconName;
href?: string;
}
// 2. Then implement the component
function FeatureCard({ title, description, icon, href }: FeatureCardProps) {
// Implementation
}
All components are React Server Components unless they need:
Event handlers (onClick, onChange)
Browser APIs (localStorage, window)
React hooks (useState, useEffect)
// Server Component (default) - no "use client" directive export function FeatureCard({ title, description }: FeatureCardProps) { return ( <article className="rounded-lg border p-6"> <h3 className="text-lg font-semibold">{title}</h3> <p className="text-muted-foreground">{description}</p> </article> ); }
Structure complex components with composable parts:
// Root component provides context
function Card({ children, className }: CardProps) {
return (
<div className={cn("rounded-lg border bg-card", className)}>
{children}
</div>
);
}
// Sub-components for each section
function CardHeader({ children, className }: CardHeaderProps) {
return (
<div className={cn("flex flex-col space-y-1.5 p-6", className)}>
{children}
</div>
);
}
function CardTitle({ children, className }: CardTitleProps) {
return (
<h3 className={cn("text-2xl font-semibold", className)}>
{children}
</h3>
);
}
function CardContent({ children, className }: CardContentProps) {
return (
<div className={cn("p-6 pt-0", className)}>
{children}
</div>
);
}
function CardFooter({ children, className }: CardFooterProps) {
return (
<div className={cn("flex items-center p-6 pt-0", className)}>
{children}
</div>
);
}
// Export compound component
export { Card, CardHeader, CardTitle, CardContent, CardFooter };
Start with the component contract:
// types.ts
import type { ReactNode } from 'react';
export interface CardProps {
children: ReactNode;
className?: string;
variant?: 'default' | 'outlined' | 'elevated';
}
export interface CardHeaderProps {
children: ReactNode;
className?: string;
}
export interface CardTitleProps {
children: ReactNode;
className?: string;
as?: 'h1' | 'h2' | 'h3' | 'h4';
}
export interface CardDescriptionProps {
children: ReactNode;
className?: string;
}
export interface CardContentProps {
children: ReactNode;
className?: string;
}
export interface CardFooterProps {
children: ReactNode;
className?: string;
align?: 'start' | 'center' | 'end' | 'between';
}
Implement each part of the compound component:
// card.tsx
import { cn } from '@/lib/utils';
import type {
CardProps,
CardHeaderProps,
CardTitleProps,
CardDescriptionProps,
CardContentProps,
CardFooterProps,
} from './types';
const variantStyles = {
default: 'bg-card text-card-foreground',
outlined: 'border-2 bg-transparent',
elevated: 'bg-card shadow-lg',
} as const;
export function Card({ children, className, variant = 'default' }: CardProps) {
return (
<div
className={cn(
'rounded-lg border',
variantStyles[variant],
className
)}
>
{children}
</div>
);
}
export function CardHeader({ children, className }: CardHeaderProps) {
return (
<div className={cn('flex flex-col space-y-1.5 p-6', className)}>
{children}
</div>
);
}
export function CardTitle({
children,
className,
as: Tag = 'h3',
}: CardTitleProps) {
return (
<Tag className={cn('text-2xl font-semibold leading-none tracking-tight', className)}>
{children}
</Tag>
);
}
export function CardDescription({ children, className }: CardDescriptionProps) {
return (
<p className={cn('text-sm text-muted-foreground', className)}>
{children}
</p>
);
}
export function CardContent({ children, className }: CardContentProps) {
return (
<div className={cn('p-6 pt-0', className)}>
{children}
</div>
);
}
const alignStyles = {
start: 'justify-start',
center: 'justify-center',
end: 'justify-end',
between: 'justify-between',
} as const;
export function CardFooter({
children,
className,
align = 'start',
}: CardFooterProps) {
return (
<div className={cn('flex items-center p-6 pt-0', alignStyles[align], className)}>
{children}
</div>
);
}
Organize exports in index.ts:
// index.ts
export {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from './card';
export type {
CardProps,
CardHeaderProps,
CardTitleProps,
CardDescriptionProps,
CardContentProps,
CardFooterProps,
} from './types';
Apply mobile-first responsive styling:
export function FeatureGrid({ features }: FeatureGridProps) {
return (
<div className={cn(
// Mobile: single column
'grid grid-cols-1 gap-4',
// Tablet: two columns
'md:grid-cols-2 md:gap-6',
// Desktop: three columns
'lg:grid-cols-3 lg:gap-8'
)}>
{features.map((feature) => (
<Card key={feature.id}>
<CardHeader>
<CardTitle>{feature.title}</CardTitle>
<CardDescription>{feature.description}</CardDescription>
</CardHeader>
</Card>
))}
</div>
);
}
Install and use Shadcn components as the foundation:
npx shadcn@latest add button card input
Reference: https://ui.shadcn.com/docs/components
Extend Shadcn components when needed:
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
interface LoadingButtonProps extends React.ComponentProps<typeof Button> {
loading?: boolean;
}
export function LoadingButton({
loading,
children,
disabled,
className,
...props
}: LoadingButtonProps) {
return (
<Button
disabled={disabled || loading}
className={cn(loading && 'cursor-wait', className)}
{...props}
>
{loading ? <Spinner className="mr-2 h-4 w-4" /> : null}
{children}
</Button>
);
}
Use Radix for accessible, unstyled primitives:
import * as Dialog from '@radix-ui/react-dialog';
export function Modal({ open, onOpenChange, children }: ModalProps) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6">
{children}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
Reference: https://www.radix-ui.com/primitives/docs/overview/introduction
Use AI Elements for AI-specific UI patterns:
import { Chat, Message, MessageInput } from '@ai-elements/react';
export function ChatInterface({ messages, onSend }: ChatInterfaceProps) {
return (
<Chat>
{messages.map((msg) => (
<Message key={msg.id} role={msg.role}>
{msg.content}
</Message>
))}
<MessageInput onSubmit={onSend} />
</Chat>
);
}
Organize component files consistently:
components/
└── feature-card/
├── index.ts # Barrel exports
├── types.ts # TypeScript interfaces
├── feature-card.tsx # Main component
├── feature-card.test.tsx # Tests
└── WIREFRAME.md # Layout documentation
For compound components:
components/
└── card/
├── index.ts # Exports all parts
├── types.ts # All interfaces
├── card.tsx # Root component
├── card-header.tsx # Sub-component
├── card-content.tsx # Sub-component
├── card-footer.tsx # Sub-component
└── card.test.tsx # Tests
Prefer composable components over prop-heavy ones:
// Avoid: Too many props
<Card
title="Feature"
description="Description"
footer={<Button>Action</Button>}
headerIcon={<Icon />}
/>
// Prefer: Composable structure
<Card>
<CardHeader>
<Icon />
<CardTitle>Feature</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
Use discriminated unions for variants:
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps {
variant?: ButtonVariant;
size?: ButtonSize;
children: ReactNode;
}
const variantStyles: Record<ButtonVariant, string> = {
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
};
const sizeStyles: Record<ButtonSize, string> = {
sm: 'h-8 px-3 text-xs',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base',
};
Support ref forwarding for DOM access:
import { forwardRef } from 'react';
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border bg-background px-3 py-2',
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = 'Input';
any, prefer unknown for generic inputstext-foreground, bg-background, not raw colorsBefore proceeding to interaction implementation:
After static UI is complete, proceed to the ui-interaction skill for adding client-side events, local state, and form validation.
Weekly Installs
0
Repository
GitHub Stars
5
First Seen
Jan 1, 1970
Security Audits
World Monitor 智能仪表板:AI驱动的全球实时监控与地缘风险分析平台
562 周安装