+++ @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
... | ... | @@ -1,7 +1,9 @@ |
| 1 | 1 |
"use client"; |
| 2 | 2 |
|
| 3 | 3 |
import { useSyncExternalStore } from "react";
|
| 4 |
+import { FoxBadge } from "../core/components/fox-badge";
|
|
| 4 | 5 |
import { cx } from "../core/utils";
|
| 6 |
+import { COMPONENT_GROUPS } from "./component-groups";
|
|
| 5 | 7 |
import { COMPONENT_EXAMPLES } from "./component-registry";
|
| 6 | 8 |
import { ComponentEmpty, ComponentView } from "./component-view";
|
| 7 | 9 |
import type { CssSection } from "./read-css-tokens";
|
... | ... | @@ -56,6 +58,34 @@ |
| 56 | 58 |
return groups; |
| 57 | 59 |
} |
| 58 | 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 }] });
|
|
| 84 |
+ } |
|
| 85 |
+ |
|
| 86 |
+ return groups; |
|
| 87 |
+} |
|
| 88 |
+ |
|
| 59 | 89 |
export function DevTestPage() {
|
| 60 | 90 |
const selection = useSelection(); |
| 61 | 91 |
const sections = useCssTokens(); |
... | ... | @@ -103,7 +133,9 @@ |
| 103 | 133 |
onClick={() => select("token", section.id)}
|
| 104 | 134 |
> |
| 105 | 135 |
<span>{section.label}</span>
|
| 106 |
- <span className={styles.navCount}>{section.tokens.length}</span>
|
|
| 136 |
+ <FoxBadge type="pastel" color="neutral" size="sm"> |
|
| 137 |
+ {String(section.tokens.length)}
|
|
| 138 |
+ </FoxBadge> |
|
| 107 | 139 |
</button> |
| 108 | 140 |
</li> |
| 109 | 141 |
))} |
... | ... | @@ -114,25 +146,41 @@ |
| 114 | 146 |
|
| 115 | 147 |
<div className={styles.navGroup}>
|
| 116 | 148 |
<h2 className={styles.navGroupTitle}>컴포넌트 · {COMPONENT_EXAMPLES.length}</h2>
|
| 117 |
- <ul className={styles.navList}>
|
|
| 118 |
- {COMPONENT_EXAMPLES.map((example) => (
|
|
| 119 |
- <li key={example.id}>
|
|
| 120 |
- <button |
|
| 121 |
- type="button" |
|
| 122 |
- className={styles.navItem}
|
|
| 123 |
- aria-current={
|
|
| 124 |
- current.kind === "component" && current.id === example.id |
|
| 125 |
- ? "page" |
|
| 126 |
- : undefined |
|
| 127 |
- } |
|
| 128 |
- onClick={() => select("component", example.id)}
|
|
| 129 |
- > |
|
| 130 |
- <span>{example.name}</span>
|
|
| 131 |
- <span className={styles.navCount}>{example.variants.length}</span>
|
|
| 132 |
- </button> |
|
| 133 |
- </li> |
|
| 134 |
- ))} |
|
| 135 | 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}>
|
|
| 136 | 184 |
{/* 컴포넌트가 아니라 안내라 항상 맨 아래에 두고 생김새를 달리한다. */}
|
| 137 | 185 |
<li> |
| 138 | 186 |
<button |
--- @fox/dev-test/dev-test.module.scss
+++ @fox/dev-test/dev-test.module.scss
... | ... | @@ -9,12 +9,15 @@ |
| 9 | 9 |
.shell {
|
| 10 | 10 |
display: grid; |
| 11 | 11 |
grid-template-columns: 1fr; |
| 12 |
- min-block-size: 100vh; |
|
| 12 |
+ min-block-size: 100dvh; |
|
| 13 | 13 |
background: fox.color(background-default); |
| 14 | 14 |
color: fox.color(font-neutral-default); |
| 15 | 15 |
|
| 16 | 16 |
@include fox.pc {
|
| 17 |
+ // 화면을 넘지 않게 못박고 스크롤은 두 칸이 각자 갖는다. |
|
| 18 |
+ block-size: 100dvh; |
|
| 17 | 19 |
grid-template-columns: 26rem 1fr; |
| 20 |
+ overflow: hidden; |
|
| 18 | 21 |
} |
| 19 | 22 |
} |
| 20 | 23 |
|
... | ... | @@ -34,9 +37,7 @@ |
| 34 | 37 |
background: fox.color(surface-neutral-gray); |
| 35 | 38 |
|
| 36 | 39 |
@include fox.pc {
|
| 37 |
- position: sticky; |
|
| 38 |
- inset-block-start: 0; |
|
| 39 |
- block-size: 100vh; |
|
| 40 |
+ min-block-size: 0; |
|
| 40 | 41 |
overflow-y: auto; |
| 41 | 42 |
border-block-end: 0; |
| 42 | 43 |
border-inline-end: fox.border(1) solid fox.color(border-neutral-subtle); |
... | ... | @@ -57,34 +58,6 @@ |
| 57 | 58 |
.brandNote {
|
| 58 | 59 |
color: fox.color(font-neutral-subtler); |
| 59 | 60 |
font-size: fox.font-size(label-xsm); |
| 60 |
-} |
|
| 61 |
- |
|
| 62 |
-.themeSwitch {
|
|
| 63 |
- display: flex; |
|
| 64 |
- gap: fox.gap(1); |
|
| 65 |
- padding: fox.padding(1); |
|
| 66 |
- border: fox.border(1) solid fox.color(border-neutral-default); |
|
| 67 |
- border-radius: fox.radius(max); |
|
| 68 |
- background: fox.color(surface-neutral-default); |
|
| 69 |
-} |
|
| 70 |
- |
|
| 71 |
-.themeButton {
|
|
| 72 |
- flex: 1; |
|
| 73 |
- padding: fox.padding(3) fox.padding(4); |
|
| 74 |
- border-radius: fox.radius(max); |
|
| 75 |
- color: fox.color(font-neutral-subtle); |
|
| 76 |
- font-size: fox.font-size(label-xsm); |
|
| 77 |
- font-weight: fox.font-weight(medium); |
|
| 78 |
- |
|
| 79 |
- &[aria-pressed="true"] {
|
|
| 80 |
- background: fox.color(button-primary-surface); |
|
| 81 |
- color: fox.color(button-primary-font); |
|
| 82 |
- } |
|
| 83 |
- |
|
| 84 |
- &:focus-visible {
|
|
| 85 |
- outline: fox.border(2) solid fox.color(border-theme-primary); |
|
| 86 |
- outline-offset: fox.border(2); |
|
| 87 |
- } |
|
| 88 | 61 |
} |
| 89 | 62 |
|
| 90 | 63 |
.nav {
|
... | ... | @@ -121,6 +94,23 @@ |
| 121 | 94 |
font-size: fox.font-size(label-xsm); |
| 122 | 95 |
font-weight: fox.font-weight(medium); |
| 123 | 96 |
opacity: 0.8; |
| 97 |
+} |
|
| 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; |
|
| 124 | 114 |
} |
| 125 | 115 |
|
| 126 | 116 |
.navList {
|
... | ... | @@ -160,12 +150,6 @@ |
| 160 | 150 |
} |
| 161 | 151 |
} |
| 162 | 152 |
|
| 163 |
-.navCount {
|
|
| 164 |
- color: currentcolor; |
|
| 165 |
- font-size: fox.font-size(label-xsm); |
|
| 166 |
- opacity: 0.7; |
|
| 167 |
-} |
|
| 168 |
- |
|
| 169 | 153 |
// "추가 방법"은 컴포넌트가 아니라 안내다. 목록 맨 아래에 두고 점선 테두리로 구분해, |
| 170 | 154 |
// 등록된 컴포넌트 탭과 같은 층위로 읽히지 않게 한다. |
| 171 | 155 |
// `.navItem` 뒤에 와야 선택 상태(aria-current)를 덮어쓴다 — 특이도가 같아 순서가 정한다. |
... | ... | @@ -200,6 +184,11 @@ |
| 200 | 184 |
padding: fox.spacing(top-md) fox.padding(7) fox.spacing(bottom-xlg); |
| 201 | 185 |
|
| 202 | 186 |
@include fox.pc {
|
| 187 |
+ min-block-size: 0; |
|
| 188 |
+ overflow-y: auto; |
|
| 189 |
+ } |
|
| 190 |
+ |
|
| 191 |
+ @include fox.pc {
|
|
| 203 | 192 |
padding: fox.spacing(top-lg) fox.padding(11) fox.spacing(bottom-xlg); |
| 204 | 193 |
} |
| 205 | 194 |
} |
--- @fox/dev-test/theme-switch.tsx
+++ @fox/dev-test/theme-switch.tsx
... | ... | @@ -1,9 +1,9 @@ |
| 1 | 1 |
"use client"; |
| 2 | 2 |
|
| 3 | 3 |
import { useSyncExternalStore } from "react";
|
| 4 |
-import styles from "./dev-test.module.scss"; |
|
| 4 |
+import { FoxSegmentedControl } from "../core/components/fox-segmented-control";
|
|
| 5 |
+import { FoxSegmentedControlItem } from "../core/components/fox-segmented-control-item";
|
|
| 5 | 6 |
|
| 6 |
-/** `@fox/styles/_root.scss`가 읽는 수동 테마 선택 속성. */ |
|
| 7 | 7 |
const THEME_ATTRIBUTE = "data-theme"; |
| 8 | 8 |
|
| 9 | 9 |
type ThemeChoice = "system" | "light" | "dark"; |
... | ... | @@ -14,10 +14,6 @@ |
| 14 | 14 |
{ value: "dark", label: "다크" },
|
| 15 | 15 |
]; |
| 16 | 16 |
|
| 17 |
-/** |
|
| 18 |
- * `<html data-theme>`가 SSOT라 React state를 두지 않는다. 저장 키는 호스트 앱이 |
|
| 19 |
- * 소유하는 값이라 `@fox`가 알면 포터빌리티가 깨지므로 선택을 저장하지 않는다. |
|
| 20 |
- */ |
|
| 21 | 17 |
function subscribe(onChange: () => void): () => void {
|
| 22 | 18 |
const observer = new MutationObserver(onChange); |
| 23 | 19 |
observer.observe(document.documentElement, {
|
... | ... | @@ -49,18 +45,16 @@ |
| 49 | 45 |
const current = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); |
| 50 | 46 |
|
| 51 | 47 |
return ( |
| 52 |
- <div className={styles.themeSwitch} role="group" aria-label="테마 전환">
|
|
| 48 |
+ <FoxSegmentedControl size="sm" name="dev-test-theme" label="테마 전환"> |
|
| 53 | 49 |
{CHOICES.map(({ value, label }) => (
|
| 54 |
- <button |
|
| 50 |
+ <FoxSegmentedControlItem |
|
| 55 | 51 |
key={value}
|
| 56 |
- type="button" |
|
| 57 |
- className={styles.themeButton}
|
|
| 58 |
- aria-pressed={current === value}
|
|
| 59 |
- onClick={() => apply(value)}
|
|
| 60 |
- > |
|
| 61 |
- {label}
|
|
| 62 |
- </button> |
|
| 52 |
+ value={value}
|
|
| 53 |
+ label={label}
|
|
| 54 |
+ checked={current === value}
|
|
| 55 |
+ onChange={(next) => apply(next as ThemeChoice)}
|
|
| 56 |
+ /> |
|
| 63 | 57 |
))} |
| 64 |
- </div> |
|
| 58 |
+ </FoxSegmentedControl> |
|
| 65 | 59 |
); |
| 66 | 60 |
} |
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?