kcg-ai-monitoring/frontend/src/design-system/DesignSystemApp.tsx

193 lines
6.9 KiB
TypeScript

import { useEffect, useState } from 'react';
import { TrkProvider, useTrk } from './lib/TrkContext';
import { IntroSection } from './sections/IntroSection';
import { TokenSection } from './sections/TokenSection';
import { TypographySection } from './sections/TypographySection';
import { BadgeSection } from './sections/BadgeSection';
import { ButtonSection } from './sections/ButtonSection';
import { FormSection } from './sections/FormSection';
import { CardSectionShowcase } from './sections/CardSection';
import { LayoutSection } from './sections/LayoutSection';
import { CatalogSection } from './sections/CatalogSection';
import { GuideSection } from './sections/GuideSection';
import './DesignSystemApp.css';
interface NavItem {
id: string;
label: string;
anchor: string;
}
const NAV_ITEMS: NavItem[] = [
{ id: 'intro', label: '1. 소개', anchor: 'TRK-SEC-intro' },
{ id: 'token', label: '2. 테마 · 토큰', anchor: 'TRK-SEC-token' },
{ id: 'typography', label: '3. 타이포그래피', anchor: 'TRK-SEC-typography' },
{ id: 'badge', label: '4. Badge', anchor: 'TRK-SEC-badge' },
{ id: 'button', label: '5. Button', anchor: 'TRK-SEC-button' },
{ id: 'form', label: '6. Form', anchor: 'TRK-SEC-form' },
{ id: 'card', label: '7. Card / Section', anchor: 'TRK-SEC-card' },
{ id: 'layout', label: '8. Layout', anchor: 'TRK-SEC-layout' },
{ id: 'catalog', label: '9. 분류 카탈로그', anchor: 'TRK-SEC-catalog' },
{ id: 'guide', label: '10. 예외 / 가이드', anchor: 'TRK-SEC-guide' },
];
function DesignSystemShell() {
const { copyMode, setCopyMode } = useTrk();
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
const [activeNav, setActiveNav] = useState<string>('intro');
// 테마 토글
useEffect(() => {
const root = document.documentElement;
root.classList.remove('dark', 'light');
root.classList.add(theme);
}, [theme]);
// 단축키: 'a' 입력 시 테마 전환 (input/textarea 등 편집 중에는 무시)
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key !== 'a' && e.key !== 'A') return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
const target = e.target as HTMLElement | null;
const tag = target?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target?.isContentEditable) {
return;
}
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []);
// 스크롤 감지로 현재 네비 하이라이트
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const id = entry.target.getAttribute('data-section-id');
if (id) setActiveNav(id);
}
}
},
{ rootMargin: '-40% 0px -55% 0px', threshold: 0 },
);
const sections = document.querySelectorAll('[data-section-id]');
sections.forEach((s) => observer.observe(s));
return () => observer.disconnect();
}, []);
const scrollTo = (anchor: string) => {
const el = document.querySelector(`[data-trk="${anchor}"]`);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
return (
<div className="ds-shell">
{/* 고정 헤더 */}
<header className="ds-header">
<div className="flex items-center gap-3">
<h1 className="text-base font-bold text-heading">KCG Design System</h1>
<code className="text-[10px] text-hint font-mono">v0.1.0 · </code>
</div>
<div className="flex items-center gap-3">
<label className="flex items-center gap-1.5 cursor-pointer text-xs text-label">
<input
type="checkbox"
checked={copyMode}
onChange={(e) => setCopyMode(e.target.checked)}
className="accent-blue-500"
/>
ID
</label>
<button
type="button"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
title="단축키: A"
className="px-3 py-1 rounded-md border border-slate-600/40 text-xs text-label hover:bg-slate-700/30 flex items-center gap-1.5"
>
<span>{theme === 'dark' ? '☾ Dark' : '☀ Light'}</span>
<kbd className="text-[9px] font-mono text-hint border border-slate-600/40 rounded px-1 py-0">A</kbd>
</button>
</div>
</header>
<div className="ds-body">
{/* 좌측 네비 */}
<nav className="ds-nav">
<div className="text-[10px] text-hint uppercase mb-2 tracking-wider">Sections</div>
<ul className="space-y-0.5">
{NAV_ITEMS.map((item) => (
<li key={item.id}>
<button
type="button"
onClick={() => scrollTo(item.anchor)}
className={`w-full text-left px-2 py-1.5 rounded text-xs transition-colors ${
activeNav === item.id
? 'bg-blue-500/15 text-blue-400 border-l-2 border-blue-500'
: 'text-label hover:bg-slate-700/20'
}`}
>
{item.label}
</button>
</li>
))}
</ul>
<div className="mt-6 pt-4 border-t border-slate-700/40 text-[10px] text-hint space-y-1">
<div>
<strong className="text-label"> ID </strong>
</div>
<code className="block font-mono">TRK-&lt;&gt;-&lt;&gt;</code>
<div className="mt-2">
: <code className="font-mono">#trk=ID</code>
</div>
</div>
</nav>
{/* 우측 컨텐츠 */}
<main className="ds-main">
<section data-section-id="intro">
<IntroSection />
</section>
<section data-section-id="token">
<TokenSection />
</section>
<section data-section-id="typography">
<TypographySection />
</section>
<section data-section-id="badge">
<BadgeSection />
</section>
<section data-section-id="button">
<ButtonSection />
</section>
<section data-section-id="form">
<FormSection />
</section>
<section data-section-id="card">
<CardSectionShowcase />
</section>
<section data-section-id="layout">
<LayoutSection />
</section>
<section data-section-id="catalog">
<CatalogSection />
</section>
<section data-section-id="guide">
<GuideSection />
</section>
</main>
</div>
</div>
);
}
export function DesignSystemApp() {
return (
<TrkProvider>
<DesignSystemShell />
</TrkProvider>
);
}