File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
"use client";
import { useSyncExternalStore } from "react";
import { COMPONENT_EXAMPLES } from "./component-registry";
import { ComponentEmpty, ComponentView } from "./component-view";
import { ThemeSwitch } from "./theme-switch";
import { TOKEN_GROUPS, TokenView } from "./token-view";
import { useTokenRegistry } from "./use-token-registry";
import styles from "./dev-test.module.scss";
// 선택 상태는 URL 해시가 SSOT다 — 새로고침하거나 링크를 공유해도 보던 섹션이 유지되고,
// React state를 따로 두지 않으므로 둘이 어긋날 일이 없다.
// 형식: `#token:color` / `#component: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}`;
}
export function DevTestPage() {
const registry = useTokenRegistry();
const selection = useSelection();
if (!registry) {
return (
<div className={styles.shell}>
<p className={styles.loading}>토큰을 읽는 중…</p>
</div>
);
}
// 사이드바에 뜰 토큰 그룹 — 정의된 순서를 우선하고, 목록에 없는 그룹은 뒤에 붙인다.
const known = TOKEN_GROUPS.filter((group) => registry[group.key]?.length);
const extra = Object.keys(registry)
.filter((key) => !TOKEN_GROUPS.some((group) => group.key === key))
.map((key) => ({ key, label: key }));
const tokenGroups = [...known, ...extra];
const current = selection ?? { kind: "token", id: tokenGroups[0]?.key ?? "" };
const totalTokens = Object.values(registry).reduce((sum, list) => sum + list.length, 0);
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>
<ul className={styles.navList}>
{tokenGroups.map((group) => (
<li key={group.key}>
<button
type="button"
className={styles.navItem}
aria-current={
current.kind === "token" && current.id === group.key ? "page" : undefined
}
onClick={() => select("token", group.key)}
>
<span>{group.label}</span>
<span className={styles.navCount}>{registry[group.key].length}</span>
</button>
</li>
))}
</ul>
</div>
<div className={styles.navGroup}>
<h2 className={styles.navGroupTitle}>컴포넌트 · {COMPONENT_EXAMPLES.length}</h2>
{COMPONENT_EXAMPLES.length ? (
<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>
<span className={styles.navCount}>{example.variants.length}</span>
</button>
</li>
))}
</ul>
) : (
<button
type="button"
className={styles.navItem}
aria-current={current.kind === "component" ? "page" : undefined}
onClick={() => select("component", "none")}
>
<span>추가 방법</span>
</button>
)}
</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 />;
})()
) : registry[current.id]?.length ? (
<TokenView group={current.id} tokens={registry[current.id]} />
) : (
<p className={styles.loading}>선택한 토큰 그룹이 없습니다.</p>
)}
</main>
</div>
);
}