feat: 디자인 테스트 페이지 컴포넌트 목록을 3depth로
대분류는 Figma 섹션을 그대로 따랐다 — Action(391:698) · Select(391:2209) · Input(391:3733) ·
Navigation(391:4035) · Layout(392:5035) · Feedback(392:5381) · Help(392:5712).
소분류는 각 대분류 안에서 성격이 같은 것끼리 묶었다.
분류표를 레지스트리와 분리해 component-groups.ts에 뒀다 — 4천 줄짜리 레지스트리를 건드리지 않고,
표에 없는 컴포넌트는 "기타"로 흘려 아무것도 목록에서 사라지지 않게 했다.
검증 — 화면 실측: 대분류 7개, 소분류 20개, 컴포넌트 62개 전부 배치("기타" 없음).
Action 버튼(4)·묶음(2) / Select 라디오(3)·체크박스(3)·셀렉트(4)·태그칩(4)·스위치(4)·부가(2) /
Input 텍스트(2)·형식입력(4)·파일(1) / Navigation 위치이동(2)·캐러셀(3) /
Layout 표목록(2)·탭(1)·배지(3)·펼침(2)·컨테이너(3) / Feedback 알림(4)·상태(3)·단계(2) /
Help 도움말(4).
Co-Authored-By: Claude Opus 5
@247d95b5783dad14fe14d235333d1656a9e3ce02
+++ @fox/dev-test/component-groups.ts
... | ... | @@ -0,0 +1,92 @@ |
| 1 | +/** | |
| 2 | + * 대분류는 Figma 섹션을 그대로 따른다. | |
| 3 | + * Action 391:698 · Select 391:2209 · Input 391:3733 · Navigation 391:4035 · | |
| 4 | + * Layout 392:5035 · Feedback 392:5381 · Help 392:5712 | |
| 5 | + */ | |
| 6 | +export type ComponentGroup = { | |
| 7 | + label: string; | |
| 8 | + subgroups: { label: string; ids: string[] }[]; | |
| 9 | +}; | |
| 10 | + | |
| 11 | +export const COMPONENT_GROUPS: ComponentGroup[] = [ | |
| 12 | + { | |
| 13 | + label: "Action", | |
| 14 | + subgroups: [ | |
| 15 | + { | |
| 16 | + label: "버튼", | |
| 17 | + ids: ["fox-button", "fox-text-button", "fox-link-button", "fox-icon-button"], | |
| 18 | + }, | |
| 19 | + { label: "묶음", ids: ["fox-button-group", "fox-button-panel"] }, | |
| 20 | + ], | |
| 21 | + }, | |
| 22 | + { | |
| 23 | + label: "Select", | |
| 24 | + subgroups: [ | |
| 25 | + { label: "라디오", ids: ["fox-radio-icon", "fox-radio", "fox-radio-group"] }, | |
| 26 | + { label: "체크박스", ids: ["fox-check-icon", "fox-checkbox", "fox-checkbox-group"] }, | |
| 27 | + { | |
| 28 | + label: "셀렉트", | |
| 29 | + ids: ["fox-select", "fox-select-option", "fox-select-option-item", "fox-select-text"], | |
| 30 | + }, | |
| 31 | + { label: "태그·칩", ids: ["fox-tag", "fox-tag-list", "fox-chip", "fox-chip-area"] }, | |
| 32 | + { | |
| 33 | + label: "스위치", | |
| 34 | + ids: [ | |
| 35 | + "fox-toggle-switch", | |
| 36 | + "fox-segmented-control", | |
| 37 | + "fox-segmented-control-item", | |
| 38 | + "fox-toggle-quantity", | |
| 39 | + ], | |
| 40 | + }, | |
| 41 | + { label: "부가", ids: ["fox-form-label", "fox-conditional-option"] }, | |
| 42 | + ], | |
| 43 | + }, | |
| 44 | + { | |
| 45 | + label: "Input", | |
| 46 | + subgroups: [ | |
| 47 | + { label: "텍스트", ids: ["fox-input", "fox-text-area"] }, | |
| 48 | + { | |
| 49 | + label: "형식 입력", | |
| 50 | + ids: ["fox-phone-number", "fox-email", "fox-certified", "fox-address"], | |
| 51 | + }, | |
| 52 | + { label: "파일", ids: ["fox-file-upload"] }, | |
| 53 | + ], | |
| 54 | + }, | |
| 55 | + { | |
| 56 | + label: "Navigation", | |
| 57 | + subgroups: [ | |
| 58 | + { label: "위치·이동", ids: ["fox-breadcrumb", "fox-pagination"] }, | |
| 59 | + { | |
| 60 | + label: "캐러셀", | |
| 61 | + ids: ["fox-carousel-indicator", "fox-carousel-button", "fox-carousel-area"], | |
| 62 | + }, | |
| 63 | + ], | |
| 64 | + }, | |
| 65 | + { | |
| 66 | + label: "Layout", | |
| 67 | + subgroups: [ | |
| 68 | + { label: "표·목록", ids: ["fox-table", "fox-bullet-list"] }, | |
| 69 | + { label: "탭", ids: ["fox-tab"] }, | |
| 70 | + { label: "배지", ids: ["fox-badge", "fox-badge-dot", "fox-badge-group"] }, | |
| 71 | + { label: "펼침", ids: ["fox-accordion", "fox-disclosure"] }, | |
| 72 | + { label: "컨테이너", ids: ["fox-card", "fox-modal", "fox-page-header"] }, | |
| 73 | + ], | |
| 74 | + }, | |
| 75 | + { | |
| 76 | + label: "Feedback", | |
| 77 | + subgroups: [ | |
| 78 | + { label: "알림", ids: ["fox-alert", "fox-toast", "fox-snackbar", "fox-snackbar-list"] }, | |
| 79 | + { label: "상태", ids: ["fox-status-indicator", "fox-helper-text", "fox-spinner"] }, | |
| 80 | + { label: "단계", ids: ["fox-step-indicator", "fox-step-indicator-item"] }, | |
| 81 | + ], | |
| 82 | + }, | |
| 83 | + { | |
| 84 | + label: "Help", | |
| 85 | + subgroups: [ | |
| 86 | + { | |
| 87 | + label: "도움말", | |
| 88 | + ids: ["fox-tooltip", "fox-tooltip-rich", "fox-popover", "fox-contextual-trigger"], | |
| 89 | + }, | |
| 90 | + ], | |
| 91 | + }, | |
| 92 | +]; |
--- @fox/dev-test/dev-test-page.tsx
+++ @fox/dev-test/dev-test-page.tsx
... | ... | @@ -3,6 +3,7 @@ |
| 3 | 3 |
import { useSyncExternalStore } from "react";
|
| 4 | 4 |
import { FoxBadge } from "../core/components/fox-badge";
|
| 5 | 5 |
import { cx } from "../core/utils";
|
| 6 |
+import { COMPONENT_GROUPS } from "./component-groups";
|
|
| 6 | 7 |
import { COMPONENT_EXAMPLES } from "./component-registry";
|
| 7 | 8 |
import { ComponentEmpty, ComponentView } from "./component-view";
|
| 8 | 9 |
import type { CssSection } from "./read-css-tokens";
|
... | ... | @@ -52,6 +53,34 @@ |
| 52 | 53 |
} else {
|
| 53 | 54 |
groups.push({ category: section.category, sections: [section] });
|
| 54 | 55 |
} |
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ return groups; |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+/** 분류표를 실제 예제로 바꾼다. 표에 없는 컴포넌트는 마지막 "기타"에 모인다. */ |
|
| 62 |
+function groupedComponents() {
|
|
| 63 |
+ const used = new Set<string>(); |
|
| 64 |
+ const groups = COMPONENT_GROUPS.map((group) => ({
|
|
| 65 |
+ label: group.label, |
|
| 66 |
+ subgroups: group.subgroups |
|
| 67 |
+ .map((subgroup) => ({
|
|
| 68 |
+ label: subgroup.label, |
|
| 69 |
+ examples: subgroup.ids.flatMap((id) => {
|
|
| 70 |
+ const example = COMPONENT_EXAMPLES.find((entry) => entry.id === id); |
|
| 71 |
+ if (!example) {
|
|
| 72 |
+ return []; |
|
| 73 |
+ } |
|
| 74 |
+ used.add(id); |
|
| 75 |
+ return [example]; |
|
| 76 |
+ }), |
|
| 77 |
+ })) |
|
| 78 |
+ .filter((subgroup) => subgroup.examples.length > 0), |
|
| 79 |
+ })).filter((group) => group.subgroups.length > 0); |
|
| 80 |
+ |
|
| 81 |
+ const rest = COMPONENT_EXAMPLES.filter((example) => !used.has(example.id)); |
|
| 82 |
+ if (rest.length > 0) {
|
|
| 83 |
+ groups.push({ label: "기타", subgroups: [{ label: "미분류", examples: rest }] });
|
|
| 55 | 84 |
} |
| 56 | 85 |
|
| 57 | 86 |
return groups; |
... | ... | @@ -117,27 +146,41 @@ |
| 117 | 146 |
|
| 118 | 147 |
<div className={styles.navGroup}>
|
| 119 | 148 |
<h2 className={styles.navGroupTitle}>컴포넌트 · {COMPONENT_EXAMPLES.length}</h2>
|
| 120 |
- <ul className={styles.navList}>
|
|
| 121 |
- {COMPONENT_EXAMPLES.map((example) => (
|
|
| 122 |
- <li key={example.id}>
|
|
| 123 |
- <button |
|
| 124 |
- type="button" |
|
| 125 |
- className={styles.navItem}
|
|
| 126 |
- aria-current={
|
|
| 127 |
- current.kind === "component" && current.id === example.id |
|
| 128 |
- ? "page" |
|
| 129 |
- : undefined |
|
| 130 |
- } |
|
| 131 |
- onClick={() => select("component", example.id)}
|
|
| 132 |
- > |
|
| 133 |
- <span>{example.name}</span>
|
|
| 134 |
- <FoxBadge type="pastel" color="neutral" size="sm"> |
|
| 135 |
- {String(example.variants.length)}
|
|
| 136 |
- </FoxBadge> |
|
| 137 |
- </button> |
|
| 138 |
- </li> |
|
| 139 |
- ))} |
|
| 140 | 149 |
|
| 150 |
+ {groupedComponents().map((group) => (
|
|
| 151 |
+ <div key={group.label} className={styles.navSubGroup}>
|
|
| 152 |
+ <h3 className={styles.navSubTitle}>{group.label}</h3>
|
|
| 153 |
+ |
|
| 154 |
+ {group.subgroups.map((subgroup) => (
|
|
| 155 |
+ <div key={subgroup.label} className={styles.navLeaf}>
|
|
| 156 |
+ <h4 className={styles.navLeafTitle}>{subgroup.label}</h4>
|
|
| 157 |
+ <ul className={styles.navList}>
|
|
| 158 |
+ {subgroup.examples.map((example) => (
|
|
| 159 |
+ <li key={example.id}>
|
|
| 160 |
+ <button |
|
| 161 |
+ type="button" |
|
| 162 |
+ className={styles.navItem}
|
|
| 163 |
+ aria-current={
|
|
| 164 |
+ current.kind === "component" && current.id === example.id |
|
| 165 |
+ ? "page" |
|
| 166 |
+ : undefined |
|
| 167 |
+ } |
|
| 168 |
+ onClick={() => select("component", example.id)}
|
|
| 169 |
+ > |
|
| 170 |
+ <span>{example.name}</span>
|
|
| 171 |
+ <FoxBadge type="pastel" color="neutral" size="sm"> |
|
| 172 |
+ {String(example.variants.length)}
|
|
| 173 |
+ </FoxBadge> |
|
| 174 |
+ </button> |
|
| 175 |
+ </li> |
|
| 176 |
+ ))} |
|
| 177 |
+ </ul> |
|
| 178 |
+ </div> |
|
| 179 |
+ ))} |
|
| 180 |
+ </div> |
|
| 181 |
+ ))} |
|
| 182 |
+ |
|
| 183 |
+ <ul className={styles.navList}>
|
|
| 141 | 184 |
{/* 컴포넌트가 아니라 안내라 항상 맨 아래에 두고 생김새를 달리한다. */}
|
| 142 | 185 |
<li> |
| 143 | 186 |
<button |
--- @fox/dev-test/dev-test.module.scss
+++ @fox/dev-test/dev-test.module.scss
... | ... | @@ -96,6 +96,23 @@ |
| 96 | 96 |
opacity: 0.8; |
| 97 | 97 |
} |
| 98 | 98 |
|
| 99 |
+// 3depth — 대분류(navSubTitle) 아래 소분류. |
|
| 100 |
+.navLeaf {
|
|
| 101 |
+ display: flex; |
|
| 102 |
+ flex-direction: column; |
|
| 103 |
+ gap: fox.gap(1); |
|
| 104 |
+ margin-block-start: fox.gap(1); |
|
| 105 |
+ padding-inline-start: fox.padding(3); |
|
| 106 |
+} |
|
| 107 |
+ |
|
| 108 |
+.navLeafTitle {
|
|
| 109 |
+ padding-inline: fox.padding(3); |
|
| 110 |
+ color: fox.color(font-neutral-subtler); |
|
| 111 |
+ font-size: fox.font-size(label-xsm); |
|
| 112 |
+ font-weight: fox.font-weight(regular); |
|
| 113 |
+ opacity: 0.7; |
|
| 114 |
+} |
|
| 115 |
+ |
|
| 99 | 116 |
.navList {
|
| 100 | 117 |
display: flex; |
| 101 | 118 |
flex-direction: column; |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?