diff --git a/frontend/src/pages/design/ButtonContent.tsx b/frontend/src/pages/design/ButtonContent.tsx new file mode 100644 index 0000000..a02dd75 --- /dev/null +++ b/frontend/src/pages/design/ButtonContent.tsx @@ -0,0 +1,741 @@ +// 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)[] = [ + '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 ( +
+
+ + {/* ── 섹션 1: 헤더 ── */} +
+

+ Components +

+

+ Button +

+

+ 사용자의 의도를 명확하게 전달하고, 행동을 유도합니다. +

+

+ 버튼의 형태와 색은 우선순위를 시각적으로 구분합니다. +

+
+ + {/* ── 섹션 2: Anatomy ── */} +
+

+ Anatomy +

+ + {/* Anatomy 카드 */} +
+
+ + {/* 왼쪽: 텍스트 + 아이콘 버튼 */} +
+
+ {/* 버튼 본체 */} +
+ {/* Container 번호 — 테두리 점선 */} + + 레이블 + + + + {/* 번호 뱃지 — Container (1) */} + + 1 + + + {/* 번호 뱃지 — Label (2) */} + + 2 + + + {/* 번호 뱃지 — Icon (3) */} + + 3 + +
+
+ + 텍스트 + 아이콘 버튼 + +
+ + {/* 오른쪽: 아이콘 전용 버튼 */} +
+
+
+ + ♥ + + {/* 번호 뱃지 — Container (1) */} + + 1 + + + {/* 번호 뱃지 — Icon (3) */} + + 3 + +
+
+ + 아이콘 전용 버튼 + +
+
+
+ + {/* 번호 목록 */} +
    + {[ + { label: 'Container', desc: '버튼의 외곽 영역. 클릭 가능한 전체 영역을 정의합니다.' }, + { label: 'Label', desc: '버튼의 텍스트 레이블.' }, + { label: 'Icon (Optional)', desc: '선택적으로 추가되는 아이콘 요소.' }, + ].map((item) => ( +
  1. + + {item.label} + + {' '}— {item.desc} +
  2. + ))} +
+
+ + {/* ── 섹션 3: Spec ── */} +
+

+ Spec +

+ + {/* 3-1. Size */} +
+

+ 1. Size +

+
+
+ {BUTTON_SIZES.map((size) => ( +
+ {/* 라벨 */} + + {size.label} + + + {/* 실제 크기 버튼 */} +
+ +
+
+ ))} +
+
+
+ + {/* 3-2. Container */} +
+

+ 2. Container +

+
+
+ + {/* Flexible */} +
+ + Flexible + +
+
+ {/* 좌측 padding 치수선 */} +
+
+ + 20px + +
+ + + + {/* 우측 padding 치수선 */} +
+
+ + 20px + +
+
+ + 콘텐츠에 맞게 너비가 자동으로 조정됩니다. + +
+
+ + {/* Fixed */} +
+ + Fixed + +
+
+ + {/* 고정 너비 표시 */} +
+
+ + Fixed Width + +
+
+
+ + 너비가 고정된 버튼입니다. + +
+
+
+
+
+ + {/* 3-3. Label */} +
+

+ 3. Label +

+
+
+ {[ + { 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) => ( +
+ + {item.resolution} + +
+
+ + {/* 패딩 주석 */} + + padding {item.padding} + +
+
+
+ ))} +
+
+
+
+ + {/* ── 섹션 4: Style (변형 × 상태 매트릭스) ── */} +
+

+ Style +

+ +
+ + {/* 열 헤더 */} + + + {/* 빈 셀 (상태 열) */} + + ))} + + + + + {stateRows.map((row, rowIdx) => ( + + {/* 상태 라벨 */} + + + {/* 각 변형별 버튼 셀 */} + {VARIANTS.map((_, vIdx) => { + const style = getVariantStyle(row, vIdx); + return ( + + ); + })} + + ))} + +
+ {VARIANTS.map((variant) => ( + + {variant} +
+ {row.state} + + +
+
+
+ +
+
+ ); +}; + +export default ButtonContent; diff --git a/frontend/src/pages/design/ComponentsOverview.tsx b/frontend/src/pages/design/ComponentsOverview.tsx new file mode 100644 index 0000000..6610197 --- /dev/null +++ b/frontend/src/pages/design/ComponentsOverview.tsx @@ -0,0 +1,219 @@ +// ComponentsOverview.tsx — Components 탭 Overview 카드 그리드 + +import type { DesignTheme } from './designTheme'; + +// ---------- 타입 ---------- + +interface OverviewCard { + id: string; + label: string; + thumbnail: (isDark: boolean) => React.ReactNode; +} + +// ---------- 썸네일 구현 ---------- + +const ButtonsThumbnail = ({ isDark }: { isDark: boolean }) => { + const accent = isDark ? '#4cd7f6' : '#06b6d4'; + const secondaryBg = isDark ? 'rgba(255,255,255,0.07)' : '#e2e8f0'; + const secondaryText = isDark ? 'rgba(223,226,243,0.85)' : '#475569'; + const outlineBorder = isDark ? 'rgba(76,215,246,0.40)' : 'rgba(6,182,212,0.50)'; + + const buttons = [ + { label: 'Primary', bg: accent, border: accent, color: isDark ? '#0a0e1a' : '#ffffff' }, + { label: 'Secondary', bg: secondaryBg, border: 'transparent', color: secondaryText }, + { label: 'Outline', bg: 'transparent', border: outlineBorder, color: accent }, + ]; + + return ( +
+ {buttons.map(({ label, bg, border, color }) => ( +
+ {label} +
+ ))} +
+ ); +}; + +const TextInputsThumbnail = ({ isDark }: { isDark: boolean }) => { + const labelColor = isDark ? 'rgba(194,198,214,0.80)' : '#64748b'; + const inputBg = isDark ? 'rgba(255,255,255,0.04)' : '#ffffff'; + const inputBorder = isDark ? 'rgba(255,255,255,0.12)' : '#cbd5e1'; + const placeholderColor = isDark ? 'rgba(140,144,159,0.60)' : '#94a3b8'; + const accentBorder = isDark ? '#4cd7f6' : '#06b6d4'; + + return ( +
+ {/* 라벨 + 기본 입력 */} +
+
+
+
+
+
+ {/* 포커스 상태 입력 */} +
+
+
+
+
+
+
+ ); +}; + +// ---------- 카드 정의 ---------- + +const OVERVIEW_CARDS: OverviewCard[] = [ + { + id: 'buttons', + label: 'Buttons', + thumbnail: (isDark) => , + }, + { + id: 'text-field', + label: 'Text Field', + thumbnail: (isDark) => , + }, +]; + +// ---------- Props ---------- + +interface ComponentsOverviewProps { + theme: DesignTheme; + onNavigate: (id: string) => void; +} + +// ---------- 컴포넌트 ---------- + +const ComponentsOverview = ({ theme, onNavigate }: ComponentsOverviewProps) => { + const t = theme; + const isDark = t.mode === 'dark'; + + const cardBg = isDark ? 'rgba(255,255,255,0.03)' : '#f5f5f5'; + const cardBorder = isDark ? 'rgba(255,255,255,0.06)' : '#e5e5e5'; + const thumbnailBorderBottom = isDark ? 'rgba(255,255,255,0.06)' : '#e0e0e0'; + + return ( +
+ + {/* ── 헤더 영역 ── */} +
+ + Components + +

+ Overview +

+

+ 재사용 가능한 UI 컴포넌트 카탈로그입니다. +

+
+ + {/* ── 3열 카드 그리드 ── */} +
+ {OVERVIEW_CARDS.map((card) => ( +
onNavigate(card.id)} + onMouseEnter={(e) => { + const el = e.currentTarget; + el.style.transform = 'scale(1.025)'; + el.style.boxShadow = isDark + ? '0 8px 24px rgba(0,0,0,0.35)' + : '0 6px 18px rgba(0,0,0,0.10)'; + el.style.borderColor = isDark + ? 'rgba(76,215,246,0.22)' + : 'rgba(6,182,212,0.28)'; + }} + onMouseLeave={(e) => { + const el = e.currentTarget; + el.style.transform = 'scale(1)'; + el.style.boxShadow = 'none'; + el.style.borderColor = cardBorder; + }} + > + {/* 썸네일 영역 */} +
+ {card.thumbnail(isDark)} +
+ + {/* 카드 라벨 */} +
+ + {card.label} + +
+
+ ))} +
+
+ ); +}; + +export default ComponentsOverview; diff --git a/frontend/src/pages/design/DesignHeader.tsx b/frontend/src/pages/design/DesignHeader.tsx index afd37c5..96654ed 100644 --- a/frontend/src/pages/design/DesignHeader.tsx +++ b/frontend/src/pages/design/DesignHeader.tsx @@ -1,3 +1,4 @@ +import { useNavigate } from 'react-router-dom'; import type { DesignTheme } from './designTheme'; export type DesignTab = 'foundations' | 'components'; @@ -16,6 +17,7 @@ const TABS: { label: string; id: DesignTab }[] = [ export const DesignHeader = ({ activeTab, onTabChange, theme, onThemeToggle }: DesignHeaderProps) => { const isDark = theme.mode === 'dark'; + const navigate = useNavigate(); return (
{/* 좌측: 로고 + 버전 뱃지 */}
- navigate('/')} + className="font-sans text-2xl leading-8 font-bold bg-transparent border-none p-0 cursor-pointer transition-opacity hover:opacity-70" style={{ letterSpacing: '2.4px', color: theme.textAccent }} > WING-OPS - +
= { - foundations: 'color', - components: 'buttons', + foundations: 'overview', + components: 'overview', }; export const DesignPage = () => { const [activeTab, setActiveTab] = useState('foundations'); const [themeMode, setThemeMode] = useState('dark'); - const [sidebarItem, setSidebarItem] = useState('color'); + const [sidebarItem, setSidebarItem] = useState('overview'); const theme = getTheme(themeMode); @@ -32,6 +36,8 @@ export const DesignPage = () => { const renderContent = () => { if (activeTab === 'foundations') { switch (sidebarItem) { + case 'overview': + return setSidebarItem(id as MenuItemId)} />; case 'color': return ; case 'typography': @@ -41,10 +47,19 @@ export const DesignPage = () => { case 'layout': return ; default: - return ; + return setSidebarItem(id as MenuItemId)} />; } } - return ; + switch (sidebarItem) { + case 'overview': + return setSidebarItem(id as MenuItemId)} />; + case 'buttons': + return ; + case 'text-field': + return ; + default: + return ; + } }; return ( diff --git a/frontend/src/pages/design/DesignSidebar.tsx b/frontend/src/pages/design/DesignSidebar.tsx index d63daa5..b953cf0 100644 --- a/frontend/src/pages/design/DesignSidebar.tsx +++ b/frontend/src/pages/design/DesignSidebar.tsx @@ -1,38 +1,27 @@ import type { DesignTheme } from './designTheme'; import type { DesignTab } from './DesignHeader'; -import wingColorPaletteIcon from '../../assets/icons/wing-color-palette.svg'; -import wingElevationIcon from '../../assets/icons/wing-elevation.svg'; -import wingFoundationsIcon from '../../assets/icons/wing-foundations.svg'; -import wingLayoutGridIcon from '../../assets/icons/wing-layout-grid.svg'; -import wingTypographyIcon from '../../assets/icons/wing-typography.svg'; - -export type FoundationsMenuItemId = 'color' | 'typography' | 'radius' | 'layout'; -export type ComponentsMenuItemId = 'buttons' | 'text-inputs' | 'controls' | 'badge' | 'dialog' | 'tabs' | 'popup' | 'navigation'; +export type FoundationsMenuItemId = 'overview' | 'color' | 'typography' | 'radius' | 'layout'; +export type ComponentsMenuItemId = 'overview' | 'buttons' | 'text-field'; export type MenuItemId = FoundationsMenuItemId | ComponentsMenuItemId; interface MenuItem { id: MenuItemId; label: string; - icon: string; } const FOUNDATIONS_MENU: MenuItem[] = [ - { id: 'color', label: 'Color', icon: wingColorPaletteIcon }, - { id: 'typography', label: 'Typography', icon: wingTypographyIcon }, - { id: 'radius', label: 'Radius', icon: wingElevationIcon }, - { id: 'layout', label: 'Layout', icon: wingLayoutGridIcon }, + { id: 'overview', label: 'Overview' }, + { id: 'color', label: 'Color' }, + { id: 'typography', label: 'Typography' }, + { id: 'radius', label: 'Radius' }, + { id: 'layout', label: 'Layout' }, ]; const COMPONENTS_MENU: MenuItem[] = [ - { id: 'buttons', label: 'Buttons', icon: wingFoundationsIcon }, - { id: 'text-inputs', label: 'Text Inputs', icon: wingFoundationsIcon }, - { id: 'controls', label: 'Controls', icon: wingFoundationsIcon }, - { id: 'badge', label: 'Badge', icon: wingColorPaletteIcon }, - { id: 'dialog', label: 'Dialog', icon: wingLayoutGridIcon }, - { id: 'tabs', label: 'Tabs', icon: wingLayoutGridIcon }, - { id: 'popup', label: 'Popup', icon: wingElevationIcon }, - { id: 'navigation', label: 'Navigation', icon: wingTypographyIcon }, + { id: 'overview', label: 'Overview' }, + { id: 'buttons', label: 'Buttons' }, + { id: 'text-field', label: 'Text Field' }, ]; const SIDEBAR_CONFIG: Record = { @@ -58,7 +47,7 @@ export function DesignSidebar({ theme, activeTab, activeItem, onItemChange }: De ); @@ -82,22 +70,6 @@ export function DesignSidebar({ theme, activeTab, activeItem, onItemChange }: De boxShadow: `0px 25px 50px -12px ${theme.sidebarShadow}`, }} > - {/* 타이틀 영역 */} - {/*
-

- {title} -

-

- {subtitle} -

-
*/} - {/* 메뉴 네비게이션 */} diff --git a/frontend/src/pages/design/FoundationsOverview.tsx b/frontend/src/pages/design/FoundationsOverview.tsx new file mode 100644 index 0000000..72fd34d --- /dev/null +++ b/frontend/src/pages/design/FoundationsOverview.tsx @@ -0,0 +1,274 @@ +// FoundationsOverview.tsx — Foundations 탭 Overview 카드 그리드 + +import type { DesignTheme } from './designTheme'; + +// ---------- 타입 ---------- + +interface OverviewCard { + id: string; + label: string; + thumbnail: (isDark: boolean) => React.ReactNode; +} + +// ---------- 썸네일 구현 ---------- + +const ColorThumbnail = ({ isDark }: { isDark: boolean }) => { + // 3x3 도트 그리드: gray / pink / cyan 컬럼, 어두운 순 + const dots: string[][] = [ + ['#9ca3af', '#f9a8d4', '#67e8f9'], + ['#4b5563', '#ec4899', '#06b6d4'], + ['#1f2937', '#9d174d', '#0e7490'], + ]; + + return ( +
+
+ {dots.map((row, ri) => + row.map((color, ci) => ( +
+ )), + )} +
+
+ ); +}; + +const TypographyThumbnail = ({ isDark }: { isDark: boolean }) => ( +
+ + 가 + + + a + +
+); + +const RadiusThumbnail = ({ isDark }: { isDark: boolean }) => { + const items = [ + { radius: '0px', size: 36 }, + { radius: '6px', size: 36 }, + { radius: '12px', size: 36 }, + { radius: '50%', size: 36 }, + ]; + + const borderColor = isDark ? 'rgba(76,215,246,0.55)' : 'rgba(6,182,212,0.65)'; + const bgColor = isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.08)'; + + return ( +
+ {items.map(({ radius, size }) => ( +
+ ))} +
+ ); +}; + +const LayoutThumbnail = ({ isDark }: { isDark: boolean }) => { + const accent = isDark ? 'rgba(76,215,246,0.18)' : 'rgba(6,182,212,0.14)'; + const accentStrong = isDark ? 'rgba(76,215,246,0.40)' : 'rgba(6,182,212,0.38)'; + const faint = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'; + + return ( +
+ {/* 헤더 바 */} +
+ {/* 2열 바디 */} +
+
+
+
+
+
+
+
+ {/* 푸터 바 */} +
+
+ ); +}; + +// ---------- 카드 정의 ---------- + +const OVERVIEW_CARDS: OverviewCard[] = [ + { + id: 'color', + label: 'Color', + thumbnail: (isDark) => , + }, + { + id: 'typography', + label: 'Typography', + thumbnail: (isDark) => , + }, + { + id: 'radius', + label: 'Radius', + thumbnail: (isDark) => , + }, + { + id: 'layout', + label: 'Layout', + thumbnail: (isDark) => , + }, +]; + +// ---------- Props ---------- + +interface FoundationsOverviewProps { + theme: DesignTheme; + onNavigate: (id: string) => void; +} + +// ---------- 컴포넌트 ---------- + +const FoundationsOverview = ({ theme, onNavigate }: FoundationsOverviewProps) => { + const t = theme; + const isDark = t.mode === 'dark'; + + const cardBg = isDark ? 'rgba(255,255,255,0.03)' : '#f5f5f5'; + const cardBorder = isDark ? 'rgba(255,255,255,0.06)' : '#e5e5e5'; + const thumbnailBorderBottom = isDark ? 'rgba(255,255,255,0.06)' : '#e0e0e0'; + + return ( +
+ + {/* ── 헤더 영역 ── */} +
+ + Foundations + +

+ Overview +

+

+ 디자인의 기반이 되는 핵심 요소 사용 기준입니다. +

+
+ + {/* ── 3열 카드 그리드 ── */} +
+ {OVERVIEW_CARDS.map((card) => ( +
onNavigate(card.id)} + onMouseEnter={(e) => { + const el = e.currentTarget; + el.style.transform = 'scale(1.025)'; + el.style.boxShadow = isDark + ? '0 8px 24px rgba(0,0,0,0.35)' + : '0 6px 18px rgba(0,0,0,0.10)'; + el.style.borderColor = isDark + ? 'rgba(76,215,246,0.22)' + : 'rgba(6,182,212,0.28)'; + }} + onMouseLeave={(e) => { + const el = e.currentTarget; + el.style.transform = 'scale(1)'; + el.style.boxShadow = 'none'; + el.style.borderColor = cardBorder; + }} + > + {/* 썸네일 영역 */} +
+ {card.thumbnail(isDark)} +
+ + {/* 카드 라벨 */} +
+ + {card.label} + +
+
+ ))} +
+
+ ); +}; + +export default FoundationsOverview; diff --git a/frontend/src/pages/design/TextFieldContent.tsx b/frontend/src/pages/design/TextFieldContent.tsx new file mode 100644 index 0000000..084ce5d --- /dev/null +++ b/frontend/src/pages/design/TextFieldContent.tsx @@ -0,0 +1,1546 @@ +// TextFieldContent.tsx — WING-OPS Text Field 컴포넌트 상세 페이지 (다크/라이트 테마 지원) + +import type { DesignTheme } from './designTheme'; + +// ---------- 타입 ---------- + +interface InputFieldStyle { + bg: string; + border: string; + textColor: string; + placeholderColor: string; + borderWidth?: string; + opacity?: string; +} + +interface StateRow { + state: string; + badge: string; + placeholder: string; + hasCursor: boolean; + style: InputFieldStyle; + showClear?: boolean; + showSparkle?: boolean; +} + +// ---------- 헬퍼 데이터 ---------- + +const getDarkStateRows = (): StateRow[] => [ + { + state: 'Enabled', + badge: 'Enabled', + placeholder: '플레이스홀더', + hasCursor: false, + style: { bg: '#1e293b', border: '#334155', textColor: '#e2e8f0', placeholderColor: '#64748b' }, + }, + { + state: 'Focused', + badge: 'Focused', + placeholder: '플레이스홀더', + hasCursor: true, + style: { bg: '#1e293b', border: '#e2e8f0', textColor: '#e2e8f0', placeholderColor: '#64748b', borderWidth: '2px' }, + }, + { + state: 'Error', + badge: 'Error', + placeholder: '플레이스홀더', + hasCursor: false, + style: { bg: '#1e293b', border: '#ef4444', textColor: '#e2e8f0', placeholderColor: '#64748b' }, + }, + { + state: 'Error Focused', + badge: 'Error Focused', + placeholder: '플레이스홀더', + hasCursor: true, + style: { bg: '#1e293b', border: '#ef4444', textColor: '#e2e8f0', placeholderColor: '#64748b', borderWidth: '2px' }, + }, + { + state: 'Disabled', + badge: 'Disabled', + placeholder: '플레이스홀더', + hasCursor: false, + style: { bg: 'rgba(255,255,255,0.02)', border: '#1e293b', textColor: '#e2e8f0', placeholderColor: '#64748b', opacity: '0.4' }, + }, + { + state: 'Read Only', + badge: 'Read Only', + placeholder: '플레이스홀더', + hasCursor: false, + style: { bg: 'rgba(255,255,255,0.02)', border: '#334155', textColor: '#e2e8f0', placeholderColor: '#64748b' }, + }, + { + state: 'AI Loading', + badge: 'AI Loading', + placeholder: '단서를 모아서 추리 중...', + hasCursor: false, + showSparkle: true, + style: { bg: 'rgba(255,255,255,0.02)', border: '#334155', textColor: '#e2e8f0', placeholderColor: '#64748b' }, + }, +]; + +const getLightStateRows = (): StateRow[] => [ + { + state: 'Enabled', + badge: 'Enabled', + placeholder: '플레이스홀더', + hasCursor: false, + style: { bg: '#fff', border: '#d1d5db', textColor: '#1f2937', placeholderColor: '#9ca3af' }, + }, + { + state: 'Focused', + badge: 'Focused', + placeholder: '플레이스홀더', + hasCursor: true, + style: { bg: '#fff', border: '#1f2937', textColor: '#1f2937', placeholderColor: '#9ca3af', borderWidth: '2px' }, + }, + { + state: 'Error', + badge: 'Error', + placeholder: '플레이스홀더', + hasCursor: false, + style: { bg: '#fff', border: '#ef4444', textColor: '#1f2937', placeholderColor: '#9ca3af' }, + }, + { + state: 'Error Focused', + badge: 'Error Focused', + placeholder: '플레이스홀더', + hasCursor: true, + style: { bg: '#fff', border: '#ef4444', textColor: '#1f2937', placeholderColor: '#9ca3af', borderWidth: '2px' }, + }, + { + state: 'Disabled', + badge: 'Disabled', + placeholder: '플레이스홀더', + hasCursor: false, + style: { bg: '#f9fafb', border: '#e5e7eb', textColor: '#1f2937', placeholderColor: '#9ca3af', opacity: '0.4' }, + }, + { + state: 'Read Only', + badge: 'Read Only', + placeholder: '플레이스홀더', + hasCursor: false, + style: { bg: '#f9fafb', border: '#d1d5db', textColor: '#1f2937', placeholderColor: '#9ca3af' }, + }, + { + state: 'AI Loading', + badge: 'AI Loading', + placeholder: '단서를 모아서 추리 중...', + hasCursor: false, + showSparkle: true, + style: { bg: '#f9fafb', border: '#d1d5db', textColor: '#1f2937', placeholderColor: '#9ca3af' }, + }, +]; + +// ---------- Props ---------- + +interface TextFieldContentProps { + theme: DesignTheme; +} + +// ---------- 컴포넌트 ---------- + +export const TextFieldContent = ({ theme }: TextFieldContentProps) => { + 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 annotationColor = '#8b5cf6'; + + // 입력 필드 공통 스타일 (Anatomy, Guideline용) + const fieldBg = isDark ? '#1e293b' : '#fff'; + const fieldBorder = isDark ? '#334155' : '#d1d5db'; + const fieldText = isDark ? '#e2e8f0' : '#1f2937'; + const fieldPlaceholder = isDark ? '#64748b' : '#9ca3af'; + + const stateRows = isDark ? getDarkStateRows() : getLightStateRows(); + + return ( +
+
+ + {/* ── 섹션 1: 헤더 ── */} +
+

+ Components +

+

+ Text Field +

+

+ 사용자로부터 텍스트 데이터를 입력받는 기본 입력 컴포넌트입니다. +

+
+ + {/* ── Input Field 소제목 ── */} +
+

+ Input Field +

+

+ 단일 행 텍스트를 입력받는 필드입니다. +

+
+ + {/* ── 섹션 2: Anatomy ── */} +
+

+ Anatomy +

+ + {/* Anatomy 카드 */} +
+
+ + {/* 입력 필드 구조 분해도 */} +
+ + {/* 상단 주석 라벨: Prefix, Input, Suffix */} +
+ {/* Prefix label */} +
+ + Prefix + + + (Optional) + + {/* 아래 화살표 선 */} +
+
+
+ + {/* Input label */} +
+ + Input + + {/* 아래 화살표 선 */} +
+
+
+ + {/* Suffix label */} +
+ + Suffix + + + (Optional) + + {/* 아래 화살표 선 */} +
+
+
+
+ + {/* 실제 입력 필드 */} +
+ {/* Prefix 아이콘 영역 */} +
+ + + + +
+ + {/* 입력 텍스트 영역 */} +
+ 입력값 텍스트 +
+ + {/* Clear 버튼 */} +
+ × +
+ + {/* Suffix 텍스트 */} + + 원 + + + {/* Container 점선 테두리 */} + +
+ + {/* Container 왼쪽 주석 */} +
+ + Container + + {/* 화살표 선 */} +
+
+
+ + {/* 하단 주석 라벨: Clear Button */} +
+
+ {/* 위 화살표 선 */} +
+
+ + Clear Button + + + (Optional) + +
+
+
+
+
+
+ + {/* ── 섹션 3: Guideline ── */} +
+

+ Guideline +

+ + {/* 3-1. Container */} +
+
+ + 1 + +

+ Container +

+
+

+ 입력 필드의 외곽 영역입니다. 테두리, 곡률, 내부 여백을 정의합니다. +

+
+
+ {/* 컨테이너 박스 + 치수선 */} +
+ {/* 높이 치수선 (오른쪽) */} +
+
+ + 44px + +
+
+ + {/* padding 치수선 (상단 왼쪽) */} + + px: 12px + + + {/* 빈 컨테이너 박스 */} +
+
+ +
+

+ height: 44px (Medium) +

+

+ padding: 12px (좌우) +

+

+ border-radius: 6px +

+
+
+
+
+ + {/* 3-2. Placeholder */} +
+
+ + 2 + +

+ Placeholder +

+
+

+ 값이 입력되지 않았을 때 표시되는 안내 텍스트입니다. 입력 시 사라집니다. +

+
+
+ {/* 플레이스홀더 있는 필드 */} +
+ + 플레이스홀더 있음 + +
+ 검색어를 입력하세요 +
+
+ + {/* 빈 필드 (플레이스홀더 없음) */} +
+ + 플레이스홀더 없음 + +
+
+
+
+
+ + {/* 3-3. Label */} +
+
+ + 3 + +

+ Label +

+
+

+ 입력 필드의 용도를 설명하는 텍스트입니다. 필수 항목은 * 표시로 구분합니다. +

+
+
+ {/* 일반 라벨 */} +
+ + 이름 + +
+ 홍길동 +
+
+ + {/* 필수 라벨 */} +
+ + 이메일{' '} + * + +
+ 이메일을 입력하세요 +
+
+
+
+
+ + {/* 3-4. Input Text */} +
+
+ + 4 + +

+ Input Text +

+
+

+ 사용자가 실제로 입력한 텍스트입니다. 플레이스홀더보다 진한 색상으로 표시됩니다. +

+
+
+
+ 홍길동 +
+
+ font-size: 14px + color: textPrimary + font-weight: 400 +
+
+
+
+ + {/* 3-5. Clear Icon */} +
+
+ + 5 + +

+ Clear Icon +

+
+

+ 입력값이 있을 때 표시되는 초기화 버튼입니다. 클릭 시 입력값을 삭제합니다. +

+
+
+ {/* 텍스트 입력 + Clear 아이콘 표시 */} +
+ + 입력값 있음 (Clear 표시) + +
+ 홍길동 + {/* Clear 버튼 */} +
+ × +
+
+
+ + {/* 빈 상태 (Clear 미표시) */} +
+ + 입력값 없음 (Clear 미표시) + +
+ 플레이스홀더 +
+
+
+
+
+ + {/* 3-6. Helper Text */} +
+
+ + 6 + +

+ Helper Text +

+
+

+ 입력 필드 하단에 표시되는 보조 텍스트입니다. 안내 또는 에러 메시지로 사용됩니다. +

+
+
+ {/* 기본 도움말 */} +
+
+ 비밀번호 +
+ + 영문, 숫자 포함 8자 이상 + +
+ + {/* 에러 메시지 */} +
+
+ 비밀번호 +
+ + 필수 입력 항목입니다. + +
+
+
+
+
+ + {/* ── 섹션 4: State (Input Field) ── */} +
+

+ State +

+ +
+
+ {stateRows.map((row) => ( +
+ {/* 왼쪽: State 라벨 + 뱃지 */} +
+ + State + + + {row.badge} + +
+ + {/* 오른쪽: 입력 필드 */} +
+ {row.showSparkle && ( + + )} + + {row.placeholder} + {row.hasCursor && ( + + | + + )} + +
+
+ ))} +
+
+
+ + {/* ════════════════════════════════════════════════════ + Text Area 단락 + ════════════════════════════════════════════════════ */} + + {/* ── Text Area 소제목 ── */} +
+

+ Text Area +

+

+ 여러 줄의 텍스트를 입력받는 필드입니다. +

+
+ + {/* ── Text Area Anatomy ── */} +
+

+ Anatomy +

+ +
+
+ + {/* TextArea 구조 분해도 */} +
+ + {/* 상단 주석 라벨: Input Area, Character Counter */} +
+ {/* Input Area label */} +
+ + Input Area + +
+
+
+ + {/* Placeholder label */} +
+ + Placeholder + +
+
+
+ + {/* Character Counter label */} +
+ + Character Counter + + + (Optional) + +
+
+
+
+ + {/* 실제 TextArea */} +
+ {/* 플레이스홀더 텍스트 */} +
+ 내용을 입력하세요 +
+ + {/* 우하단: 문자 수 카운터 + resize 핸들 */} +
+ + 0/500 + + {/* Resize 핸들 (대각선 줄무늬) */} +
+
+ + {/* Container 점선 테두리 */} + +
+ + {/* Container 왼쪽 주석 */} +
+ + Container + +
+
+
+ + {/* 하단 주석 라벨: Resize Handle */} +
+
+
+
+ + Resize Handle + +
+
+
+
+
+
+ + {/* ── Text Area Guideline ── */} +
+

+ Guideline +

+ + {/* TA-1. Container */} +
+
+ + 1 + +

+ Container +

+
+

+ 텍스트 영역의 외곽 컨테이너입니다. 기본 높이 112px이며 사용자가 리사이즈할 수 있습니다. +

+
+
+
+ {/* 높이 치수선 (오른쪽) */} +
+
+ + 112px + +
+
+ + {/* padding 치수선 (상단 왼쪽) */} + + p: 12px + + + {/* 빈 컨테이너 박스 */} +
+
+ +
+

+ height: 112px (default) +

+

+ padding: 12px +

+

+ border-radius: 6px +

+

+ resize: vertical +

+
+
+
+
+ + {/* TA-2. Placeholder */} +
+
+ + 2 + +

+ Placeholder +

+
+

+ 값이 입력되지 않았을 때 표시되는 안내 텍스트입니다. +

+
+
+ {/* 플레이스홀더 있는 TextArea */} +
+ + 플레이스홀더 있음 + +
+ 내용을 입력하세요 +
+
+ + {/* 빈 TextArea */} +
+ + 플레이스홀더 없음 + +
+
+
+
+
+ + {/* TA-3. Label */} +
+
+ + 3 + +

+ Label +

+
+

+ 텍스트 영역의 용도를 설명하는 라벨입니다. +

+
+
+ {/* 기본 라벨 */} +
+ + 내용 + +
+ 내용을 입력하세요 +
+
+ + {/* 필수(*) 라벨 */} +
+ + 비고{' '} + * + +
+ 필수 항목입니다 +
+
+
+
+
+ + {/* TA-4. Input Text */} +
+
+ + 4 + +

+ Input Text +

+
+

+ 사용자가 입력한 여러 줄의 텍스트입니다. +

+
+
+
+ {'오늘 점검 내용을 기록합니다.\n상세 내용은 아래와 같습니다.'} +
+
+ font-size: 14px + color: textPrimary + line-height: 1.6 +
+
+
+
+ + {/* TA-5. Clear Icon */} +
+
+ + 5 + +

+ Clear Icon +

+
+

+ 입력값 초기화 버튼입니다. 텍스트 영역 우상단에 표시됩니다. +

+
+
+ {/* 텍스트 있는 상태 (Clear 표시) */} +
+ + 입력값 있음 (Clear 표시) + +
+ 입력된 내용이 있습니다. + {/* Clear 버튼 우상단 */} +
+ × +
+
+
+ + {/* 빈 상태 (Clear 미표시) */} +
+ + 입력값 없음 (Clear 미표시) + +
+ 내용을 입력하세요 +
+
+
+
+
+ + {/* TA-6. Helper Text */} +
+
+ + 6 + +

+ Helper Text +

+
+

+ 텍스트 영역 하단의 도움말 또는 에러 메시지입니다. +

+
+
+ {/* 기본 도움말 + 문자 수 카운터 */} +
+
+ 내용을 입력하세요 +
+
+ + 상세 내용을 입력해 주세요 + + + 0/500 + +
+
+ + {/* 에러 메시지 */} +
+
+ 내용을 입력하세요 +
+ + 필수 입력 항목입니다. + +
+
+
+
+
+ + {/* ── Text Area State ── */} +
+

+ State +

+ +
+
+ {stateRows.map((row) => ( +
+ {/* 왼쪽: State 라벨 + 뱃지 */} +
+ + State + + + {row.badge} + +
+ + {/* 오른쪽: TextArea */} +
+
+ {row.showSparkle && ( + + )} + + {row.placeholder} + {row.hasCursor && ( + + | + + )} + +
+
+
+ ))} +
+
+
+ +
+
+ ); +}; + +export default TextFieldContent;