File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
08-19
"use client";
import { useSyncExternalStore } from "react";
import { FoxBadge } from "../core/components/fox-badge";
import { cx } from "../core/utils";
import { COMPONENT_EXAMPLES } from "./component-registry";
import { ComponentEmpty, ComponentView } from "./component-view";
import type { CssSection } from "./read-css-tokens";
import { ThemeSwitch } from "./theme-switch";
import { TokenView } from "./token-view";
import { useCssTokens } from "./use-css-tokens";
import styles from "./dev-test.module.scss";
// 선택 상태는 URL 해시가 SSOT다. 형식: `#token:light` / `#component:fox-button`
function subscribeHash(onChange: () => void): () => void {
window.addEventListener("hashchange", onChange);
return () => window.removeEventListener("hashchange", onChange);
}
function getHash(): string {
return window.location.hash.slice(1);
}
function getServerHash(): string {
return "";
}
function useSelection(): { kind: string; id: string } | null {
const hash = useSyncExternalStore(subscribeHash, getHash, getServerHash);
const [kind, id] = hash.split(":");
return kind && id ? { kind, id } : null;
}
function select(kind: string, id: string): void {
window.location.hash = `${kind}:${id}`;
}
const GUIDE_ID = "guide";
function isRegistered(id: string): boolean {
return COMPONENT_EXAMPLES.some((example) => example.id === id);
}
/** Figma 변수 페이지와 대조하는 화면이라 우리 CSS 그룹이 아닌 원본 구획을 따른다. */
function groupedSections(sections: CssSection[]) {
const groups: { category: string | null; sections: CssSection[] }[] = [];
for (const section of sections) {
const last = groups[groups.length - 1];
if (last && last.category === section.category && section.category !== null) {
last.sections.push(section);
} else {
groups.push({ category: section.category, sections: [section] });
}
}
return groups;
}
export function DevTestPage() {
const selection = useSelection();
const sections = useCssTokens();
if (!sections) {
return (
<div className={styles.shell}>
<p className={styles.loading}>스타일시트를 읽는 중…</p>
</div>
);
}
const totalTokens = sections.reduce((sum, section) => sum + section.tokens.length, 0);
const current = selection ?? { kind: "token", id: sections[0]?.id ?? "" };
const activeSection = sections.find((section) => section.id === current.id);
return (
<div className={styles.shell}>
<aside className={styles.sidebar}>
<div className={styles.brand}>
<span className={styles.brandTitle}>@fox 디자인 시스템</span>
<span className={styles.brandNote}>개발 전용 테스트 페이지</span>
</div>
<ThemeSwitch />
<nav className={styles.nav} aria-label="섹션">
<div className={styles.navGroup}>
<h2 className={styles.navGroupTitle}>토큰 · {totalTokens}</h2>
{groupedSections(sections).map((group) => (
<div key={group.category ?? group.sections[0].id} className={styles.navSubGroup}>
{group.category ? (
<h3 className={styles.navSubTitle}>{group.category}</h3>
) : null}
<ul className={styles.navList}>
{group.sections.map((section) => (
<li key={section.id}>
<button
type="button"
className={styles.navItem}
aria-current={
current.kind === "token" && current.id === section.id ? "page" : undefined
}
onClick={() => select("token", section.id)}
>
<span>{section.label}</span>
<FoxBadge type="pastel" color="neutral" size="sm">
{String(section.tokens.length)}
</FoxBadge>
</button>
</li>
))}
</ul>
</div>
))}
</div>
<div className={styles.navGroup}>
<h2 className={styles.navGroupTitle}>컴포넌트 · {COMPONENT_EXAMPLES.length}</h2>
<ul className={styles.navList}>
{COMPONENT_EXAMPLES.map((example) => (
<li key={example.id}>
<button
type="button"
className={styles.navItem}
aria-current={
current.kind === "component" && current.id === example.id
? "page"
: undefined
}
onClick={() => select("component", example.id)}
>
<span>{example.name}</span>
<FoxBadge type="pastel" color="neutral" size="sm">
{String(example.variants.length)}
</FoxBadge>
</button>
</li>
))}
{/* 컴포넌트가 아니라 안내라 항상 맨 아래에 두고 생김새를 달리한다. */}
<li>
<button
type="button"
className={cx(styles.navItem, styles.navItemGuide)}
aria-current={
current.kind === "component" && !isRegistered(current.id) ? "page" : undefined
}
onClick={() => select("component", GUIDE_ID)}
>
<span>추가 방법</span>
<span aria-hidden="true" className={styles.navGuideMark}>
+
</span>
</button>
</li>
</ul>
</div>
</nav>
</aside>
<main className={styles.content}>
{current.kind === "component" ? (
(() => {
const example = COMPONENT_EXAMPLES.find((entry) => entry.id === current.id);
return example ? <ComponentView example={example} /> : <ComponentEmpty />;
})()
) : activeSection ? (
<TokenView section={activeSection} />
) : (
<p className={styles.loading}>선택한 토큰 섹션이 없습니다.</p>
)}
</main>
</div>
);
}