wing-ops/frontend/src/pages/design/ButtonContent.tsx

742 lines
27 KiB
TypeScript
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ButtonContent.tsx — WING-OPS Button 컴포넌트 상세 페이지 (다크/라이트 테마 지원)
import type { DesignTheme } from './designTheme';
// ---------- 타입 ----------
interface ButtonSizeRow {
label: string;
heightClass: string;
heightPx: number;
px: number;
}
interface ButtonVariantStyle {
bg: string;
text: string;
border?: string;
}
interface ButtonStateRow {
state: string;
accent: ButtonVariantStyle;
primary: ButtonVariantStyle;
secondary: ButtonVariantStyle;
tertiary: ButtonVariantStyle;
tertiaryFilled: ButtonVariantStyle;
}
// ---------- 데이터 ----------
const BUTTON_SIZES: ButtonSizeRow[] = [
{ label: 'XLarge (56)', heightClass: 'h-14', heightPx: 56, px: 24 },
{ label: 'Large (48)', heightClass: 'h-12', heightPx: 48, px: 20 },
{ label: 'Medium (44)', heightClass: 'h-11', heightPx: 44, px: 16 },
{ label: 'Small (32)', heightClass: 'h-8', heightPx: 32, px: 12 },
{ label: 'XSmall (24)', heightClass: 'h-6', heightPx: 24, px: 8 },
];
const VARIANTS = ['Accent', 'Primary', 'Secondary', 'Tertiary', 'Tertiary (filled)'] as const;
const getDarkStateRows = (): ButtonStateRow[] => [
{
state: 'Default',
accent: { bg: '#ef4444', text: '#fff' },
primary: { bg: '#1a1a2e', text: '#fff' },
secondary: { bg: '#6b7280', text: '#fff' },
tertiary: { bg: 'transparent', text: '#c2c6d6', border: '#6b7280' },
tertiaryFilled: { bg: '#374151', text: '#c2c6d6' },
},
{
state: 'Hover',
accent: { bg: '#dc2626', text: '#fff' },
primary: { bg: '#2d2d44', text: '#fff' },
secondary: { bg: '#7c8393', text: '#fff' },
tertiary: { bg: 'rgba(255,255,255,0.05)', text: '#c2c6d6', border: '#9ca3af' },
tertiaryFilled: { bg: '#4b5563', text: '#c2c6d6' },
},
{
state: 'Pressed',
accent: { bg: '#b91c1c', text: '#fff' },
primary: { bg: '#3d3d5c', text: '#fff' },
secondary: { bg: '#9ca3af', text: '#fff' },
tertiary: { bg: 'rgba(255,255,255,0.1)', text: '#c2c6d6', border: '#9ca3af' },
tertiaryFilled: { bg: '#6b7280', text: '#c2c6d6' },
},
{
state: 'Disabled',
accent: { bg: 'rgba(239,68,68,0.3)', text: 'rgba(255,255,255,0.5)' },
primary: { bg: 'rgba(26,26,46,0.5)', text: 'rgba(255,255,255,0.4)' },
secondary: { bg: 'rgba(107,114,128,0.3)', text: 'rgba(255,255,255,0.4)' },
tertiary: { bg: 'transparent', text: 'rgba(255,255,255,0.3)', border: 'rgba(107,114,128,0.3)' },
tertiaryFilled: { bg: 'rgba(55,65,81,0.3)', text: 'rgba(255,255,255,0.3)' },
},
];
const getLightStateRows = (): ButtonStateRow[] => [
{
state: 'Default',
accent: { bg: '#ef4444', text: '#fff' },
primary: { bg: '#1a1a2e', text: '#fff' },
secondary: { bg: '#d1d5db', text: '#374151' },
tertiary: { bg: 'transparent', text: '#374151', border: '#d1d5db' },
tertiaryFilled: { bg: '#e5e7eb', text: '#374151' },
},
{
state: 'Hover',
accent: { bg: '#dc2626', text: '#fff' },
primary: { bg: '#2d2d44', text: '#fff' },
secondary: { bg: '#bcc0c7', text: '#374151' },
tertiary: { bg: 'rgba(0,0,0,0.03)', text: '#374151', border: '#9ca3af' },
tertiaryFilled: { bg: '#d1d5db', text: '#374151' },
},
{
state: 'Pressed',
accent: { bg: '#b91c1c', text: '#fff' },
primary: { bg: '#3d3d5c', text: '#fff' },
secondary: { bg: '#9ca3af', text: '#374151' },
tertiary: { bg: 'rgba(0,0,0,0.06)', text: '#374151', border: '#6b7280' },
tertiaryFilled: { bg: '#bcc0c7', text: '#374151' },
},
{
state: 'Disabled',
accent: { bg: 'rgba(239,68,68,0.3)', text: 'rgba(255,255,255,0.5)' },
primary: { bg: 'rgba(26,26,46,0.3)', text: 'rgba(255,255,255,0.5)' },
secondary: { bg: 'rgba(209,213,219,0.5)', text: 'rgba(55,65,81,0.4)' },
tertiary: { bg: 'transparent', text: 'rgba(55,65,81,0.3)', border: 'rgba(209,213,219,0.5)' },
tertiaryFilled: { bg: 'rgba(229,231,235,0.5)', text: 'rgba(55,65,81,0.3)' },
},
];
// ---------- Props ----------
interface ButtonContentProps {
theme: DesignTheme;
}
// ---------- 헬퍼 ----------
function getVariantStyle(row: ButtonStateRow, variantIndex: number): ButtonVariantStyle {
const keys: (keyof Omit<ButtonStateRow, 'state'>)[] = [
'accent',
'primary',
'secondary',
'tertiary',
'tertiaryFilled',
];
return row[keys[variantIndex]];
}
// ---------- 컴포넌트 ----------
export const ButtonContent = ({ theme }: ButtonContentProps) => {
const t = theme;
const isDark = t.mode === 'dark';
const sectionCardBg = isDark ? 'rgba(255,255,255,0.03)' : '#f5f5f5';
const dividerColor = isDark ? 'rgba(255,255,255,0.08)' : '#e5e7eb';
const badgeBg = isDark ? '#4a5568' : '#6b7280';
const annotationColor = isDark ? '#f87171' : '#ef4444';
const buttonDarkBg = isDark ? '#e2e8f0' : '#1a1a2e';
const buttonDarkText = isDark ? '#1a1a2e' : '#fff';
const stateRows = isDark ? getDarkStateRows() : getLightStateRows();
return (
<div className="p-12" style={{ color: t.textPrimary }}>
<div style={{ maxWidth: '64rem' }}>
{/* ── 섹션 1: 헤더 ── */}
<div
className="pb-10 mb-12 border-b border-solid"
style={{ borderColor: dividerColor }}
>
<p
className="font-mono text-sm uppercase tracking-widest mb-3"
style={{ color: t.textAccent }}
>
Components
</p>
<h1
className="text-4xl font-bold mb-4"
style={{ color: t.textPrimary }}
>
Button
</h1>
<p
className="text-lg mb-1"
style={{ color: t.textSecondary }}
>
, .
</p>
<p
className="text-lg"
style={{ color: t.textSecondary }}
>
.
</p>
</div>
{/* ── 섹션 2: Anatomy ── */}
<div className="mb-16">
<h2
className="text-2xl font-bold mb-8"
style={{ color: t.textPrimary }}
>
Anatomy
</h2>
{/* Anatomy 카드 */}
<div
className="rounded-xl p-10 mb-8"
style={{ backgroundColor: sectionCardBg }}
>
<div className="flex flex-row items-start gap-16 justify-center">
{/* 왼쪽: 텍스트 + 아이콘 버튼 */}
<div className="flex flex-col items-center gap-6">
<div className="relative">
{/* 버튼 본체 */}
<div
className="relative inline-flex items-center gap-2 px-5 rounded-md"
style={{
height: '44px',
backgroundColor: buttonDarkBg,
color: buttonDarkText,
fontSize: '14px',
fontWeight: 600,
}}
>
{/* Container 번호 — 테두리 점선 */}
<span
className="absolute inset-0 rounded-md pointer-events-none"
style={{
border: `1.5px dashed ${isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.20)'}`,
}}
/>
<span></span>
<span className="font-bold">+</span>
{/* 번호 뱃지 — Container (1) */}
<span
className="absolute flex items-center justify-center rounded-full text-white font-bold"
style={{
width: '22px',
height: '22px',
fontSize: '10px',
backgroundColor: badgeBg,
top: '-12px',
left: '-12px',
}}
>
1
</span>
{/* 번호 뱃지 — Label (2) */}
<span
className="absolute flex items-center justify-center rounded-full text-white font-bold"
style={{
width: '22px',
height: '22px',
fontSize: '10px',
backgroundColor: badgeBg,
top: '-12px',
left: '50%',
transform: 'translateX(-50%)',
}}
>
2
</span>
{/* 번호 뱃지 — Icon (3) */}
<span
className="absolute flex items-center justify-center rounded-full text-white font-bold"
style={{
width: '22px',
height: '22px',
fontSize: '10px',
backgroundColor: badgeBg,
top: '-12px',
right: '-12px',
}}
>
3
</span>
</div>
</div>
<span
className="text-xs font-mono"
style={{ color: t.textMuted }}
>
+
</span>
</div>
{/* 오른쪽: 아이콘 전용 버튼 */}
<div className="flex flex-col items-center gap-6">
<div className="relative">
<div
className="relative inline-flex items-center justify-center rounded-md"
style={{
width: '44px',
height: '44px',
backgroundColor: buttonDarkBg,
color: buttonDarkText,
fontSize: '18px',
}}
>
<span
className="absolute inset-0 rounded-md pointer-events-none"
style={{
border: `1.5px dashed ${isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.20)'}`,
}}
/>
{/* 번호 뱃지 — Container (1) */}
<span
className="absolute flex items-center justify-center rounded-full text-white font-bold"
style={{
width: '22px',
height: '22px',
fontSize: '10px',
backgroundColor: badgeBg,
top: '-12px',
left: '-12px',
}}
>
1
</span>
{/* 번호 뱃지 — Icon (3) */}
<span
className="absolute flex items-center justify-center rounded-full text-white font-bold"
style={{
width: '22px',
height: '22px',
fontSize: '10px',
backgroundColor: badgeBg,
top: '-12px',
right: '-12px',
}}
>
3
</span>
</div>
</div>
<span
className="text-xs font-mono"
style={{ color: t.textMuted }}
>
</span>
</div>
</div>
</div>
{/* 번호 목록 */}
<ol className="flex flex-col gap-2 pl-5 list-decimal">
{[
{ label: 'Container', desc: '버튼의 외곽 영역. 클릭 가능한 전체 영역을 정의합니다.' },
{ label: 'Label', desc: '버튼의 텍스트 레이블.' },
{ label: 'Icon (Optional)', desc: '선택적으로 추가되는 아이콘 요소.' },
].map((item) => (
<li key={item.label} style={{ color: t.textSecondary }}>
<span className="font-bold" style={{ color: t.textPrimary }}>
{item.label}
</span>
{' '} {item.desc}
</li>
))}
</ol>
</div>
{/* ── 섹션 3: Spec ── */}
<div className="mb-16">
<h2
className="text-2xl font-bold mb-10"
style={{ color: t.textPrimary }}
>
Spec
</h2>
{/* 3-1. Size */}
<div className="mb-12">
<h3
className="text-xl font-semibold mb-6"
style={{ color: t.textPrimary }}
>
1. Size
</h3>
<div
className="rounded-xl p-8"
style={{ backgroundColor: sectionCardBg }}
>
<div className="flex flex-col gap-5">
{BUTTON_SIZES.map((size) => (
<div key={size.label} className="flex items-center justify-between gap-8">
{/* 라벨 */}
<span
className="font-mono text-sm w-36 shrink-0"
style={{ color: t.textSecondary }}
>
{size.label}
</span>
{/* 실제 크기 버튼 */}
<div className="flex-1 flex items-center">
<button
type="button"
className="rounded-md font-semibold text-sm"
style={{
height: `${size.heightPx}px`,
paddingLeft: `${size.px}px`,
paddingRight: `${size.px}px`,
backgroundColor: buttonDarkBg,
color: buttonDarkText,
border: 'none',
cursor: 'default',
fontSize: size.heightPx <= 24 ? '11px' : size.heightPx <= 32 ? '12px' : '14px',
}}
>
</button>
</div>
</div>
))}
</div>
</div>
</div>
{/* 3-2. Container */}
<div className="mb-12">
<h3
className="text-xl font-semibold mb-6"
style={{ color: t.textPrimary }}
>
2. Container
</h3>
<div
className="rounded-xl p-8"
style={{ backgroundColor: sectionCardBg }}
>
<div className="flex flex-col gap-8">
{/* Flexible */}
<div className="flex flex-col gap-3">
<span
className="font-mono text-sm font-bold"
style={{ color: t.textPrimary }}
>
Flexible
</span>
<div className="flex items-center gap-4">
<div className="relative inline-flex">
{/* 좌측 padding 치수선 */}
<div
className="absolute top-1/2 flex items-center"
style={{
left: '0',
transform: 'translateY(-50%)',
color: annotationColor,
}}
>
<div
style={{
width: '20px',
height: '1px',
backgroundColor: annotationColor,
}}
/>
<span
className="font-mono absolute"
style={{
fontSize: '9px',
color: annotationColor,
top: '-14px',
left: '2px',
whiteSpace: 'nowrap',
}}
>
20px
</span>
</div>
<button
type="button"
className="rounded-md font-semibold"
style={{
height: '44px',
paddingLeft: '20px',
paddingRight: '20px',
backgroundColor: buttonDarkBg,
color: buttonDarkText,
fontSize: '14px',
border: 'none',
cursor: 'default',
}}
>
</button>
{/* 우측 padding 치수선 */}
<div
className="absolute top-1/2 flex items-center justify-end"
style={{
right: '0',
transform: 'translateY(-50%)',
}}
>
<div
style={{
width: '20px',
height: '1px',
backgroundColor: annotationColor,
}}
/>
<span
className="font-mono absolute"
style={{
fontSize: '9px',
color: annotationColor,
top: '-14px',
right: '2px',
whiteSpace: 'nowrap',
}}
>
20px
</span>
</div>
</div>
<span
className="font-mono text-xs"
style={{ color: t.textSecondary }}
>
.
</span>
</div>
</div>
{/* Fixed */}
<div className="flex flex-col gap-3">
<span
className="font-mono text-sm font-bold"
style={{ color: t.textPrimary }}
>
Fixed
</span>
<div className="flex items-center gap-4">
<div className="relative">
<button
type="button"
className="rounded-md font-semibold"
style={{
height: '44px',
width: '160px',
backgroundColor: buttonDarkBg,
color: buttonDarkText,
fontSize: '14px',
border: 'none',
cursor: 'default',
}}
>
</button>
{/* 고정 너비 표시 */}
<div
className="absolute"
style={{
bottom: '-18px',
left: '0',
right: '0',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
<div style={{ height: '1px', flex: 1, backgroundColor: annotationColor }} />
<span
className="font-mono"
style={{ fontSize: '9px', color: annotationColor, whiteSpace: 'nowrap' }}
>
Fixed Width
</span>
<div style={{ height: '1px', flex: 1, backgroundColor: annotationColor }} />
</div>
</div>
<span
className="font-mono text-xs ml-4"
style={{ color: t.textSecondary }}
>
.
</span>
</div>
</div>
</div>
</div>
</div>
{/* 3-3. Label */}
<div className="mb-12">
<h3
className="text-xl font-semibold mb-6"
style={{ color: t.textPrimary }}
>
3. Label
</h3>
<div
className="rounded-xl p-8"
style={{ backgroundColor: sectionCardBg }}
>
<div className="flex flex-col gap-8">
{[
{ resolution: '해상도 430', width: '100%', maxWidth: '390px', padding: 16 },
{ resolution: '해상도 360', width: '100%', maxWidth: '328px', padding: 16 },
{ resolution: '해상도 320', width: '248px', maxWidth: '248px', padding: 16 },
].map((item) => (
<div key={item.resolution} className="flex flex-col gap-3">
<span
className="font-mono text-sm"
style={{ color: t.textSecondary }}
>
{item.resolution}
</span>
<div className="flex items-center gap-6">
<div
className="relative"
style={{ width: item.width, maxWidth: item.maxWidth }}
>
<button
type="button"
className="w-full rounded-md font-semibold"
style={{
height: '44px',
paddingLeft: `${item.padding}px`,
paddingRight: `${item.padding}px`,
backgroundColor: buttonDarkBg,
color: buttonDarkText,
fontSize: '14px',
border: 'none',
cursor: 'default',
}}
>
</button>
{/* 패딩 주석 */}
<span
className="absolute font-mono"
style={{
fontSize: '9px',
color: annotationColor,
top: '-16px',
left: '0',
}}
>
padding {item.padding}
</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* ── 섹션 4: Style (변형 × 상태 매트릭스) ── */}
<div
className="pt-12 border-t border-solid"
style={{ borderColor: dividerColor }}
>
<h2
className="text-2xl font-bold mb-8"
style={{ color: t.textPrimary }}
>
Style
</h2>
<div
className="rounded-xl p-8 overflow-x-auto"
style={{ backgroundColor: sectionCardBg }}
>
<table style={{ borderCollapse: 'collapse', minWidth: '700px' }}>
{/* 열 헤더 */}
<thead>
<tr>
{/* 빈 셀 (상태 열) */}
<th style={{ width: '100px', padding: '8px 12px' }} />
{VARIANTS.map((variant) => (
<th
key={variant}
className="font-mono text-xs font-semibold text-center pb-4"
style={{
color: t.textSecondary,
padding: '8px 12px',
whiteSpace: 'nowrap',
}}
>
{variant}
</th>
))}
</tr>
</thead>
<tbody>
{stateRows.map((row, rowIdx) => (
<tr key={row.state}>
{/* 상태 라벨 */}
<td
className="font-mono text-xs font-medium"
style={{
color: t.textSecondary,
padding: rowIdx === 0 ? '8px 12px 8px 0' : '8px 12px 8px 0',
verticalAlign: 'middle',
whiteSpace: 'nowrap',
}}
>
{row.state}
</td>
{/* 각 변형별 버튼 셀 */}
{VARIANTS.map((_, vIdx) => {
const style = getVariantStyle(row, vIdx);
return (
<td
key={vIdx}
style={{ padding: '8px 12px', verticalAlign: 'middle', textAlign: 'center' }}
>
<button
type="button"
className="rounded-md font-semibold"
style={{
width: '96px',
height: '40px',
backgroundColor: style.bg,
color: style.text,
border: style.border ? `1.5px solid ${style.border}` : 'none',
fontSize: '12px',
cursor: 'default',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
</button>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};
export default ButtonContent;