193 lines
6.9 KiB
TypeScript
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-<카테고리>-<슬러그></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>
|
|
);
|
|
}
|