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

1359 lines
49 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.

// LayoutContent.tsx — WING-OPS Layout 카탈로그 (KT 디자인시스템 시각화 영감)
import type { DesignTheme } from './designTheme';
// ---------- 데이터 타입 ----------
interface Breakpoint {
name: string;
krdsName: string;
prefix: string;
minWidth: number;
maxWidth: number | null;
columns: number;
inUse: boolean;
note: string;
}
interface DeviceSpec {
device: string;
prefix: string;
width: string;
columns: number;
gutter: string;
margin: string;
supported: boolean;
}
interface SpacingToken {
className: string;
rem: string;
px: number;
usage: string;
}
interface ZLayer {
name: string;
zIndex: number;
description: string;
color: string;
}
interface ShellClass {
className: string;
role: string;
styles: string;
}
interface GridRule {
name: string;
krds: string;
wingOps: string;
note: string;
}
interface SubPageMapping {
krdsRegion: string;
wingOpsComponent: string;
implementation: string;
color: string;
}
interface MultiplierAnnotation {
position: 'top' | 'left' | 'right' | 'bottom';
multiplier: string;
px: number;
}
// ---------- Breakpoints ----------
const BREAKPOINTS: Breakpoint[] = [
{
name: '-',
krdsName: 'xsmall',
prefix: '-',
minWidth: 0,
maxWidth: 360,
columns: 4,
inUse: false,
note: '미지원',
},
{
name: '-',
krdsName: 'small',
prefix: '-',
minWidth: 360,
maxWidth: 768,
columns: 4,
inUse: false,
note: '미지원 (모바일)',
},
{
name: 'md',
krdsName: 'medium',
prefix: 'md:',
minWidth: 768,
maxWidth: 1024,
columns: 8,
inUse: false,
note: '미지원 (태블릿)',
},
{
name: 'lg',
krdsName: 'large',
prefix: 'lg:',
minWidth: 1024,
maxWidth: 1280,
columns: 12,
inUse: false,
note: '미지원',
},
{
name: 'xl',
krdsName: 'xlarge',
prefix: 'xl:',
minWidth: 1280,
maxWidth: 1440,
columns: 12,
inUse: true,
note: 'WING-OPS 최소 지원',
},
{
name: '2xl',
krdsName: 'xxlarge',
prefix: '2xl:',
minWidth: 1440,
maxWidth: null,
columns: 12,
inUse: true,
note: 'WING-OPS 주 사용 해상도',
},
];
// 타임라인 정규화 (0 ~ 2000px 범위)
const TIMELINE_MAX = 2000;
const toPercent = (px: number) => Math.min(100, (px / TIMELINE_MAX) * 100);
const TIMELINE_MARKERS = [360, 768, 1024, 1280, 1440, 1920];
// ---------- Device Specs (xl/2xl만 활성) ----------
const DEVICE_SPECS: DeviceSpec[] = [
{
device: 'Desktop xl',
prefix: 'xl',
width: '1280px 1439px',
columns: 12,
gutter: '24px (gap-6)',
margin: '24px (px-6)',
supported: true,
},
{
device: 'Desktop 2xl',
prefix: '2xl',
width: '≥ 1440px',
columns: 12,
gutter: '24px (gap-6)',
margin: '32px (px-8)',
supported: true,
},
];
// ---------- Spacing Scale ----------
const SPACING_TOKENS: SpacingToken[] = [
{ className: '0.5', rem: '0.125rem', px: 2, usage: '미세 간격' },
{ className: '1', rem: '0.25rem', px: 4, usage: '최소 간격 (gap-1)' },
{ className: '1.5', rem: '0.375rem', px: 6, usage: '컴팩트 간격 (gap-1.5)' },
{ className: '2', rem: '0.5rem', px: 8, usage: '기본 간격 (gap-2, p-2)' },
{ className: '2.5', rem: '0.625rem', px: 10, usage: '중간 간격' },
{ className: '3', rem: '0.75rem', px: 12, usage: '표준 간격 (gap-3, p-3)' },
{ className: '4', rem: '1rem', px: 16, usage: '넓은 간격 (p-4, gap-4)' },
{ className: '5', rem: '1.25rem', px: 20, usage: '패널 패딩 (px-5, py-5)' },
{ className: '6', rem: '1.5rem', px: 24, usage: '섹션 간격 (gap-6, p-6)' },
{ className: '8', rem: '2rem', px: 32, usage: '큰 간격 (px-8, gap-8)' },
{ className: '16', rem: '4rem', px: 64, usage: '최대 간격' },
];
const SPACING_MAX_PX = Math.max(...SPACING_TOKENS.map((s) => s.px));
// ---------- Z-Index Layers (논리적 계층 — 디자인 시스템 진실 소스) ----------
const Z_LAYERS: ZLayer[] = [
{ name: 'Tooltip', zIndex: 60, description: '툴팁, 드롭다운 메뉴', color: '#a855f7' },
{ name: 'Popup', zIndex: 50, description: '팝업, 지도 오버레이', color: '#f97316' },
{ name: 'Modal', zIndex: 40, description: '모달 다이얼로그, 백드롭', color: '#ef4444' },
{ name: 'TopBar', zIndex: 30, description: '상단 네비게이션 바', color: '#3b82f6' },
{ name: 'Sidebar', zIndex: 20, description: '사이드바, 패널', color: '#06b6d4' },
{ name: 'Content', zIndex: 10, description: '메인 콘텐츠 영역', color: '#22c55e' },
{ name: 'Base', zIndex: 0, description: '기본 레이어, 배경', color: '#9ba3b8' },
];
// ---------- App Shell Classes ----------
const SHELL_CLASSES: ShellClass[] = [
{
className: '.wing-panel',
role: '탭 콘텐츠 패널',
styles: 'flex flex-col h-full overflow-hidden',
},
{
className: '.wing-panel-scroll',
role: '패널 내 스크롤 영역',
styles: 'flex-1 overflow-y-auto',
},
{
className: '.wing-header-bar',
role: '패널 헤더',
styles: 'flex items-center justify-between shrink-0 px-5 border-b',
},
{ className: '.wing-sidebar', role: '사이드바', styles: 'flex flex-col border-r border-stroke' },
];
// ---------- KRDS Grid Rules ----------
const GRID_RULES: GridRule[] = [
{
name: 'Grid base',
krds: '8pt grid',
wingOps: 'Tailwind 4px base + 8pt 권장',
note: 'gap-2=8px, p-4=16px, gap-6=24px 등 8pt 배수 우선',
},
{
name: 'Max container',
krds: '1200px fixed',
wingOps: 'max-w-[1440px]',
note: '데스크톱 전용 풀스크린 앱 특성 반영',
},
{
name: 'Screen margin',
krds: '24px (≥medium)',
wingOps: 'px-5 ~ px-8 (2032px)',
note: 'KRDS medium+ 기준 충족',
},
{
name: 'Column gutter',
krds: '1624px (medium+)',
wingOps: 'gap-2 ~ gap-6 (824px)',
note: 'KRDS 권장 범위 내 사용',
},
{
name: 'Sub-page 구조',
krds: 'Header → Left → Main → Right → Footer',
wingOps: 'TopBar → Sidebar → Content',
note: 'Footer 없음 — 풀스크린 앱',
},
];
// ---------- KRDS Sub-page Mappings ----------
const SUB_PAGE_MAPPINGS: SubPageMapping[] = [
{
krdsRegion: 'Header',
wingOpsComponent: 'TopBar',
implementation: 'h-[52px] / shrink-0 / z-30',
color: '#3b82f6',
},
{
krdsRegion: 'Sub Navigation',
wingOpsComponent: 'SubMenuBar',
implementation: 'shrink-0 / 조건부 렌더',
color: '#06b6d4',
},
{
krdsRegion: 'Left Menu',
wingOpsComponent: 'Sidebar',
implementation: '가변 너비 / flex-col / border-r',
color: '#a855f7',
},
{
krdsRegion: 'Main Contents',
wingOpsComponent: 'Content',
implementation: 'flex-1 / overflow-y-auto',
color: '#22c55e',
},
{
krdsRegion: 'Right Menu',
wingOpsComponent: 'Right Panel',
implementation: '조건부 / 탭 콘텐츠 내부',
color: '#f97316',
},
{
krdsRegion: 'Footer',
wingOpsComponent: '없음',
implementation: '풀스크린 앱 — 미사용',
color: '#9ba3b8',
},
];
// ---------- Multiplier 어노테이션 (4pt Grid 데모용) ----------
const CARD_ANNOTATIONS: MultiplierAnnotation[] = [
{ position: 'top', multiplier: 'x4', px: 16 },
{ position: 'left', multiplier: 'x5', px: 20 },
{ position: 'right', multiplier: 'x5', px: 20 },
{ position: 'bottom', multiplier: 'x4', px: 16 },
];
// ---------- Props ----------
interface LayoutContentProps {
theme: DesignTheme;
}
// ---------- 컴포넌트 ----------
export const LayoutContent = ({ theme }: LayoutContentProps) => {
const t = theme;
const isDark = t.mode === 'dark';
// 시각화 색상
const accentTint = isDark ? 'rgba(76,215,246,0.18)' : 'rgba(6,182,212,0.14)';
const accentTintLight = isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)';
const dimBg = isDark ? 'rgba(140,144,159,0.12)' : 'rgba(148,163,184,0.10)';
const dimText = isDark ? '#8c909f' : '#94a3b8';
const cardSurface = isDark ? '#1b1f2c' : '#ffffff';
return (
<div className="pt-24 px-8 pb-16 flex flex-col gap-16 items-start justify-start max-w-[1440px]">
{/* ── 섹션 1: 헤더 + 개요 ── */}
<div
className="w-full border-b border-solid pb-8 flex flex-col gap-4"
style={{ borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0' }}
>
<div className="flex flex-col gap-2">
<h1 className="font-sans text-3xl leading-9 font-bold" style={{ color: t.textPrimary }}>
Layout
</h1>
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
WING-OPS는 .
(100vh), flex . KRDS
xlarge / xxlarge .
</p>
</div>
<div className="grid grid-cols-3 gap-3 w-full max-w-3xl">
{[
{ label: 'Viewport', value: '100vh fixed', desc: 'overflow: hidden' },
{ label: 'Layout', value: 'flex 기반', desc: 'grid 보조' },
{ label: 'Min Width', value: '1280px', desc: 'xl: 이상' },
].map((item) => (
<div
key={item.label}
className="rounded-lg border border-solid px-4 py-3 flex flex-col gap-1"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
{item.label}
</span>
<span className="font-mono text-base font-bold" style={{ color: t.textAccent }}>
{item.value}
</span>
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
{item.desc}
</span>
</div>
))}
</div>
</div>
{/* ── 섹션 2: Breakpoint 타임라인 ── */}
<div className="w-full flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
Breakpoint
</h2>
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
. WING-OPS
(xl, 2xl) cyan으로 .
</p>
</div>
{/* 타임라인 다이어그램 */}
<div
className="rounded-lg border border-solid p-8"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder, boxShadow: t.cardShadow }}
>
{/* 가로축 마커 (분기점 라벨) */}
<div className="relative h-7 mb-2">
{TIMELINE_MARKERS.map((px) => (
<div
key={px}
className="absolute -translate-x-1/2 flex flex-col items-center"
style={{ left: `${toPercent(px)}%` }}
>
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(66,71,84,0.50)' : '#1e293b',
color: '#ffffff',
}}
>
{px}
</span>
</div>
))}
</div>
{/* 분기점 점선 + tier 막대 컨테이너 */}
<div className="relative" style={{ minHeight: '270px' }}>
{/* 수직 점선 */}
{TIMELINE_MARKERS.map((px) => (
<div
key={`line-${px}`}
className="absolute top-0 bottom-0 border-l border-dashed"
style={{
left: `${toPercent(px)}%`,
borderColor: isDark ? 'rgba(140,144,159,0.25)' : 'rgba(148,163,184,0.40)',
}}
/>
))}
{/* tier 막대들 */}
<div className="flex flex-col gap-2.5 pt-2">
{BREAKPOINTS.map((bp) => {
const startPct = toPercent(bp.minWidth);
const endPct = bp.maxWidth ? toPercent(bp.maxWidth) : 100;
const widthPct = endPct - startPct;
const barColor = bp.inUse ? accentTint : dimBg;
const labelColor = bp.inUse ? t.textAccent : dimText;
return (
<div
key={bp.krdsName}
className="relative rounded border border-solid flex items-center px-3"
style={{
marginLeft: `${startPct}%`,
width: `${widthPct}%`,
minHeight: '34px',
backgroundColor: barColor,
borderColor: bp.inUse
? isDark
? 'rgba(76,215,246,0.45)'
: 'rgba(6,182,212,0.40)'
: isDark
? 'rgba(140,144,159,0.25)'
: 'rgba(148,163,184,0.30)',
}}
>
{/* 컬럼 그리드 미니 */}
<div className="flex items-center gap-[2px] mr-3">
{Array.from({ length: bp.columns }).map((_, i) => (
<div
key={i}
style={{
width: '4px',
height: '14px',
backgroundColor: bp.inUse
? isDark
? 'rgba(76,215,246,0.55)'
: 'rgba(6,182,212,0.45)'
: isDark
? 'rgba(140,144,159,0.40)'
: 'rgba(148,163,184,0.35)',
borderRadius: '1px',
}}
/>
))}
</div>
<span
className="font-mono text-caption font-bold"
style={{ color: labelColor }}
>
{bp.krdsName}
</span>
{bp.inUse && (
<span
className="font-mono text-caption rounded px-1.5 py-0 ml-2"
style={{
backgroundColor: isDark ? 'rgba(34,197,94,0.18)' : 'rgba(34,197,94,0.12)',
color: '#22c55e',
}}
>
in use
</span>
)}
<span
className="font-mono text-caption ml-auto"
style={{ color: bp.inUse ? t.textSecondary : dimText }}
>
{bp.columns} cols
</span>
</div>
);
})}
</div>
</div>
{/* 범례 */}
<div
className="flex items-center gap-4 pt-6 mt-6 border-t border-solid"
style={{ borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0' }}
>
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded border border-solid"
style={{
backgroundColor: accentTint,
borderColor: isDark ? 'rgba(76,215,246,0.45)' : 'rgba(6,182,212,0.40)',
}}
/>
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
WING-OPS
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded border border-solid"
style={{
backgroundColor: dimBg,
borderColor: isDark ? 'rgba(140,144,159,0.25)' : 'rgba(148,163,184,0.30)',
}}
/>
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
(1280px )
</span>
</div>
</div>
</div>
</div>
{/* ── 섹션 3: Grid 시각화 카드 ── */}
<div className="w-full flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
Grid
</h2>
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
, , . xl / 2xl
.
</p>
</div>
<div className="grid grid-cols-2 gap-5 w-full">
{DEVICE_SPECS.map((spec) => (
<div
key={spec.device}
className="rounded-lg border border-solid overflow-hidden flex flex-col"
style={{
backgroundColor: t.cardBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
{/* 카드 헤더 */}
<div
className="px-5 py-4 border-b border-solid flex flex-col gap-1"
style={{ borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0' }}
>
<div className="flex items-center gap-2">
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
Breakpoint
</span>
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(66,71,84,0.50)' : '#1e293b',
color: '#ffffff',
}}
>
{spec.prefix}
</span>
</div>
<div className="flex items-center gap-2">
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
Width
</span>
<span className="font-mono text-sm" style={{ color: t.textPrimary }}>
{spec.width}
</span>
</div>
</div>
{/* 시각화 영역 */}
<div
className="px-5 py-6 flex flex-col gap-3"
style={{
backgroundColor: isDark ? 'rgba(10,14,26,0.40)' : 'rgba(248,250,252,0.60)',
}}
>
{/* 컬럼 위 어노테이션 (margin / gutter) */}
<div className="flex items-center justify-between gap-1">
{Array.from({ length: spec.columns + 1 }).map((_, idx) => {
const isEdge = idx === 0 || idx === spec.columns;
return (
<span
key={idx}
className="font-mono text-caption rounded px-1 py-0.5"
style={{
fontSize: '9px',
backgroundColor: isEdge
? isDark
? 'rgba(66,71,84,0.50)'
: '#1e293b'
: 'transparent',
color: isEdge ? '#ffffff' : t.textMuted,
border: isEdge ? 'none' : `1px solid ${t.cardBorder}`,
}}
>
{isEdge ? (spec.prefix === '2xl' ? '32' : '24') : '24'}
</span>
);
})}
</div>
{/* 컬럼 막대 시각화 */}
<div className="flex items-stretch gap-1" style={{ minHeight: '120px' }}>
{Array.from({ length: spec.columns }).map((_, i) => (
<div
key={i}
className="flex-1 rounded-sm"
style={{
backgroundColor: accentTint,
border: `1px solid ${isDark ? 'rgba(76,215,246,0.30)' : 'rgba(6,182,212,0.25)'}`,
}}
/>
))}
</div>
{/* 가짜 TopBar mockup */}
<div
className="rounded border border-solid flex items-center justify-between px-3 py-1.5 mt-2"
style={{
backgroundColor: cardSurface,
borderColor: isDark ? 'rgba(66,71,84,0.30)' : '#e2e8f0',
}}
>
<span
className="font-sans text-caption font-bold"
style={{ color: t.textAccent }}
>
WING-OPS
</span>
<div className="flex gap-2">
{['예측', 'HNS', '구조', '관리'].map((label) => (
<span
key={label}
className="font-korean text-caption"
style={{ color: t.textMuted }}
>
{label}
</span>
))}
</div>
</div>
</div>
{/* 카드 푸터 — Grid 정보 */}
<div
className="px-5 py-3 border-t border-solid grid grid-cols-3 gap-3"
style={{ borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0' }}
>
{[
{ label: 'Columns', value: `${spec.columns}` },
{ label: 'Gutter', value: spec.gutter },
{ label: 'Margin', value: spec.margin },
].map((info) => (
<div key={info.label} className="flex flex-col gap-0.5">
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '0.5px', color: t.textMuted, fontSize: '9px' }}
>
{info.label}
</span>
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
{info.value}
</span>
</div>
))}
</div>
</div>
))}
</div>
{/* 미지원 안내 */}
<div
className="rounded border border-solid px-4 py-3 flex items-center gap-3"
style={{
backgroundColor: isDark ? 'rgba(249,115,22,0.06)' : 'rgba(249,115,22,0.05)',
borderColor: isDark ? 'rgba(249,115,22,0.25)' : 'rgba(249,115,22,0.20)',
}}
>
<span className="font-mono text-base" style={{ color: '#f97316' }}>
</span>
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
<strong style={{ color: '#f97316' }}>1280px </strong> Mobile / Tablet
(xs / s / md / lg) .
</span>
</div>
</div>
{/* ── 섹션 4: App Shell + KRDS Region 매핑 ── */}
<div className="w-full flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
App Shell
</h2>
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
WING-OPS KRDS Sub-page .
</p>
</div>
{/* 다이어그램 */}
<div
className="rounded-lg border border-solid overflow-hidden w-full max-w-3xl"
style={{
backgroundColor: isDark ? '#0a0e1a' : '#f8fafc',
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
{/* TopBar */}
<div
className="flex items-center justify-between px-4 border-b border-solid"
style={{
height: '48px',
backgroundColor: isDark ? 'rgba(59,130,246,0.10)' : 'rgba(59,130,246,0.06)',
borderColor: isDark ? 'rgba(59,130,246,0.25)' : 'rgba(59,130,246,0.18)',
}}
>
<div className="flex items-center gap-3">
<span className="font-mono text-sm font-bold" style={{ color: '#3b82f6' }}>
TopBar
</span>
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(59,130,246,0.18)' : 'rgba(59,130,246,0.12)',
color: '#3b82f6',
}}
>
KRDS · Header
</span>
</div>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
h-[52px] / shrink-0
</span>
</div>
{/* SubMenuBar */}
<div
className="flex items-center justify-between px-4 border-b border-solid"
style={{
height: '32px',
backgroundColor: isDark ? 'rgba(6,182,212,0.08)' : 'rgba(6,182,212,0.05)',
borderColor: isDark ? 'rgba(6,182,212,0.20)' : 'rgba(6,182,212,0.15)',
}}
>
<div className="flex items-center gap-3">
<span className="font-mono text-caption font-bold" style={{ color: '#06b6d4' }}>
SubMenuBar
</span>
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(6,182,212,0.18)' : 'rgba(6,182,212,0.12)',
color: '#06b6d4',
}}
>
KRDS · Sub Navigation
</span>
</div>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
shrink-0 /
</span>
</div>
{/* Content Area */}
<div className="flex flex-row" style={{ height: '220px' }}>
{/* Sidebar */}
<div
className="flex flex-col items-center justify-center gap-2 border-r border-solid"
style={{
width: '160px',
backgroundColor: isDark ? 'rgba(168,85,247,0.08)' : 'rgba(168,85,247,0.05)',
borderColor: isDark ? 'rgba(168,85,247,0.20)' : 'rgba(168,85,247,0.15)',
}}
>
<span className="font-mono text-caption font-bold" style={{ color: '#a855f7' }}>
Sidebar
</span>
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(168,85,247,0.18)' : 'rgba(168,85,247,0.12)',
color: '#a855f7',
}}
>
KRDS · Left Menu
</span>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
flex-col / border-r
</span>
</div>
{/* Main Content */}
<div
className="flex-1 flex flex-col items-center justify-center gap-2"
style={{
backgroundColor: isDark ? 'rgba(34,197,94,0.06)' : 'rgba(34,197,94,0.04)',
}}
>
<span className="font-mono text-caption font-bold" style={{ color: '#22c55e' }}>
Content
</span>
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(34,197,94,0.18)' : 'rgba(34,197,94,0.12)',
color: '#22c55e',
}}
>
KRDS · Main Contents
</span>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
flex-1 / overflow-y-auto
</span>
</div>
{/* Right Panel (조건부) */}
<div
className="flex flex-col items-center justify-center gap-2 border-l border-dashed"
style={{
width: '120px',
backgroundColor: isDark ? 'rgba(249,115,22,0.06)' : 'rgba(249,115,22,0.04)',
borderColor: isDark ? 'rgba(249,115,22,0.25)' : 'rgba(249,115,22,0.20)',
}}
>
<span className="font-mono text-caption font-bold" style={{ color: '#f97316' }}>
Right Panel
</span>
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(249,115,22,0.15)' : 'rgba(249,115,22,0.10)',
color: '#f97316',
}}
>
KRDS · Right Menu
</span>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
</span>
</div>
</div>
{/* Footer 안내 */}
<div
className="border-t border-dashed px-4 py-2 flex items-center justify-center gap-2"
style={{
borderColor: isDark ? 'rgba(140,144,159,0.20)' : 'rgba(148,163,184,0.30)',
backgroundColor: isDark ? 'rgba(140,144,159,0.04)' : 'rgba(148,163,184,0.04)',
}}
>
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{ backgroundColor: dimBg, color: dimText }}
>
KRDS · Footer
</span>
<span className="font-korean text-caption" style={{ color: dimText }}>
</span>
</div>
</div>
{/* 매핑 표 (다이어그램 부속) */}
<div
className="rounded-lg border border-solid overflow-hidden w-full"
style={{
backgroundColor: t.tableContainerBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
<div
className="grid"
style={{
gridTemplateColumns: '160px 160px 1fr',
backgroundColor: t.tableHeaderBg,
borderBottom: `1px solid ${t.tableRowBorder}`,
}}
>
{(['KRDS Region', 'WING-OPS Component', 'Implementation'] as const).map((col) => (
<div key={col} className="py-2.5 px-4">
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
{col}
</span>
</div>
))}
</div>
{SUB_PAGE_MAPPINGS.map((row, idx) => (
<div
key={row.krdsRegion}
className="grid items-center"
style={{
gridTemplateColumns: '160px 160px 1fr',
borderTop: idx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
}}
>
<div className="py-2.5 px-4 flex items-center gap-2">
<div
className="w-2 h-2 rounded-full shrink-0"
style={{ backgroundColor: row.color }}
/>
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
{row.krdsRegion}
</span>
</div>
<div className="py-2.5 px-4">
<span className="font-mono text-caption" style={{ color: t.textSecondary }}>
{row.wingOpsComponent}
</span>
</div>
<div className="py-2.5 px-4">
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
{row.implementation}
</span>
</div>
</div>
))}
</div>
</div>
{/* ── 섹션 5: Spacing Scale 막대 시각화 ── */}
<div className="w-full flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
Spacing
</h2>
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
UI . Tailwind
spacing , px .
</p>
</div>
<div
className="rounded-lg border border-solid p-6 flex flex-col gap-2.5"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder, boxShadow: t.cardShadow }}
>
{SPACING_TOKENS.map((token) => {
const widthPct = (token.px / SPACING_MAX_PX) * 100;
return (
<div
key={token.className}
className="grid items-center"
style={{ gridTemplateColumns: '90px 60px 1fr 200px' }}
>
{/* 토큰 배지 */}
<span
className="font-mono text-caption rounded border border-solid px-2 py-0.5 inline-block w-fit"
style={{
color: t.textAccent,
backgroundColor: accentTintLight,
borderColor: isDark ? 'rgba(76,215,246,0.30)' : 'rgba(6,182,212,0.25)',
}}
>
{token.className}
</span>
{/* px 값 */}
<span
className="font-mono text-caption font-bold text-right pr-3"
style={{ color: t.textPrimary }}
>
{token.px}px
</span>
{/* 막대 시각화 */}
<div className="flex items-center">
<div
className="rounded-sm"
style={{
width: `${widthPct}%`,
minWidth: '4px',
height: '14px',
backgroundColor: accentTint,
border: `1px solid ${isDark ? 'rgba(76,215,246,0.40)' : 'rgba(6,182,212,0.35)'}`,
}}
/>
</div>
{/* 사용처 */}
<span
className="font-korean text-caption text-right"
style={{ color: t.textMuted }}
>
{token.usage}
</span>
</div>
);
})}
</div>
</div>
{/* ── 섹션 6: 4pt Grid 원칙 + Multiplier ── */}
<div className="w-full flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
4pt Grid
</h2>
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
4point . ,
2 .
</p>
</div>
<div className="grid grid-cols-2 gap-5">
{/* Part A — 원칙 카드 */}
<div
className="rounded-lg border border-solid p-5 flex flex-col gap-3"
style={{
backgroundColor: t.cardBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
Principles
</span>
<div className="flex flex-col gap-3">
{[
{
label: '기본',
text: '4px base — 모든 spacing은 4의 배수',
example: '4 / 8 / 12 / 16 / 20 / 24 / 32',
},
{
label: '권장',
text: '8pt 배수 우선 — gap-2, p-4, gap-6',
example: '8 / 16 / 24 / 32 / 64',
},
{
label: '예외',
text: '컴팩트 컴포넌트만 4px / 12px 사용',
example: 'gap-1, gap-3, p-1.5',
},
].map((item) => (
<div key={item.label} className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{ backgroundColor: accentTintLight, color: t.textAccent }}
>
{item.label}
</span>
<span className="font-korean text-xs" style={{ color: t.textPrimary }}>
{item.text}
</span>
</div>
<span className="font-mono text-caption pl-1" style={{ color: t.textMuted }}>
{item.example}
</span>
</div>
))}
</div>
</div>
{/* Part B — Multiplier 어노테이션 시각화 */}
<div
className="rounded-lg border border-solid p-5 flex flex-col gap-3"
style={{
backgroundColor: t.cardBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
Multiplier Annotation
</span>
<div
className="rounded relative flex items-center justify-center"
style={{
backgroundColor: isDark ? 'rgba(10,14,26,0.50)' : 'rgba(248,250,252,0.80)',
minHeight: '220px',
padding: '40px',
}}
>
{/* 어노테이션 — top */}
<div className="absolute top-3 left-1/2 -translate-x-1/2 flex items-center gap-1">
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(66,71,84,0.50)' : '#1e293b',
color: '#ffffff',
fontSize: '10px',
}}
>
{CARD_ANNOTATIONS[0].multiplier}={CARD_ANNOTATIONS[0].px}
</span>
</div>
{/* 어노테이션 — left */}
<div className="absolute left-3 top-1/2 -translate-y-1/2">
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(66,71,84,0.50)' : '#1e293b',
color: '#ffffff',
fontSize: '10px',
}}
>
{CARD_ANNOTATIONS[1].multiplier}={CARD_ANNOTATIONS[1].px}
</span>
</div>
{/* 어노테이션 — right */}
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(66,71,84,0.50)' : '#1e293b',
color: '#ffffff',
fontSize: '10px',
}}
>
{CARD_ANNOTATIONS[2].multiplier}={CARD_ANNOTATIONS[2].px}
</span>
</div>
{/* 어노테이션 — bottom */}
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1">
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(66,71,84,0.50)' : '#1e293b',
color: '#ffffff',
fontSize: '10px',
}}
>
{CARD_ANNOTATIONS[3].multiplier}={CARD_ANNOTATIONS[3].px}
</span>
</div>
{/* 실제 카드 mockup */}
<div
className="rounded-lg border border-solid flex flex-col gap-2 px-5 py-4"
style={{
backgroundColor: cardSurface,
borderColor: isDark ? 'rgba(76,215,246,0.40)' : 'rgba(6,182,212,0.30)',
minWidth: '180px',
}}
>
<span className="font-korean text-xs font-bold" style={{ color: t.textPrimary }}>
</span>
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
</span>
<div
className="rounded px-2 py-1 self-start mt-1"
style={{ backgroundColor: accentTintLight }}
>
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
Action
</span>
</div>
</div>
</div>
<p className="font-korean text-caption" style={{ color: t.textMuted }}>
spacing 4px multiplier를 .
</p>
</div>
</div>
</div>
{/* ── 섹션 7: Z-Index Layers ── */}
<div className="w-full flex flex-col gap-6">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
Z-Index Layers
</h2>
<span
className="font-mono text-caption rounded px-2 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.10)' : 'rgba(6,182,212,0.08)',
color: t.textAccent,
}}
>
</span>
</div>
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
UI . z-index가 ,
.
</p>
</div>
<div className="flex flex-col gap-0" style={{ maxWidth: '600px' }}>
{Z_LAYERS.map((layer, idx) => (
<div
key={layer.name}
className="flex flex-row items-stretch"
style={{ marginLeft: `${idx * 16}px` }}
>
<div className="w-12 shrink-0 flex items-center justify-end pr-3">
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
{layer.zIndex}
</span>
</div>
<div
className="flex-1 flex flex-row items-center gap-3 px-4 py-3 border border-solid"
style={{
backgroundColor: isDark ? `${layer.color}15` : `${layer.color}0c`,
borderColor: isDark ? `${layer.color}35` : `${layer.color}28`,
borderRadius: '6px',
marginBottom: '-1px',
}}
>
<div
className="w-2 h-2 rounded-full shrink-0"
style={{ backgroundColor: layer.color }}
/>
<span className="font-mono text-xs font-bold" style={{ color: layer.color }}>
{layer.name}
</span>
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
{layer.description}
</span>
</div>
</div>
))}
</div>
</div>
{/* ── 섹션 8: CSS 클래스 + KRDS Grid Rules 통합 ── */}
<div className="w-full flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
Reference
</h2>
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
App Shell CSS KRDS Grid .
</p>
</div>
<div className="grid grid-cols-2 gap-5">
{/* CSS 클래스 */}
<div
className="rounded-lg border border-solid overflow-hidden"
style={{
backgroundColor: t.tableContainerBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
<div
className="px-4 py-2.5 border-b border-solid"
style={{
backgroundColor: t.tableHeaderBg,
borderColor: t.tableRowBorder,
}}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
CSS Classes
</span>
</div>
{SHELL_CLASSES.map((cls, idx) => (
<div
key={cls.className}
className="px-4 py-3 flex flex-col gap-1"
style={{
borderTop: idx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
}}
>
<div className="flex items-center gap-2">
<span
className="font-mono text-caption rounded border border-solid px-1.5 py-0.5"
style={{
color: t.textAccent,
backgroundColor: accentTintLight,
borderColor: isDark ? 'rgba(76,215,246,0.30)' : 'rgba(6,182,212,0.25)',
}}
>
{cls.className}
</span>
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
{cls.role}
</span>
</div>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
{cls.styles}
</span>
</div>
))}
</div>
{/* KRDS Grid Rules */}
<div
className="rounded-lg border border-solid overflow-hidden"
style={{
backgroundColor: t.tableContainerBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
<div
className="px-4 py-2.5 border-b border-solid"
style={{
backgroundColor: t.tableHeaderBg,
borderColor: t.tableRowBorder,
}}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
KRDS vs WING-OPS
</span>
</div>
{GRID_RULES.map((rule, idx) => (
<div
key={rule.name}
className="px-4 py-3 flex flex-col gap-1"
style={{
borderTop: idx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
}}
>
<span className="font-mono text-caption font-bold" style={{ color: t.textPrimary }}>
{rule.name}
</span>
<div className="flex items-center gap-1.5">
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
KRDS:
</span>
<span className="font-mono text-caption" style={{ color: t.textSecondary }}>
{rule.krds}
</span>
</div>
<div className="flex items-center gap-1.5">
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
WING:
</span>
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
{rule.wingOps}
</span>
</div>
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
{rule.note}
</span>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default LayoutContent;