重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
atomic-design-atoms by thebushidocollective/han
npx skills add https://github.com/thebushidocollective/han --skill atomic-design-atoms掌握原子组件的创建——这是您设计系统中基础的、不可分割的构建块。原子是最小的功能单元,进一步拆分将失去其意义。
原子是基本的 UI 元素,是设计系统中所有其他事物的基础。它们具有以下特点:
// atoms/Button/Button.tsx
import React from 'react';
import type { ButtonHTMLAttributes } from 'react';
import styles from './Button.module.css';
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'danger';
export type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** 视觉样式变体 */
variant?: ButtonVariant;
/** 按钮尺寸 */
size?: ButtonSize;
/** 全宽按钮 */
fullWidth?: boolean;
/** 加载状态 */
isLoading?: boolean;
/** 左侧图标 */
leftIcon?: React.ReactNode;
/** 右侧图标 */
rightIcon?: React.ReactNode;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = 'primary',
size = 'md',
fullWidth = false,
isLoading = false,
leftIcon,
rightIcon,
disabled,
children,
className,
...props
},
ref
) => {
const classNames = [
styles.button,
styles[variant],
styles[size],
fullWidth && styles.fullWidth,
isLoading && styles.loading,
className,
]
.filter(Boolean)
.join(' ');
return (
<button
ref={ref}
className={classNames}
disabled={disabled || isLoading}
aria-busy={isLoading}
{...props}
>
{isLoading ? (
<span className={styles.spinner} aria-hidden="true" />
) : (
<>
{leftIcon && <span className={styles.leftIcon}>{leftIcon}</span>}
<span className={styles.content}>{children}</span>
{rightIcon && <span className={styles.rightIcon}>{rightIcon}</span>}
</>
)}
</button>
);
}
);
Button.displayName = 'Button';
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
/* atoms/Button/Button.module.css */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 150ms ease-in-out;
text-decoration: none;
}
.button:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 变体 */
.primary {
background-color: var(--color-primary-500);
color: var(--color-white);
}
.primary:hover:not(:disabled) {
background-color: var(--color-primary-600);
}
.secondary {
background-color: transparent;
color: var(--color-primary-500);
border: 1px solid var(--color-primary-500);
}
.secondary:hover:not(:disabled) {
background-color: var(--color-primary-50);
}
.tertiary {
background-color: transparent;
color: var(--color-primary-500);
}
.tertiary:hover:not(:disabled) {
background-color: var(--color-primary-50);
}
.danger {
background-color: var(--color-danger-500);
color: var(--color-white);
}
.danger:hover:not(:disabled) {
background-color: var(--color-danger-600);
}
/* 尺寸 */
.sm {
padding: 6px 12px;
font-size: 14px;
min-height: 32px;
}
.md {
padding: 8px 16px;
font-size: 16px;
min-height: 40px;
}
.lg {
padding: 12px 24px;
font-size: 18px;
min-height: 48px;
}
/* 修饰符 */
.fullWidth {
width: 100%;
}
.loading {
position: relative;
color: transparent;
}
.spinner {
position: absolute;
width: 16px;
height: 16px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.75s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 图标间距 */
.leftIcon,
.rightIcon {
display: flex;
align-items: center;
}
// atoms/Input/Input.tsx
import React from 'react';
import type { InputHTMLAttributes } from 'react';
import styles from './Input.module.css';
export type InputSize = 'sm' | 'md' | 'lg';
export interface InputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
/** 尺寸变体 */
size?: InputSize;
/** 错误状态 */
hasError?: boolean;
/** 左侧附加元素 */
leftAddon?: React.ReactNode;
/** 右侧附加元素 */
rightAddon?: React.ReactNode;
}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
(
{
size = 'md',
hasError = false,
leftAddon,
rightAddon,
disabled,
className,
...props
},
ref
) => {
const wrapperClasses = [
styles.wrapper,
styles[size],
hasError && styles.error,
disabled && styles.disabled,
className,
]
.filter(Boolean)
.join(' ');
return (
<div className={wrapperClasses}>
{leftAddon && <span className={styles.leftAddon}>{leftAddon}</span>}
<input
ref={ref}
className={styles.input}
disabled={disabled}
aria-invalid={hasError}
{...props}
/>
{rightAddon && <span className={styles.rightAddon}>{rightAddon}</span>}
</div>
);
}
);
Input.displayName = 'Input';
/* atoms/Input/Input.module.css */
.wrapper {
display: flex;
align-items: center;
border: 1px solid var(--color-neutral-300);
border-radius: 6px;
background-color: var(--color-white);
transition: border-color 150ms, box-shadow 150ms;
}
.wrapper:focus-within {
border-color: var(--color-primary-500);
box-shadow: 0 0 0 3px var(--color-primary-100);
}
.input {
flex: 1;
border: none;
background: transparent;
outline: none;
width: 100%;
}
.input::placeholder {
color: var(--color-neutral-400);
}
/* 错误状态 */
.error {
border-color: var(--color-danger-500);
}
.error:focus-within {
border-color: var(--color-danger-500);
box-shadow: 0 0 0 3px var(--color-danger-100);
}
/* 禁用状态 */
.disabled {
background-color: var(--color-neutral-100);
cursor: not-allowed;
}
.disabled .input {
cursor: not-allowed;
}
/* 尺寸 */
.sm {
min-height: 32px;
}
.sm .input {
padding: 6px 12px;
font-size: 14px;
}
.md {
min-height: 40px;
}
.md .input {
padding: 8px 12px;
font-size: 16px;
}
.lg {
min-height: 48px;
}
.lg .input {
padding: 12px 16px;
font-size: 18px;
}
/* 附加元素 */
.leftAddon,
.rightAddon {
display: flex;
align-items: center;
padding: 0 12px;
color: var(--color-neutral-500);
}
// atoms/Label/Label.tsx
import React from 'react';
import type { LabelHTMLAttributes } from 'react';
import styles from './Label.module.css';
export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
/** 指示必填字段 */
required?: boolean;
/** 禁用状态样式 */
disabled?: boolean;
}
export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
({ required = false, disabled = false, children, className, ...props }, ref) => {
const classNames = [
styles.label,
disabled && styles.disabled,
className,
]
.filter(Boolean)
.join(' ');
return (
<label ref={ref} className={classNames} {...props}>
{children}
{required && (
<span className={styles.required} aria-hidden="true">
*
</span>
)}
</label>
);
}
);
Label.displayName = 'Label';
// atoms/Icon/Icon.tsx
import React from 'react';
export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
const sizeMap: Record<IconSize, number> = {
xs: 12,
sm: 16,
md: 20,
lg: 24,
xl: 32,
};
export interface IconProps extends React.SVGAttributes<SVGElement> {
/** 图标名称/标识符 */
name: string;
/** 图标尺寸 */
size?: IconSize;
/** 自定义颜色 */
color?: string;
/** 无障碍标签 */
label?: string;
}
export const Icon: React.FC<IconProps> = ({
name,
size = 'md',
color = 'currentColor',
label,
className,
...props
}) => {
const pixelSize = sizeMap[size];
return (
<svg
className={className}
width={pixelSize}
height={pixelSize}
fill={color}
aria-label={label}
aria-hidden={!label}
role={label ? 'img' : 'presentation'}
{...props}
>
<use href={`/icons.svg#${name}`} />
</svg>
);
};
Icon.displayName = 'Icon';
// atoms/Avatar/Avatar.tsx
import React from 'react';
import styles from './Avatar.module.css';
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export interface AvatarProps {
/** 图片源 URL */
src?: string;
/** 图片替代文本 */
alt: string;
/** 备用首字母 */
initials?: string;
/** 尺寸变体 */
size?: AvatarSize;
/** 额外的类名 */
className?: string;
}
export const Avatar: React.FC<AvatarProps> = ({
src,
alt,
initials,
size = 'md',
className,
}) => {
const [imageError, setImageError] = React.useState(false);
const classNames = [styles.avatar, styles[size], className]
.filter(Boolean)
.join(' ');
const showImage = src && !imageError;
const showInitials = !showImage && initials;
return (
<div className={classNames} role="img" aria-label={alt}>
{showImage && (
<img
src={src}
alt={alt}
className={styles.image}
onError={() => setImageError(true)}
/>
)}
{showInitials && (
<span className={styles.initials} aria-hidden="true">
{initials}
</span>
)}
{!showImage && !showInitials && (
<span className={styles.placeholder} aria-hidden="true">
?
</span>
)}
</div>
);
};
Avatar.displayName = 'Avatar';
// atoms/Badge/Badge.tsx
import React from 'react';
import styles from './Badge.module.css';
export type BadgeVariant =
| 'default'
| 'primary'
| 'success'
| 'warning'
| 'danger'
| 'info';
export type BadgeSize = 'sm' | 'md';
export interface BadgeProps {
/** 视觉变体 */
variant?: BadgeVariant;
/** 尺寸变体 */
size?: BadgeSize;
/** 徽章内容 */
children: React.ReactNode;
/** 额外的类名 */
className?: string;
}
export const Badge: React.FC<BadgeProps> = ({
variant = 'default',
size = 'md',
children,
className,
}) => {
const classNames = [styles.badge, styles[variant], styles[size], className]
.filter(Boolean)
.join(' ');
return <span className={classNames}>{children}</span>;
};
Badge.displayName = 'Badge';
// atoms/Checkbox/Checkbox.tsx
import React from 'react';
import type { InputHTMLAttributes } from 'react';
import styles from './Checkbox.module.css';
export interface CheckboxProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
/** 不确定状态 */
indeterminate?: boolean;
/** 标签文本 */
label?: string;
}
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
({ indeterminate = false, label, disabled, className, ...props }, ref) => {
const inputRef = React.useRef<HTMLInputElement>(null);
React.useImperativeHandle(ref, () => inputRef.current!);
React.useEffect(() => {
if (inputRef.current) {
inputRef.current.indeterminate = indeterminate;
}
}, [indeterminate]);
const wrapperClasses = [
styles.wrapper,
disabled && styles.disabled,
className,
]
.filter(Boolean)
.join(' ');
const checkbox = (
<span className={styles.checkbox}>
<input
ref={inputRef}
type="checkbox"
className={styles.input}
disabled={disabled}
{...props}
/>
<span className={styles.control} aria-hidden="true">
<svg className={styles.check} viewBox="0 0 12 10">
<polyline points="1.5 6 4.5 9 10.5 1" />
</svg>
<svg className={styles.indeterminate} viewBox="0 0 12 2">
<line x1="1" y1="1" x2="11" y2="1" />
</svg>
</span>
</span>
);
if (label) {
return (
<label className={wrapperClasses}>
{checkbox}
<span className={styles.label}>{label}</span>
</label>
);
}
return checkbox;
}
);
Checkbox.displayName = 'Checkbox';
// atoms/Typography/Text.tsx
import React from 'react';
import styles from './Typography.module.css';
export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export type TextWeight = 'normal' | 'medium' | 'semibold' | 'bold';
export type TextColor = 'default' | 'muted' | 'primary' | 'success' | 'danger';
export interface TextProps {
as?: 'p' | 'span' | 'div';
size?: TextSize;
weight?: TextWeight;
color?: TextColor;
truncate?: boolean;
children: React.ReactNode;
className?: string;
}
export const Text: React.FC<TextProps> = ({
as: Component = 'p',
size = 'md',
weight = 'normal',
color = 'default',
truncate = false,
children,
className,
}) => {
const classNames = [
styles.text,
styles[`size-${size}`],
styles[`weight-${weight}`],
styles[`color-${color}`],
truncate && styles.truncate,
className,
]
.filter(Boolean)
.join(' ');
return <Component className={classNames}>{children}</Component>;
};
// atoms/Typography/Heading.tsx
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
export interface HeadingProps {
level: HeadingLevel;
as?: `h${HeadingLevel}`;
children: React.ReactNode;
className?: string;
}
export const Heading: React.FC<HeadingProps> = ({
level,
as,
children,
className,
}) => {
const Component = as || (`h${level}` as const);
const classNames = [styles.heading, styles[`h${level}`], className]
.filter(Boolean)
.join(' ');
return <Component className={classNames}>{children}</Component>;
};
// 良好:允许父组件访问 DOM 节点
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
(props, ref) => <input ref={ref} {...props} />
);
// 不良:父组件无法访问 DOM
export const Input = (props: InputProps) => <input {...props} />;
// 良好:支持所有原生按钮属性
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
}
// 不良:缺少原生属性
interface ButtonProps {
onClick?: () => void;
disabled?: boolean;
}
// 良好:开箱即用
export const Button = ({
variant = 'primary',
size = 'md',
type = 'button', // 防止意外的表单提交
...props
}) => { ... };
// 不良:需要显式传递属性
export const Button = ({ variant, size, ...props }) => { ... };
// 良好:没有业务逻辑
const Button = ({ onClick, children }) => (
<button onClick={onClick}>{children}</button>
);
// 不良:原子包含 API 调用
const SubmitButton = () => {
const handleClick = async () => {
await api.submit(); // 原子中包含业务逻辑!
};
return <button onClick={handleClick}>提交</button>;
};
// 不良:原子管理自己的状态
const Input = () => {
const [value, setValue] = useState('');
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
};
// 良好:由父组件控制
const Input = ({ value, onChange }) => (
<input value={value} onChange={onChange} />
);
// 不良:原子中包含复杂的验证逻辑
const EmailInput = ({ value, onChange }) => {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
return <input value={value} className={isValid ? '' : 'error'} />;
};
// 良好:验证由父组件/分子处理
const Input = ({ value, onChange, hasError }) => (
<input value={value} className={hasError ? 'error' : ''} />
);
// 不良:硬编码颜色
const Button = () => (
<button style={{ backgroundColor: '#2196f3' }}>点击</button>
);
// 良好:使用设计令牌
const Button = () => (
<button style={{ backgroundColor: 'var(--color-primary-500)' }}>
点击
</button>
);
atomic-design-fundamentals - 核心方法论概述atomic-design-molecules - 将原子组合成分子每周安装量
54
代码仓库
GitHub 星标数
129
首次出现
2026年1月24日
安全审计
安装于
opencode46
codex46
gemini-cli43
github-copilot42
cursor36
kimi-cli35
Master the creation of atomic components - the fundamental, indivisible building blocks of your design system. Atoms are the smallest functional units that cannot be broken down further without losing meaning.
Atoms are the basic UI elements that serve as the foundation for everything else in your design system. They are:
// atoms/Button/Button.tsx
import React from 'react';
import type { ButtonHTMLAttributes } from 'react';
import styles from './Button.module.css';
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'danger';
export type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Visual style variant */
variant?: ButtonVariant;
/** Size of the button */
size?: ButtonSize;
/** Full width button */
fullWidth?: boolean;
/** Loading state */
isLoading?: boolean;
/** Left icon */
leftIcon?: React.ReactNode;
/** Right icon */
rightIcon?: React.ReactNode;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = 'primary',
size = 'md',
fullWidth = false,
isLoading = false,
leftIcon,
rightIcon,
disabled,
children,
className,
...props
},
ref
) => {
const classNames = [
styles.button,
styles[variant],
styles[size],
fullWidth && styles.fullWidth,
isLoading && styles.loading,
className,
]
.filter(Boolean)
.join(' ');
return (
<button
ref={ref}
className={classNames}
disabled={disabled || isLoading}
aria-busy={isLoading}
{...props}
>
{isLoading ? (
<span className={styles.spinner} aria-hidden="true" />
) : (
<>
{leftIcon && <span className={styles.leftIcon}>{leftIcon}</span>}
<span className={styles.content}>{children}</span>
{rightIcon && <span className={styles.rightIcon}>{rightIcon}</span>}
</>
)}
</button>
);
}
);
Button.displayName = 'Button';
/* atoms/Button/Button.module.css */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 150ms ease-in-out;
text-decoration: none;
}
.button:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Variants */
.primary {
background-color: var(--color-primary-500);
color: var(--color-white);
}
.primary:hover:not(:disabled) {
background-color: var(--color-primary-600);
}
.secondary {
background-color: transparent;
color: var(--color-primary-500);
border: 1px solid var(--color-primary-500);
}
.secondary:hover:not(:disabled) {
background-color: var(--color-primary-50);
}
.tertiary {
background-color: transparent;
color: var(--color-primary-500);
}
.tertiary:hover:not(:disabled) {
background-color: var(--color-primary-50);
}
.danger {
background-color: var(--color-danger-500);
color: var(--color-white);
}
.danger:hover:not(:disabled) {
background-color: var(--color-danger-600);
}
/* Sizes */
.sm {
padding: 6px 12px;
font-size: 14px;
min-height: 32px;
}
.md {
padding: 8px 16px;
font-size: 16px;
min-height: 40px;
}
.lg {
padding: 12px 24px;
font-size: 18px;
min-height: 48px;
}
/* Modifiers */
.fullWidth {
width: 100%;
}
.loading {
position: relative;
color: transparent;
}
.spinner {
position: absolute;
width: 16px;
height: 16px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.75s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Icon spacing */
.leftIcon,
.rightIcon {
display: flex;
align-items: center;
}
// atoms/Input/Input.tsx
import React from 'react';
import type { InputHTMLAttributes } from 'react';
import styles from './Input.module.css';
export type InputSize = 'sm' | 'md' | 'lg';
export interface InputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
/** Size variant */
size?: InputSize;
/** Error state */
hasError?: boolean;
/** Left addon element */
leftAddon?: React.ReactNode;
/** Right addon element */
rightAddon?: React.ReactNode;
}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
(
{
size = 'md',
hasError = false,
leftAddon,
rightAddon,
disabled,
className,
...props
},
ref
) => {
const wrapperClasses = [
styles.wrapper,
styles[size],
hasError && styles.error,
disabled && styles.disabled,
className,
]
.filter(Boolean)
.join(' ');
return (
<div className={wrapperClasses}>
{leftAddon && <span className={styles.leftAddon}>{leftAddon}</span>}
<input
ref={ref}
className={styles.input}
disabled={disabled}
aria-invalid={hasError}
{...props}
/>
{rightAddon && <span className={styles.rightAddon}>{rightAddon}</span>}
</div>
);
}
);
Input.displayName = 'Input';
/* atoms/Input/Input.module.css */
.wrapper {
display: flex;
align-items: center;
border: 1px solid var(--color-neutral-300);
border-radius: 6px;
background-color: var(--color-white);
transition: border-color 150ms, box-shadow 150ms;
}
.wrapper:focus-within {
border-color: var(--color-primary-500);
box-shadow: 0 0 0 3px var(--color-primary-100);
}
.input {
flex: 1;
border: none;
background: transparent;
outline: none;
width: 100%;
}
.input::placeholder {
color: var(--color-neutral-400);
}
/* Error state */
.error {
border-color: var(--color-danger-500);
}
.error:focus-within {
border-color: var(--color-danger-500);
box-shadow: 0 0 0 3px var(--color-danger-100);
}
/* Disabled state */
.disabled {
background-color: var(--color-neutral-100);
cursor: not-allowed;
}
.disabled .input {
cursor: not-allowed;
}
/* Sizes */
.sm {
min-height: 32px;
}
.sm .input {
padding: 6px 12px;
font-size: 14px;
}
.md {
min-height: 40px;
}
.md .input {
padding: 8px 12px;
font-size: 16px;
}
.lg {
min-height: 48px;
}
.lg .input {
padding: 12px 16px;
font-size: 18px;
}
/* Addons */
.leftAddon,
.rightAddon {
display: flex;
align-items: center;
padding: 0 12px;
color: var(--color-neutral-500);
}
// atoms/Label/Label.tsx
import React from 'react';
import type { LabelHTMLAttributes } from 'react';
import styles from './Label.module.css';
export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
/** Indicates required field */
required?: boolean;
/** Disabled state styling */
disabled?: boolean;
}
export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
({ required = false, disabled = false, children, className, ...props }, ref) => {
const classNames = [
styles.label,
disabled && styles.disabled,
className,
]
.filter(Boolean)
.join(' ');
return (
<label ref={ref} className={classNames} {...props}>
{children}
{required && (
<span className={styles.required} aria-hidden="true">
*
</span>
)}
</label>
);
}
);
Label.displayName = 'Label';
// atoms/Icon/Icon.tsx
import React from 'react';
export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
const sizeMap: Record<IconSize, number> = {
xs: 12,
sm: 16,
md: 20,
lg: 24,
xl: 32,
};
export interface IconProps extends React.SVGAttributes<SVGElement> {
/** Icon name/identifier */
name: string;
/** Icon size */
size?: IconSize;
/** Custom color */
color?: string;
/** Accessible label */
label?: string;
}
export const Icon: React.FC<IconProps> = ({
name,
size = 'md',
color = 'currentColor',
label,
className,
...props
}) => {
const pixelSize = sizeMap[size];
return (
<svg
className={className}
width={pixelSize}
height={pixelSize}
fill={color}
aria-label={label}
aria-hidden={!label}
role={label ? 'img' : 'presentation'}
{...props}
>
<use href={`/icons.svg#${name}`} />
</svg>
);
};
Icon.displayName = 'Icon';
// atoms/Avatar/Avatar.tsx
import React from 'react';
import styles from './Avatar.module.css';
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export interface AvatarProps {
/** Image source URL */
src?: string;
/** Alt text for image */
alt: string;
/** Fallback initials */
initials?: string;
/** Size variant */
size?: AvatarSize;
/** Additional class name */
className?: string;
}
export const Avatar: React.FC<AvatarProps> = ({
src,
alt,
initials,
size = 'md',
className,
}) => {
const [imageError, setImageError] = React.useState(false);
const classNames = [styles.avatar, styles[size], className]
.filter(Boolean)
.join(' ');
const showImage = src && !imageError;
const showInitials = !showImage && initials;
return (
<div className={classNames} role="img" aria-label={alt}>
{showImage && (
<img
src={src}
alt={alt}
className={styles.image}
onError={() => setImageError(true)}
/>
)}
{showInitials && (
<span className={styles.initials} aria-hidden="true">
{initials}
</span>
)}
{!showImage && !showInitials && (
<span className={styles.placeholder} aria-hidden="true">
?
</span>
)}
</div>
);
};
Avatar.displayName = 'Avatar';
// atoms/Badge/Badge.tsx
import React from 'react';
import styles from './Badge.module.css';
export type BadgeVariant =
| 'default'
| 'primary'
| 'success'
| 'warning'
| 'danger'
| 'info';
export type BadgeSize = 'sm' | 'md';
export interface BadgeProps {
/** Visual variant */
variant?: BadgeVariant;
/** Size variant */
size?: BadgeSize;
/** Badge content */
children: React.ReactNode;
/** Additional class name */
className?: string;
}
export const Badge: React.FC<BadgeProps> = ({
variant = 'default',
size = 'md',
children,
className,
}) => {
const classNames = [styles.badge, styles[variant], styles[size], className]
.filter(Boolean)
.join(' ');
return <span className={classNames}>{children}</span>;
};
Badge.displayName = 'Badge';
// atoms/Checkbox/Checkbox.tsx
import React from 'react';
import type { InputHTMLAttributes } from 'react';
import styles from './Checkbox.module.css';
export interface CheckboxProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
/** Indeterminate state */
indeterminate?: boolean;
/** Label text */
label?: string;
}
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
({ indeterminate = false, label, disabled, className, ...props }, ref) => {
const inputRef = React.useRef<HTMLInputElement>(null);
React.useImperativeHandle(ref, () => inputRef.current!);
React.useEffect(() => {
if (inputRef.current) {
inputRef.current.indeterminate = indeterminate;
}
}, [indeterminate]);
const wrapperClasses = [
styles.wrapper,
disabled && styles.disabled,
className,
]
.filter(Boolean)
.join(' ');
const checkbox = (
<span className={styles.checkbox}>
<input
ref={inputRef}
type="checkbox"
className={styles.input}
disabled={disabled}
{...props}
/>
<span className={styles.control} aria-hidden="true">
<svg className={styles.check} viewBox="0 0 12 10">
<polyline points="1.5 6 4.5 9 10.5 1" />
</svg>
<svg className={styles.indeterminate} viewBox="0 0 12 2">
<line x1="1" y1="1" x2="11" y2="1" />
</svg>
</span>
</span>
);
if (label) {
return (
<label className={wrapperClasses}>
{checkbox}
<span className={styles.label}>{label}</span>
</label>
);
}
return checkbox;
}
);
Checkbox.displayName = 'Checkbox';
// atoms/Typography/Text.tsx
import React from 'react';
import styles from './Typography.module.css';
export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export type TextWeight = 'normal' | 'medium' | 'semibold' | 'bold';
export type TextColor = 'default' | 'muted' | 'primary' | 'success' | 'danger';
export interface TextProps {
as?: 'p' | 'span' | 'div';
size?: TextSize;
weight?: TextWeight;
color?: TextColor;
truncate?: boolean;
children: React.ReactNode;
className?: string;
}
export const Text: React.FC<TextProps> = ({
as: Component = 'p',
size = 'md',
weight = 'normal',
color = 'default',
truncate = false,
children,
className,
}) => {
const classNames = [
styles.text,
styles[`size-${size}`],
styles[`weight-${weight}`],
styles[`color-${color}`],
truncate && styles.truncate,
className,
]
.filter(Boolean)
.join(' ');
return <Component className={classNames}>{children}</Component>;
};
// atoms/Typography/Heading.tsx
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
export interface HeadingProps {
level: HeadingLevel;
as?: `h${HeadingLevel}`;
children: React.ReactNode;
className?: string;
}
export const Heading: React.FC<HeadingProps> = ({
level,
as,
children,
className,
}) => {
const Component = as || (`h${level}` as const);
const classNames = [styles.heading, styles[`h${level}`], className]
.filter(Boolean)
.join(' ');
return <Component className={classNames}>{children}</Component>;
};
// GOOD: Allows parent to access DOM node
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
(props, ref) => <input ref={ref} {...props} />
);
// BAD: No way for parent to access DOM
export const Input = (props: InputProps) => <input {...props} />;
// GOOD: Supports all native button attributes
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
}
// BAD: Missing native attributes
interface ButtonProps {
onClick?: () => void;
disabled?: boolean;
}
// GOOD: Works out of the box
export const Button = ({
variant = 'primary',
size = 'md',
type = 'button', // Prevent accidental form submissions
...props
}) => { ... };
// BAD: Requires explicit props
export const Button = ({ variant, size, ...props }) => { ... };
// GOOD: No business logic
const Button = ({ onClick, children }) => (
<button onClick={onClick}>{children}</button>
);
// BAD: Atom has API call
const SubmitButton = () => {
const handleClick = async () => {
await api.submit(); // Business logic in atom!
};
return <button onClick={handleClick}>Submit</button>;
};
// BAD: Atom manages its own state
const Input = () => {
const [value, setValue] = useState('');
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
};
// GOOD: Controlled by parent
const Input = ({ value, onChange }) => (
<input value={value} onChange={onChange} />
);
// BAD: Complex validation in atom
const EmailInput = ({ value, onChange }) => {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
return <input value={value} className={isValid ? '' : 'error'} />;
};
// GOOD: Validation handled by parent/molecule
const Input = ({ value, onChange, hasError }) => (
<input value={value} className={hasError ? 'error' : ''} />
);
// BAD: Hardcoded colors
const Button = () => (
<button style={{ backgroundColor: '#2196f3' }}>Click</button>
);
// GOOD: Uses design tokens
const Button = () => (
<button style={{ backgroundColor: 'var(--color-primary-500)' }}>
Click
</button>
);
atomic-design-fundamentals - Core methodology overviewatomic-design-molecules - Composing atoms into moleculesWeekly Installs
54
Repository
GitHub Stars
129
First Seen
Jan 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode46
codex46
gemini-cli43
github-copilot42
cursor36
kimi-cli35
站立会议模板:敏捷开发每日站会指南与工具(含远程团队异步模板)
10,500 周安装