File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
"use client";
import { useState, type ReactNode } from "react";
import { FoxButton } from "../core/components/fox-button";
import { FoxTextButton } from "../core/components/fox-text-button";
import { FoxLinkButton } from "../core/components/fox-link-button";
import { FoxIconButton } from "../core/components/fox-icon-button";
import { FoxButtonGroup } from "../core/components/fox-button-group";
import styles from "./dev-test.module.scss";
export interface ComponentExample {
/** 해시 링크에 쓰이는 kebab-case 식별자. */
id: string;
name: string;
description?: string;
/** 눈으로 비교해야 하는 조합을 빠짐없이 넣는다 — 예제가 곧 회귀 확인 수단이다. */
variants: { label: string; node: ReactNode }[];
}
// 자리 확인용 도형. `currentColor`로 그려야 계열별 색이 입혀진다.
function DemoIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M6 3l5 5-5 5" />
</svg>
);
}
function DemoStar() {
return (
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 1.6l1.9 4 4.4.6-3.2 3 .8 4.3L8 11.5l-3.9 2 .8-4.3-3.2-3 4.4-.6z" />
</svg>
);
}
// overlay 계열은 어두운 반투명 배경이라 밝은 바탕 위에서는 대비를 볼 수 없다.
function OnPhoto({ children }: { children: ReactNode }) {
return (
<span
style={{
display: "inline-flex",
gap: "0.8rem",
padding: "0.8rem",
borderRadius: "0.8rem",
backgroundImage: "url(https://picsum.photos/seed/picsum/200/300)",
backgroundSize: "cover",
}}
>
{children}
</span>
);
}
// active는 호출부가 소유하므로 켜고 끄는 걸 보려면 예제가 상태를 들고 있어야 한다.
function IconToggleDemo() {
const [on, setOn] = useState(false);
return (
<>
<FoxIconButton
size="md"
type="default"
label={on ? "즐겨찾기 해제" : "즐겨찾기"}
icon={<DemoStar />}
active={on}
onAction={() => setOn((prev) => !prev)}
/>
<FoxIconButton
size="md"
type="ghost"
label={on ? "즐겨찾기 해제" : "즐겨찾기"}
icon={<DemoStar />}
active={on}
onAction={() => setOn((prev) => !prev)}
/>
<OnPhoto>
<FoxIconButton
size="md"
type="overlay"
label={on ? "즐겨찾기 해제" : "즐겨찾기"}
icon={<DemoStar />}
active={on}
onAction={() => setOn((prev) => !prev)}
/>
</OnPhoto>
</>
);
}
/** 컴포넌트를 만들 때마다 항목 하나를 추가한다 — 다른 파일은 고치지 않는다. */
export const COMPONENT_EXAMPLES: ComponentExample[] = [
{
id: "fox-button",
name: "FoxButton",
description:
"Figma 시안(관리자페이지 node 390:439)을 토큰만으로 옮겼습니다. 너비는 내용에 따라 가변이고 높이만 고정입니다. hover·pressed는 마우스를 올리거나 눌러 확인하세요. error 계열은 시안 미정이라 비어 있습니다.",
variants: [
{
label: "계열 (type) — hover·pressed 확인 가능",
node: (
<>
<FoxButton size="md" label="default" />
<FoxButton size="md" type="primary" label="primary" />
<FoxButton size="md" type="secondary" label="secondary" />
<FoxButton size="md" type="error" label="error (시안 미정)" />
</>
),
},
{
label: "크기 (size)",
node: (
<>
<FoxButton size="lg" label="lg" />
<FoxButton size="md" label="md" />
<FoxButton size="sm" label="sm" />
<FoxButton size="xsm" label="xsm" />
</>
),
},
{
label: "아이콘",
node: (
<>
<FoxButton size="md" label="leading" leadingIcon={<DemoIcon />} />
<FoxButton size="md" label="trailing" trailingIcon={<DemoIcon />} />
<FoxButton size="md" label="both" leadingIcon={<DemoIcon />} trailingIcon={<DemoIcon />} />
</>
),
},
{
label: "상태 — disabled / loading (둘 다 클릭이 막힌다)",
node: (
<>
<FoxButton size="md" label="normal" onAction={() => undefined} />
<FoxButton size="md" label="disabled" disabled />
<FoxButton size="md" label="loading" loading />
<FoxButton size="md" label="loading + icon" loading leadingIcon={<DemoIcon />} />
</>
),
},
{
label: "fullWidth",
node: <FoxButton size="md" label="fullWidth" fullWidth />,
},
{
label: "htmlType — 폼 안에서의 역할",
node: (
<>
<FoxButton size="md" label="button (기본)" />
<FoxButton size="md" label="submit" htmlType="submit" />
<FoxButton size="md" label="reset" htmlType="reset" />
</>
),
},
{
label: "hidden — 렌더되지 않는다(아래는 비어 있는 것이 정상)",
node: <FoxButton size="md" label="hidden" hidden />,
},
],
},
{
id: "fox-text-button",
name: "FoxTextButton",
description:
"Figma 시안(btn-txt, 컴포넌트 세트 106:2468)을 토큰만으로 옮겼습니다. 높이를 고정하지 않고 패딩과 line-height로만 크기를 잡습니다. lg만 상하 패딩과 간격이 한 단계 넓습니다.",
variants: [
{
label: "계열 (type) — primary는 굵기가 한 단계 더 굵다",
node: (
<>
<FoxTextButton size="md" type="primary" label="primary" />
<FoxTextButton size="md" type="secondary" label="secondary" />
</>
),
},
{
label: "크기 (size)",
node: (
<>
<FoxTextButton size="lg" label="lg" />
<FoxTextButton size="md" label="md" />
<FoxTextButton size="sm" label="sm" />
<FoxTextButton size="xsm" label="xsm" />
</>
),
},
{
label: "아이콘",
node: (
<>
<FoxTextButton size="md" label="leading" leadingIcon={<DemoIcon />} />
<FoxTextButton size="md" label="trailing" trailingIcon={<DemoIcon />} />
<FoxTextButton
size="md"
label="both"
leadingIcon={<DemoIcon />}
trailingIcon={<DemoIcon />}
/>
</>
),
},
{
label: "상태 — disabled",
node: (
<>
<FoxTextButton size="md" label="normal" onAction={() => undefined} />
<FoxTextButton size="md" label="disabled" disabled />
<FoxTextButton size="md" type="secondary" label="disabled (secondary)" disabled />
</>
),
},
],
},
{
id: "fox-link-button",
name: "FoxLinkButton",
description:
"Figma 시안(btn-lnk, 컴포넌트 세트 107:207)을 토큰만으로 옮겼습니다. href를 주면 <a>, 없으면 <button>으로 렌더합니다. 방문 색은 브라우저가 실제 방문 기록으로만 켜므로 아래에서 링크를 한 번 눌러 돌아오면 색이 바뀝니다. tertiary만 평소에 밑줄이 없고 hover에서만 생깁니다.",
variants: [
{
label: "계열 (type) — hover하면 tertiary에 밑줄이 생긴다",
node: (
<>
<FoxLinkButton size="md" type="primary" label="primary" href="https://example.com" />
<FoxLinkButton
size="md"
type="secondary"
label="secondary"
href="https://example.com"
/>
<FoxLinkButton size="md" type="tertiary" label="tertiary" href="https://example.com" />
</>
),
},
{
label: "크기 (size)",
node: (
<>
<FoxLinkButton size="lg" label="lg" href="https://example.com" />
<FoxLinkButton size="md" label="md" href="https://example.com" />
<FoxLinkButton size="sm" label="sm" href="https://example.com" />
</>
),
},
{
label: "아이콘 — 라벨 뒤에만 놓인다",
node: (
<>
<FoxLinkButton size="lg" label="lg" href="https://example.com" icon={<DemoIcon />} />
<FoxLinkButton size="md" label="md" href="https://example.com" icon={<DemoIcon />} />
<FoxLinkButton size="sm" label="sm" href="https://example.com" icon={<DemoIcon />} />
</>
),
},
{
label: "href 없음 — <button>으로 렌더된다(방문 색이 켜지지 않는다)",
node: (
<>
<FoxLinkButton size="md" label="onAction만" onAction={() => undefined} />
<FoxLinkButton size="md" type="tertiary" label="tertiary" onAction={() => undefined} />
</>
),
},
{
label: "상태 — disabled (앵커는 href가 빠져 포커스·클릭이 함께 사라진다)",
node: (
<>
<FoxLinkButton size="md" label="disabled (a)" href="https://example.com" disabled />
<FoxLinkButton size="md" label="disabled (button)" disabled />
</>
),
},
],
},
{
id: "fox-icon-button",
name: "FoxIconButton",
description:
"Figma 시안(btn-ico 400:6312 + btn-ico-toggle 404:5247)을 토큰만으로 옮겼습니다. 두 시안이 계열·크기·모양이 같고 켜짐 상태만 달라 한 컴포넌트로 합쳤습니다 — active를 주면 토글(aria-pressed)이 됩니다. ghost는 테두리가 없는 대신 아이콘이 한 단계 큽니다.",
variants: [
{
label: "계열 (type) — overlay는 사진 위에서 봐야 한다",
node: (
<>
<FoxIconButton size="md" type="default" label="default" icon={<DemoIcon />} />
<FoxIconButton size="md" type="ghost" label="ghost" icon={<DemoIcon />} />
<OnPhoto>
<FoxIconButton size="md" type="overlay" label="overlay" icon={<DemoIcon />} />
</OnPhoto>
</>
),
},
{
label: "크기 (size) — default 계열",
node: (
<>
<FoxIconButton size="lg" label="lg" icon={<DemoIcon />} />
<FoxIconButton size="md" label="md" icon={<DemoIcon />} />
<FoxIconButton size="sm" label="sm" icon={<DemoIcon />} />
<FoxIconButton size="xsm" label="xsm" icon={<DemoIcon />} />
</>
),
},
{
label: "크기 (size) — ghost는 같은 상자에 아이콘만 한 단계 크다",
node: (
<>
<FoxIconButton size="lg" type="ghost" label="lg" icon={<DemoIcon />} />
<FoxIconButton size="md" type="ghost" label="md" icon={<DemoIcon />} />
<FoxIconButton size="sm" type="ghost" label="sm" icon={<DemoIcon />} />
<FoxIconButton size="xsm" type="ghost" label="xsm" icon={<DemoIcon />} />
</>
),
},
{
label: "토글 — 눌러서 켜고 끈다 (active)",
node: <IconToggleDemo />,
},
{
label: "토글 켜짐 고정 — 계열별 색 비교",
node: (
<>
<FoxIconButton size="md" type="default" label="on" icon={<DemoStar />} active />
<FoxIconButton size="md" type="ghost" label="on" icon={<DemoStar />} active />
<OnPhoto>
<FoxIconButton size="md" type="overlay" label="on" icon={<DemoStar />} active />
</OnPhoto>
</>
),
},
{
label: "상태 — disabled (켜짐보다 우선한다)",
node: (
<>
<FoxIconButton size="md" label="disabled" icon={<DemoIcon />} disabled />
<FoxIconButton size="md" type="ghost" label="disabled" icon={<DemoIcon />} disabled />
<FoxIconButton
size="md"
type="overlay"
label="disabled"
icon={<DemoIcon />}
disabled
/>
<FoxIconButton
size="md"
label="active + disabled"
icon={<DemoStar />}
active
disabled
/>
</>
),
},
],
},
{
id: "fox-button-group",
name: "FoxButtonGroup",
description:
"간격만 Figma 시안(btn-group, 컴포넌트 세트 398:4854)에서 가져왔습니다 — 크기·방향과 무관하게 gap-2(4px) 하나뿐이라 그룹에는 크기별 스타일이 없습니다. 크기는 그룹이 단일 진실 공급원이라 자식이 저마다 다른 size를 줘도 그룹 값으로 통일됩니다. 세로일 때는 자식이 항상 폭을 채웁니다. 블록 레벨 flex라 부모 폭을 채웁니다.",
variants: [
{
label: "가로 · 정렬 (align) — 회색 테두리가 그룹 영역",
node: (
<div className={styles.demoStack}>
{(["start", "center", "end"] as const).map((align) => (
<div key={align} className={styles.demoOutline}>
<FoxButtonGroup size="md" align={align}>
<FoxButton size="md" label={align} />
<FoxButton size="md" type="primary" label="확인" />
</FoxButtonGroup>
</div>
))}
</div>
),
},
{
label: "크기 (size) — 자식이 준 size는 무시된다",
node: (
<div className={styles.demoStack}>
{(["lg", "md", "sm", "xsm"] as const).map((size) => (
<FoxButtonGroup key={size} size={size}>
{/* 일부러 어긋난 size를 준다 — 전부 그룹 값으로 통일되어야 정상 */}
<FoxButton size="lg" label={size} />
<FoxButton size="xsm" type="primary" label="확인" />
<FoxIconButton size="lg" label="공유" icon={<DemoIcon />} />
</FoxButtonGroup>
))}
</div>
),
},
{
label: "세로 — 자식이 항상 폭을 채운다",
node: (
<div className={styles.demoOutline}>
<FoxButtonGroup size="md" orientation="vertical">
<FoxButton size="md" label="취소" />
<FoxButton size="md" type="primary" label="확인" />
</FoxButtonGroup>
</div>
),
},
{
label: "wrap — 폭이 모자라면 다음 줄로 넘어간다",
node: (
<div className={styles.demoStack}>
<div className={styles.demoNarrow}>
<FoxButtonGroup size="md" wrap>
{["첫째", "둘째", "셋째", "넷째", "다섯째"].map((label) => (
<FoxButton key={label} size="md" label={label} />
))}
</FoxButtonGroup>
</div>
<div className={styles.demoNarrow}>
<FoxButtonGroup size="md">
{["첫째", "둘째", "셋째", "넷째", "다섯째"].map((label) => (
<FoxButton key={label} size="md" label={label} />
))}
</FoxButtonGroup>
</div>
</div>
),
},
{
label: "버튼이 아닌 자식 — 호스트 요소는 건드리지 않는다",
node: (
<FoxButtonGroup size="sm" align="center">
<FoxButton size="sm" label="이전" />
<span className={styles.demoText}>1 / 10</span>
<FoxButton size="sm" label="다음" />
</FoxButtonGroup>
),
},
{
label: "프래그먼트로 감싼 자식도 덮어쓴다",
node: (
<FoxButtonGroup size="xsm">
<>
<FoxButton size="lg" label="프래그먼트 안" />
<FoxButton size="lg" type="primary" label="확인" />
</>
<FoxButton size="lg" label="프래그먼트 밖" />
</FoxButtonGroup>
),
},
],
},
];