@hjmds/design-contracts 1.12.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/avatar-fallback.d.ts +11 -0
- package/dist/avatar-fallback.d.ts.map +1 -1
- package/dist/avatar-fallback.js +21 -0
- package/dist/avatar-fallback.js.map +1 -1
- package/dist/base-recipes.d.ts +17 -0
- package/dist/base-recipes.d.ts.map +1 -1
- package/dist/base-recipes.js +17 -0
- package/dist/base-recipes.js.map +1 -1
- package/dist/catalog.d.ts +25 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/command-palette.d.ts +14 -9
- package/dist/command-palette.d.ts.map +1 -1
- package/dist/command-palette.js +8 -9
- package/dist/command-palette.js.map +1 -1
- package/dist/component-recipes.d.ts +17 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +5 -0
- package/dist/component-recipes.js.map +1 -1
- package/dist/provider-button.d.ts.map +1 -1
- package/dist/provider-button.js +3 -0
- package/dist/provider-button.js.map +1 -1
- package/dist/reactions.d.ts +10 -0
- package/dist/reactions.d.ts.map +1 -1
- package/dist/reactions.js +7 -0
- package/dist/reactions.js.map +1 -1
- package/dist/screen-patterns.d.ts +147 -0
- package/dist/screen-patterns.d.ts.map +1 -0
- package/dist/screen-patterns.js +149 -0
- package/dist/screen-patterns.js.map +1 -0
- package/dist/slider.d.ts +8 -0
- package/dist/slider.d.ts.map +1 -1
- package/dist/slider.js +6 -1
- package/dist/slider.js.map +1 -1
- package/dist/upload-item.d.ts +5 -0
- package/dist/upload-item.d.ts.map +1 -1
- package/dist/upload-item.js +5 -0
- package/dist/upload-item.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/action-session.md +3 -3
- package/docs/agreement.md +5 -0
- package/docs/avatar-fallback.md +7 -0
- package/docs/bottom-navigation.md +6 -0
- package/docs/brand-boundary.md +1 -1
- package/docs/button-label.md +6 -0
- package/docs/clipboard.md +3 -0
- package/docs/command-palette.md +45 -2
- package/docs/consumer-policy.md +5 -1
- package/docs/data-table.md +6 -4
- package/docs/dialog.md +8 -2
- package/docs/form.md +51 -0
- package/docs/generated/component-maturity.md +1 -1
- package/docs/generated/renderer-evidence.json +3 -3
- package/docs/generated/renderer-evidence.md +1 -1
- package/docs/generated/showcase-manifest.json +1 -1
- package/docs/link.md +8 -0
- package/docs/migration-native-legacy-removal.md +45 -1
- package/docs/optional-adapters.md +1 -1
- package/docs/password-field.md +5 -0
- package/docs/product-composition-adoption.md +40 -0
- package/docs/progress.md +19 -1
- package/docs/provider-button.md +13 -0
- package/docs/result.md +3 -0
- package/docs/screen-chrome.md +10 -0
- package/docs/screen-patterns.md +376 -0
- package/docs/sheet.md +12 -0
- package/docs/splitter.md +8 -2
- package/docs/theming.md +36 -29
- package/docs/toggle-group.md +7 -0
- package/docs/tour.md +7 -1
- package/docs/tree.md +5 -2
- package/docs/upload-item.md +7 -0
- package/docs/usage/README.md +236 -0
- package/docs/usage/STANDARD.md +108 -0
- package/docs/usage/components/accordion.md +107 -0
- package/docs/usage/components/activity-heatmap.md +104 -0
- package/docs/usage/components/affix.md +86 -0
- package/docs/usage/components/agreement.md +129 -0
- package/docs/usage/components/alert-dialog.md +130 -0
- package/docs/usage/components/anchor.md +96 -0
- package/docs/usage/components/aspect-ratio.md +89 -0
- package/docs/usage/components/asset.md +126 -0
- package/docs/usage/components/auth-provider-button.md +116 -0
- package/docs/usage/components/auth-screen-layout.md +129 -0
- package/docs/usage/components/avatar.md +114 -0
- package/docs/usage/components/badge.md +84 -0
- package/docs/usage/components/bottom-cta.md +125 -0
- package/docs/usage/components/bottom-info.md +99 -0
- package/docs/usage/components/bottom-navigation.md +136 -0
- package/docs/usage/components/breadcrumb.md +81 -0
- package/docs/usage/components/button.md +118 -0
- package/docs/usage/components/calendar.md +122 -0
- package/docs/usage/components/card.md +110 -0
- package/docs/usage/components/carousel.md +113 -0
- package/docs/usage/components/celebration.md +96 -0
- package/docs/usage/components/chat-message.md +122 -0
- package/docs/usage/components/chat-screen.md +112 -0
- package/docs/usage/components/checkbox-group.md +104 -0
- package/docs/usage/components/checkbox.md +103 -0
- package/docs/usage/components/chip.md +104 -0
- package/docs/usage/components/code-block.md +111 -0
- package/docs/usage/components/collapsible.md +112 -0
- package/docs/usage/components/color-picker.md +86 -0
- package/docs/usage/components/combobox.md +137 -0
- package/docs/usage/components/command-palette.md +125 -0
- package/docs/usage/components/comment-thread-screen.md +125 -0
- package/docs/usage/components/container.md +98 -0
- package/docs/usage/components/content-transition.md +101 -0
- package/docs/usage/components/context-menu.md +136 -0
- package/docs/usage/components/counter-badge.md +107 -0
- package/docs/usage/components/data-table.md +122 -0
- package/docs/usage/components/date-picker.md +142 -0
- package/docs/usage/components/date-range-picker.md +111 -0
- package/docs/usage/components/description-list.md +103 -0
- package/docs/usage/components/design-system-provider.md +124 -0
- package/docs/usage/components/dialog.md +176 -0
- package/docs/usage/components/divider.md +89 -0
- package/docs/usage/components/editor-screen.md +126 -0
- package/docs/usage/components/effect-surface.md +120 -0
- package/docs/usage/components/empty-state.md +114 -0
- package/docs/usage/components/field.md +129 -0
- package/docs/usage/components/file-picker.md +114 -0
- package/docs/usage/components/floating-action-button.md +138 -0
- package/docs/usage/components/form.md +162 -0
- package/docs/usage/components/grid.md +99 -0
- package/docs/usage/components/heading.md +87 -0
- package/docs/usage/components/icon-button.md +126 -0
- package/docs/usage/components/icon.md +105 -0
- package/docs/usage/components/image.md +122 -0
- package/docs/usage/components/keyboard-avoiding.md +93 -0
- package/docs/usage/components/keyboard-dock.md +110 -0
- package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
- package/docs/usage/components/keyboard-motion-provider.md +86 -0
- package/docs/usage/components/layout.md +117 -0
- package/docs/usage/components/link.md +121 -0
- package/docs/usage/components/list-detail-screen.md +103 -0
- package/docs/usage/components/list-row.md +124 -0
- package/docs/usage/components/list.md +119 -0
- package/docs/usage/components/load-more.md +115 -0
- package/docs/usage/components/masonry.md +109 -0
- package/docs/usage/components/media-selection-screen.md +119 -0
- package/docs/usage/components/mentions.md +119 -0
- package/docs/usage/components/menu.md +129 -0
- package/docs/usage/components/menubar.md +93 -0
- package/docs/usage/components/message-composer.md +124 -0
- package/docs/usage/components/moderation-screen.md +113 -0
- package/docs/usage/components/notice.md +106 -0
- package/docs/usage/components/notification-inbox-screen.md +97 -0
- package/docs/usage/components/notification-item.md +98 -0
- package/docs/usage/components/number-field.md +131 -0
- package/docs/usage/components/onboarding-screen.md +106 -0
- package/docs/usage/components/otp-field.md +101 -0
- package/docs/usage/components/pagination.md +82 -0
- package/docs/usage/components/password-field.md +137 -0
- package/docs/usage/components/permission-screen.md +107 -0
- package/docs/usage/components/photo-source-sheet.md +119 -0
- package/docs/usage/components/popover.md +108 -0
- package/docs/usage/components/profile-screen.md +89 -0
- package/docs/usage/components/progress.md +122 -0
- package/docs/usage/components/qr-code.md +122 -0
- package/docs/usage/components/radio-group.md +124 -0
- package/docs/usage/components/radio.md +104 -0
- package/docs/usage/components/result.md +116 -0
- package/docs/usage/components/saved-items-screen.md +126 -0
- package/docs/usage/components/screen-layout.md +119 -0
- package/docs/usage/components/search-field.md +120 -0
- package/docs/usage/components/search-screen.md +215 -0
- package/docs/usage/components/section.md +111 -0
- package/docs/usage/components/segmented-control.md +138 -0
- package/docs/usage/components/select.md +142 -0
- package/docs/usage/components/settings-screen.md +126 -0
- package/docs/usage/components/shared-transition-element.md +111 -0
- package/docs/usage/components/shared-transition-screen.md +86 -0
- package/docs/usage/components/sheet.md +151 -0
- package/docs/usage/components/side-panel.md +104 -0
- package/docs/usage/components/sidebar.md +107 -0
- package/docs/usage/components/skeleton.md +105 -0
- package/docs/usage/components/skip-nav.md +76 -0
- package/docs/usage/components/slider.md +121 -0
- package/docs/usage/components/sortable-collection.md +127 -0
- package/docs/usage/components/spinner.md +86 -0
- package/docs/usage/components/splitter.md +103 -0
- package/docs/usage/components/stack.md +93 -0
- package/docs/usage/components/statistic.md +123 -0
- package/docs/usage/components/steps.md +110 -0
- package/docs/usage/components/surface.md +91 -0
- package/docs/usage/components/swipe-actions.md +124 -0
- package/docs/usage/components/switch.md +120 -0
- package/docs/usage/components/tabs.md +134 -0
- package/docs/usage/components/tag.md +84 -0
- package/docs/usage/components/tags-input.md +111 -0
- package/docs/usage/components/text-area.md +112 -0
- package/docs/usage/components/text-format.md +75 -0
- package/docs/usage/components/text-transition.md +104 -0
- package/docs/usage/components/text.md +101 -0
- package/docs/usage/components/thinking-orb.md +105 -0
- package/docs/usage/components/timeline.md +105 -0
- package/docs/usage/components/toast.md +145 -0
- package/docs/usage/components/toggle-group.md +95 -0
- package/docs/usage/components/tooltip.md +103 -0
- package/docs/usage/components/top-bar.md +124 -0
- package/docs/usage/components/top.md +89 -0
- package/docs/usage/components/tour.md +118 -0
- package/docs/usage/components/transfer-list.md +115 -0
- package/docs/usage/components/tree.md +91 -0
- package/docs/usage/components/upload-item.md +99 -0
- package/docs/usage/components/virtual-list.md +105 -0
- package/docs/usage/components/visually-hidden.md +72 -0
- package/docs/usage/components/watermark.md +78 -0
- package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
- package/docs/usage/compositions/action-recovery-save.md +235 -0
- package/docs/usage/compositions/action-recovery-undo.md +193 -0
- package/docs/usage/compositions/common-message.md +132 -0
- package/docs/usage/compositions/common-notification.md +101 -0
- package/docs/usage/compositions/compound-controls.md +186 -0
- package/docs/usage/compositions/data-layouts.md +157 -0
- package/docs/usage/compositions/disclosure.md +144 -0
- package/docs/usage/compositions/environment-matrix.md +139 -0
- package/docs/usage/compositions/expo-interactions.md +149 -0
- package/docs/usage/compositions/family-drawer.md +201 -0
- package/docs/usage/compositions/floating-action-button.md +197 -0
- package/docs/usage/compositions/input-sheet.md +148 -0
- package/docs/usage/compositions/interaction-adapters.md +190 -0
- package/docs/usage/compositions/interaction-flow-apply.md +205 -0
- package/docs/usage/compositions/interaction-flow-draft.md +188 -0
- package/docs/usage/compositions/interaction-flow-search.md +171 -0
- package/docs/usage/compositions/native-renderers.md +106 -0
- package/docs/usage/compositions/navigation-bar-collection.md +164 -0
- package/docs/usage/compositions/optional-adapters.md +169 -0
- package/docs/usage/compositions/optional-motion.md +109 -0
- package/docs/usage/compositions/photo-source.md +104 -0
- package/docs/usage/compositions/purpose-input-comment.md +110 -0
- package/docs/usage/compositions/purpose-input-message.md +119 -0
- package/docs/usage/compositions/reference-first.md +96 -0
- package/docs/usage/compositions/reference-review.md +107 -0
- package/docs/usage/compositions/reference-settings.md +107 -0
- package/docs/usage/compositions/selection-scope.md +174 -0
- package/docs/usage/compositions/stea-event-ticket.md +166 -0
- package/docs/usage/compositions/stea-flip-card.md +162 -0
- package/docs/usage/compositions/stea-order-progress.md +184 -0
- package/docs/usage/compositions/stea-otp-verify.md +215 -0
- package/docs/usage/compositions/stea-pixel-empty.md +140 -0
- package/docs/usage/compositions/stea-schedule-card.md +169 -0
- package/docs/usage/compositions/stea-stat-summary.md +154 -0
- package/docs/usage/compositions/time-selection.md +174 -0
- package/docs/usage/compositions/toast-layout.md +128 -0
- package/docs/usage/compositions/visual-foundations.md +185 -0
- package/docs/usage/compositions/web-additions.md +146 -0
- package/docs/usage/compositions/web-navigation.md +143 -0
- package/docs/usage/screens/common-chat.md +127 -0
- package/docs/usage/screens/common-comments.md +108 -0
- package/docs/usage/screens/common-inbox.md +110 -0
- package/docs/usage/screens/common-login.md +98 -0
- package/docs/usage/screens/common-profile.md +221 -0
- package/docs/usage/screens/common-saved.md +127 -0
- package/docs/usage/screens/common-search.md +274 -0
- package/docs/usage/screens/common-settings.md +126 -0
- package/docs/usage/screens/common-shell.md +108 -0
- package/docs/usage/screens/dashboard.md +245 -0
- package/docs/usage/screens/discovery-gallery.md +306 -0
- package/docs/usage/screens/flow-collection.md +96 -0
- package/docs/usage/screens/flow-editor.md +120 -0
- package/docs/usage/screens/flow-media.md +111 -0
- package/docs/usage/screens/flow-moderation.md +120 -0
- package/docs/usage/screens/flow-onboarding.md +193 -0
- package/docs/usage/screens/flow-permission.md +103 -0
- package/docs/usage/screens/landing.md +347 -0
- package/docs/usage/screens/mockup-studio.md +190 -0
- package/docs/usage/screens/notification-settings.md +206 -0
- package/docs/usage/screens/reference-comparison.md +159 -0
- package/docs/usage/templates/component.md +61 -0
- package/docs/usage/templates/composition.md +47 -0
- package/docs/usage/templates/screen.md +56 -0
- package/docs/usage/templates/token.md +32 -0
- package/docs/usage/tokens/color.md +142 -0
- package/docs/usage/tokens/elevation-opacity.md +86 -0
- package/docs/usage/tokens/layers.md +98 -0
- package/docs/usage/tokens/layout.md +114 -0
- package/docs/usage/tokens/motion.md +88 -0
- package/docs/usage/tokens/radius.md +53 -0
- package/docs/usage/tokens/size.md +74 -0
- package/docs/usage/tokens/spacing.md +73 -0
- package/docs/usage/tokens/stroke.md +50 -0
- package/docs/usage/tokens/theme-studio.md +70 -0
- package/docs/usage/tokens/typography-studio.md +70 -0
- package/docs/usage/tokens/typography.md +89 -0
- package/package.json +7 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Tour
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Tour](../../tour.md), `src/tour.ts`(`tourRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/오버레이/사용 안내 둘러보기`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
새 화면·새 기능을 처음 만난 사용자에게 화면의 여러 요소를 순서대로 짚어 설명할 때 쓴다.
|
|
14
|
+
카드가 대상 요소 옆에 붙고, 배경은 가려지며, 다음·이전·건너뛰기·완료로 진행한다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 요소 하나에 대한 짧은 설명 | [Tooltip](tooltip.md), [Popover](popover.md) |
|
|
21
|
+
| 사용자가 직접 진행하는 다단계 입력 흐름 | [Steps](steps.md) |
|
|
22
|
+
| 한 번에 하나의 내용을 띄움 | [Dialog](dialog.md), [Sheet](sheet.md) |
|
|
23
|
+
|
|
24
|
+
## 공개 이름과 import
|
|
25
|
+
|
|
26
|
+
| 이름 | 역할 | Web | Native |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `Tour` | 기본 | `@hjmds/react`, `/tour` | — |
|
|
29
|
+
|
|
30
|
+
descriptor 타입은 `@hjmds/design-contracts/components/tour`에 있다.
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { Tour } from "@hjmds/react/tour";
|
|
37
|
+
|
|
38
|
+
<Tour
|
|
39
|
+
descriptor={{
|
|
40
|
+
accessibilityLabel: t("tour.home.label"),
|
|
41
|
+
currentStepId: stepId,
|
|
42
|
+
steps: [
|
|
43
|
+
{ id: "search", anchorId: "home-search", title: t("tour.search.title"), description: t("tour.search.body") },
|
|
44
|
+
{ id: "new", anchorId: "home-new", title: t("tour.new.title"), description: t("tour.new.body"), placement: "top" },
|
|
45
|
+
],
|
|
46
|
+
labels: { next: t("tour.next"), previous: t("tour.previous"), skip: t("tour.skip"), done: t("tour.done") },
|
|
47
|
+
}}
|
|
48
|
+
resolveAnchor={(id) => document.querySelector<HTMLElement>(`[data-tour="${id}"]`)}
|
|
49
|
+
composeAnnouncement={({ position, total, title, description }) =>
|
|
50
|
+
t("tour.announce", { position, total, title, description })}
|
|
51
|
+
onStepChange={(id) => setStepId(id)}
|
|
52
|
+
open={open}
|
|
53
|
+
onOpenChange={(next, { reason }) => { setOpen(next); if (!next) saveTourSeen(reason); }}
|
|
54
|
+
/>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Native: 없음. Native에서 Tour를 흉내 내 조립하지 않는다.
|
|
58
|
+
|
|
59
|
+
## 축과 기본값
|
|
60
|
+
|
|
61
|
+
| prop | 값 | 기본값 | 설명 |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| `descriptor` | `{ accessibilityLabel, currentStepId, steps: readonly { id, anchorId, title, description, placement?, align? }[], labels: { next, previous, skip, done } }` | — (필수) | 문자열은 모두 비어 있으면 안 된다 |
|
|
64
|
+
| `descriptor.currentStepId` | step id | — | 진행 위치는 이것 하나다. 다음·이전을 누르면 `onStepChange`만 호출되므로 제품이 상태를 갱신해야 카드가 움직인다 |
|
|
65
|
+
| `onStepChange` | `(stepId: Id, reason: "next" \| "previous") => void` | — (필수) | 이동할 step id |
|
|
66
|
+
| `resolveAnchor` | `(anchorId: string) => HTMLElement \| null` | — (필수) | 대상 요소를 찾는다 |
|
|
67
|
+
| `composeAnnouncement` | `(info: { position: number, total: number, title: string, description: string }) => string` | — (필수) | 보조기술이 읽는 단계 문장 |
|
|
68
|
+
| `open` + `onOpenChange` | 제어(둘 다 필수) | — | `defaultOpen`과 섞으면 예외 |
|
|
69
|
+
| `defaultOpen` | `boolean` | `false` | 비제어 |
|
|
70
|
+
| `onOpenChange` | `(open: boolean, detail: { reason }) => void` | — | `reason`은 열림 `trigger`, 닫힘 `skip` · `escape` · `complete` · `programmatic` · `interrupted`. 바깥 클릭으로는 닫히지 않는다 |
|
|
71
|
+
| step `placement` | `top` · `bottom` · `start` · `end` | 자동 배치 | — |
|
|
72
|
+
| step `align` | `start` · `center` · `end` | 자동 배치 | — |
|
|
73
|
+
| `trigger` | `ReactElement` 하나 | — | 그 요소가 여는 버튼이 되고, 닫힐 때 포커스가 그리로 돌아간다 |
|
|
74
|
+
| `portalContainer` | `HTMLElement` | `document.body` | — |
|
|
75
|
+
|
|
76
|
+
`layoutStyle`은 없다. 화면 흐름 밖 오버레이라 배치할 루트가 없다(`className`만 카드에 붙는다).
|
|
77
|
+
|
|
78
|
+
## 배치
|
|
79
|
+
|
|
80
|
+
| 항목 | 값 | 근거 |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| 크기 | 카드 최대 폭 320, 안쪽 여백 `spacing.md` 16, 모서리 `radius.md` 12. 뷰포트 높이를 넘으면 카드 안에서 세로 스크롤. 버튼은 Button `medium` 44 | `tourRecipe.maxWidth`, `.hjm-tour` |
|
|
83
|
+
| 간격 | 카드–대상 요소 `spacing.xs` 8. 카드 안 블록 사이 `spacing.sm` 12, 제목 위 `spacing.xxs` 4, 설명 위 `spacing.xs` 8, 행동 사이 `spacing.xs` 8 | `tourRecipe.sideOffset`, `.hjm-tour__*` |
|
|
84
|
+
| 순서·정렬 | 카드 안: 진행 수(`1 / 3`) → 제목 → 설명 → 행동 줄. 행동 줄은 끝 정렬, **[건너뛰기 ghost] [이전 secondary] [다음·완료 primary]** | `react/src/tour.tsx`, `.hjm-tour__actions` |
|
|
85
|
+
| 고정·스크롤 | 화면 전체를 덮는 오버레이. 배경막(`backdrop.modal`)이 뷰포트 전체에 깔리고 대상 요소 둘레에 강조 테두리(focus 색 2px, `radius.md` 12). 카드는 portal로 떠서 모달 층 바로 위. 대상 요소는 스크롤해서 화면 안에 보이게 두고, 고정 바 아래 가려진 요소를 대상으로 삼지 않는다 | `.hjm-tour-backdrop`, `.hjm-tour-highlight`, `getModalLayer(0) + 1` |
|
|
86
|
+
| 좁은 폭·큰 글자 | 카드가 `placement` 쪽에 자리가 모자라면 반대 축으로 옮겨진다. 행동 줄은 좁으면 줄바꿈 | `useAnchoredPopup`(`fallbackAxis`), `.hjm-tour__actions`(flex-wrap) |
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
┌──────────── 배경막(전체) ─────────────┐
|
|
90
|
+
│ ┌─────────┐ ← 대상 강조 테두리 │
|
|
91
|
+
│ │ 검색 │ │
|
|
92
|
+
│ └─────────┘ │
|
|
93
|
+
│ ↕ spacing.xs 8 │
|
|
94
|
+
│ ┌──────────────────────────┐ ≤ 320 │
|
|
95
|
+
│ │ 1 / 3 │ │
|
|
96
|
+
│ │ 제목 │ │
|
|
97
|
+
│ │ 설명 │ │
|
|
98
|
+
│ │ [건너뛰기] [이전] [다음] │ │
|
|
99
|
+
│ └──────────────────────────┘ │
|
|
100
|
+
└────────────────────────────────────────┘
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## 꼭 지킬 것
|
|
105
|
+
|
|
106
|
+
- `anchorId`는 제품이 소유한 불투명 키다. ref·좌표를 넘기지 않고 `resolveAnchor`가 요소를 찾는다.
|
|
107
|
+
같은 요소를 두 step에서 설명해도 된다(id만 유일하면 된다).
|
|
108
|
+
- `composeAnnouncement`는 위치·제목·설명을 담은 문장을 i18n으로 조립해 반환한다. 빈 문자열이면 예외다.
|
|
109
|
+
화면의 카드 문구는 보조기술에서 숨겨지고 이 문장만 읽힌다.
|
|
110
|
+
- step `title`·`description`, `labels`의 네 값은 모두 비어 있으면 안 된다.
|
|
111
|
+
- 다시 보지 않기 같은 "본 적 있음" 저장은 제품 몫이다. `onOpenChange`의 `reason`으로 판단한다.
|
|
112
|
+
- 첫 step의 이전 버튼은 `aria-disabled`다. 포커스는 받고(카드 안 탭 순서 유지) 눌러도 아무 일도 없다.
|
|
113
|
+
제품이 첫 step에서 이전 버튼을 숨기거나 `disabled`로 바꾸지 않는다.
|
|
114
|
+
|
|
115
|
+
## 함정
|
|
116
|
+
|
|
117
|
+
- 열린 채로 언마운트되면 `onOpenChange(false, { reason: "interrupted" })`가 한 번 온다. 라우트 이동을
|
|
118
|
+
완료로 기록하지 않도록 사유를 구분한다.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# TransferList
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [TransferList](../../transfer-list.md), `src/transfer-list.ts`(`transferListRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/목록 간 항목 이동`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
한 항목 집합을 두 목록으로 나누고 사용자가 항목을 오가게 할 때 쓴다. 후보 ↔ 확정 명단,
|
|
14
|
+
권한 없음 ↔ 권한 있음 같은 화면이다. 값은 오른쪽(target) 패널에 들어간 id 집합 하나다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 목록에서 여러 개를 체크만 함 | [CheckboxGroup](checkbox-group.md) |
|
|
21
|
+
| 드롭다운에서 하나 고름 | [Select](select.md), 검색이 필요하면 [Combobox](combobox.md) |
|
|
22
|
+
| 고른 값을 태그로 쌓음 | [TagsInput](tags-input.md) |
|
|
23
|
+
| 순서 바꾸기 | [SortableCollection](sortable-collection.md) |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `TransferList` | 기본 | `@hjmds/react`, `/transfer-list` | `@hjmds/react-native`, `/transfer-list` |
|
|
30
|
+
|
|
31
|
+
## 최소 사용 예
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Web
|
|
35
|
+
import { TransferList } from "@hjmds/react/transfer-list";
|
|
36
|
+
|
|
37
|
+
// 상태 → i18n 키 상수 표. 키를 템플릿 문자열로 만들지 않는다.
|
|
38
|
+
const movedKey = { toTarget: "members.moved.toTarget", toSource: "members.moved.toSource" } as const;
|
|
39
|
+
|
|
40
|
+
const labels = {
|
|
41
|
+
source: t("members.candidates"),
|
|
42
|
+
target: t("members.confirmed"),
|
|
43
|
+
toTarget: t("members.add"),
|
|
44
|
+
toSource: t("members.remove"),
|
|
45
|
+
selectAll: t("members.selectAll"),
|
|
46
|
+
empty: t("members.empty"),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
<TransferList
|
|
50
|
+
items={people.map((p) => ({ id: p.id, label: p.name, textValue: p.name }))}
|
|
51
|
+
labels={labels}
|
|
52
|
+
targetKeys={confirmed}
|
|
53
|
+
onTargetKeysChange={setConfirmed}
|
|
54
|
+
onMove={(ids, direction) => announce(t(movedKey[direction], { count: ids.length }))}
|
|
55
|
+
/>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// Native
|
|
60
|
+
import { TransferList } from "@hjmds/react-native/transfer-list";
|
|
61
|
+
|
|
62
|
+
<TransferList items={items} labels={labels} targetKeys={confirmed} onTargetKeysChange={setConfirmed} />
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 축과 기본값
|
|
66
|
+
|
|
67
|
+
| prop | 값 | 기본값 | 설명 |
|
|
68
|
+
| --- | --- | --- | --- |
|
|
69
|
+
| `targetKeys` + `onTargetKeysChange` | `ReadonlySet<Id>`, `(keys: ReadonlySet<Id>) => void` | — | 제어. 콜백은 다음 target 집합 전체를 받는다. source 패널은 `items` 중 target이 아닌 나머지이고, 두 패널 모두 `items` 순서를 유지한다 |
|
|
70
|
+
| `defaultTargetKeys` | `ReadonlySet<Id>` | 빈 집합 | 비제어 |
|
|
71
|
+
| `onMove` | `(movedIds: readonly Id[], direction: "toTarget" \| "toSource") => void` | — | 이동 직후 한 번. 낭독 문장을 만든다 |
|
|
72
|
+
| `labels` | `{ source, target, toTarget, toSource, selectAll, empty }`(모두 `string`) | — (필수) | — |
|
|
73
|
+
| `items` | `{ id, label, textValue, description?, disabled? }[]` | — | `disabled` 항목은 선택도 이동도 되지 않는다 |
|
|
74
|
+
| 패널별 체크 선택 | 내부 상태 | — | 이동 전 임시 상태라 prop으로 받지 않는다. 이동 버튼은 해당 패널에 선택이 있을 때만 활성화된다 |
|
|
75
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | — | 최상위 컨테이너 배치. Web·Native 모두 |
|
|
76
|
+
| `style`(Native) | `StyleProp<ViewStyle>` | — | deprecated — `layoutStyle`. 개발 모드에서 한 번 경고하고 다음 major에서 제거된다 |
|
|
77
|
+
|
|
78
|
+
## 배치
|
|
79
|
+
|
|
80
|
+
| 항목 | 값 | 근거 |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| 크기 | 페이지 폭 전체. 패널 머리(Web)·전체 선택 행(Native)·항목 행 최소 높이 `control.minTouchTarget` 44. Web 패널 테두리 1px, `radius.md` 12. 이동 버튼은 Button `secondary` `medium`(44). 패널 높이 상한은 HJM이 정하지 않는다 | `transferListRecipe.panelHeader`, `.hjm-transfer-list__*`, `react-native/src/transfer-list.tsx` |
|
|
83
|
+
| 간격 | Web 열 간격 `spacing.md` 16, 이동 버튼 열은 위에서 `spacing.xl` 24 내려와 버튼 사이 `moveControls.gap` `spacing.sm` 12. 머리 좌우 여백 `spacing.sm` 12, 목록 안쪽 여백 `spacing.xxs` 4, 행 좌우 여백·요소 사이 `collectionItemContract` `spacing.sm` 12(모서리 `radius.md`). Native 블록 사이·버튼 사이 `spacing.sm` 12 | `.hjm-transfer-list`, `.hjm-transfer-list__actions`, Native `gap: spacing.sm` |
|
|
84
|
+
| 순서·정렬 | Web 넓은 폭: `[source 패널] [이동 버튼 열] [target 패널]`. Native: source 라벨 → source 패널 → 이동 버튼 줄(가운데 정렬) → target 라벨 → target 패널. 주 행동(저장)은 컴포넌트 밖 폼 하단 | `react/src/transfer-list.tsx`, `react-native/src/transfer-list.tsx` |
|
|
85
|
+
| 고정·스크롤 | 화면 본문의 폼 영역에 두고 화면과 함께 스크롤한다 | — |
|
|
86
|
+
| 좁은 폭·큰 글자 | Web은 폭 30rem(480) 이하에서 한 열로 쌓여 source → 버튼 → target. Native는 항상 세로 | `@media (max-width: 30rem)` |
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
Web ≥ 480 Web < 480 · Native
|
|
90
|
+
┌──────────┐ ┌──────────┐ ┌──────────────┐
|
|
91
|
+
│ 후보 ☐ │ [추가 →] │ 확정 ☐ │ │ 후보 패널 │
|
|
92
|
+
├──────────┤ [← 빼기] ├──────────┤ └──────────────┘
|
|
93
|
+
│ ☐ 항목 │ │ ☐ 항목 │ [추가] [빼기]
|
|
94
|
+
│ ☐ 항목 │ │ │ ┌──────────────┐
|
|
95
|
+
└──────────┘ └──────────┘ │ 확정 패널 │
|
|
96
|
+
1fr ← 16 → auto ← 16 → 1fr └──────────────┘
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## 꼭 지킬 것
|
|
101
|
+
|
|
102
|
+
- `labels`의 여섯 문구는 모두 제품 i18n으로 넣는다. HJM은 문장을 만들지 않는다.
|
|
103
|
+
- 이동 결과 낭독은 제품 몫이다. `onMove(movedIds, direction)`로 "2명 이동" 같은 문장을 만들어 알린다.
|
|
104
|
+
- 배치는 `layoutStyle`(최상위 컨테이너)로 한다. Web `className`·Native의 deprecated `style`로 패널·행 모양을 덮지 않는다.
|
|
105
|
+
|
|
106
|
+
## 플랫폼 차이
|
|
107
|
+
|
|
108
|
+
| 항목 | Web | Native |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| 배치 | 두 패널 좌우 + 가운데 버튼 | 세로로 쌓음(source → 버튼 → target) |
|
|
111
|
+
| 행 의미 | `listbox`/`option`(`aria-selected`) | `checkbox` 행 |
|
|
112
|
+
| 키보드 | 화살표·Home/End 이동, Space 선택, Enter로 그 행만 이동 | 없음 |
|
|
113
|
+
| 이동 후 포커스 | 빈 자리로 올라온 행, 비면 빈 상태 문구 | 옮기지 않음 |
|
|
114
|
+
| 패널 비었을 때 전체 선택 | 활성 | `disabled` |
|
|
115
|
+
| 외부 꾸밈 | `className`, `ref`, `layoutStyle` | `layoutStyle`(`style`은 deprecated) |
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Tree
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Tree](../../tree.md), 체크 집계 [TreeSelect](../../tree-select.md), `src/tree.ts`(`treeRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/데이터 표시/트리 목록`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
깊이가 정해지지 않은 계층 데이터를 펼치고 접으며 탐색하고, 그 안에서 하나 또는 여럿을 고를 때
|
|
14
|
+
쓴다. 폴더 구조, 조직도, 카테고리 트리가 여기에 속한다. 노드마다 체크(부모 집계 포함)도 할 수 있다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 단계가 고정된 2단(구역 → 항목) 목록 | [List](list.md), [Menu](menu.md) |
|
|
21
|
+
| 접고 펴는 내용 구역 | [Accordion](accordion.md), [Collapsible](collapsible.md) |
|
|
22
|
+
| 평평한 목록의 다중 체크 | [CheckboxGroup](checkbox-group.md) |
|
|
23
|
+
| 두 목록 사이 이동 | [TransferList](transfer-list.md) |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `Tree` | 기본 | `@hjmds/react`, `/tree` | — |
|
|
30
|
+
|
|
31
|
+
노드 타입은 `@hjmds/design-contracts/components/tree`, 체크 helper(`resolveTreeCheckedStates`,
|
|
32
|
+
`toggleTreeCheckedSelection`)는 `@hjmds/design-contracts/components/tree-select`에 있다.
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { Tree } from "@hjmds/react/tree";
|
|
39
|
+
|
|
40
|
+
<Tree
|
|
41
|
+
label={t("files.tree")}
|
|
42
|
+
nodes={[{ id: "docs", label: t("files.docs"), textValue: "docs", children: [
|
|
43
|
+
{ id: "readme", label: "README", textValue: "README" },
|
|
44
|
+
] }]}
|
|
45
|
+
composeAccessibleName={({ depth, position, siblingCount, label, hasChildren, expanded }) =>
|
|
46
|
+
t("files.node", { depth, position, siblingCount, label, state: hasChildren ? (expanded ? "open" : "closed") : "leaf" })}
|
|
47
|
+
expandedKeys={expanded}
|
|
48
|
+
onExpandedKeysChange={setExpanded}
|
|
49
|
+
selection={{ mode: "single", selectedKey: selected, onSelectionChange: setSelected }}
|
|
50
|
+
/>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Native: 없음.
|
|
54
|
+
|
|
55
|
+
## 축과 기본값
|
|
56
|
+
|
|
57
|
+
| prop | 값 | 기본값 | 설명 |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| `nodes` | `readonly { id, label, textValue, description?, disabled?, children? }[]` | — (필수) | `children`은 없거나 하나 이상 |
|
|
60
|
+
| `composeAccessibleName` | `(info: { depth, position, siblingCount, label, hasChildren, expanded }) => string` | — (필수) | `depth`·`position`은 1부터 |
|
|
61
|
+
| `expandedKeys` + `onExpandedKeysChange` | `ReadonlySet<Id>`, `(keys: ReadonlySet<Id>) => void` | — | 제어 펼침. 사라졌거나 자식이 없는 노드의 펼침 키는 버려진다 |
|
|
62
|
+
| `defaultExpandedKeys` | `ReadonlySet<Id>` | 빈 집합 | 비제어 펼침 |
|
|
63
|
+
| `selection` | `{ mode: "none" }` · `{ mode: "single", selectedKey \| defaultSelectedKey, onSelectionChange?: (key: Id \| null) => void, disallowEmptySelection? }` · `{ mode: "multiple", selectedKeys \| defaultSelectedKeys, onSelectionChange?: (keys: ReadonlySet<Id>) => void }` | 생략하면 선택 없음 | `selectedKey(s)`를 주면 제어(이때 `onSelectionChange` 필수), `defaultSelectedKey(s)`만 주면 비제어로 내부 상태에 보관된다. `single`에서 `disallowEmptySelection`을 주면 같은 노드를 다시 눌러도 해제되지 않는다 |
|
|
64
|
+
| `checkedStates` + `onCheckedToggle` | `ReadonlyMap<Id, true \| false \| "mixed">`, `(id: Id) => void` | — | 함께 주면 행 클릭·Enter·Space가 선택 대신 체크. 집계는 `resolveTreeCheckedStates`, 토글은 `toggleTreeCheckedSelection` |
|
|
65
|
+
| `asyncState` | `{ status: "idle" }` 또는 `{ status: "loading" \| "loadingMore" \| "empty" \| "error", message: string }` | `{ status: "idle" }` | `idle` 밖의 상태는 `message` 필수 |
|
|
66
|
+
| `renderToggle` | `(state: { expanded: boolean }) => ReactNode` | `▸`/`▾` | 펼침 표시 글리프를 제품이 그린다(장식) |
|
|
67
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | — | 루트 배치 |
|
|
68
|
+
|
|
69
|
+
## 배치
|
|
70
|
+
|
|
71
|
+
| 항목 | 값 | 근거 |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| 크기 | 행 최소 높이 `control.minTouchTarget` 44 | `treeRecipe.node`(`collectionItemContract`), `.hjm-tree__node` |
|
|
74
|
+
| 간격 | 들여쓰기는 깊이 1단계마다 `spacing.lg` 20씩 늘어난다(최상위 0) | `treeRecipe.indentPerLevel`, `.hjm-tree__indent` |
|
|
75
|
+
| 순서·정렬 | 행은 위→아래 한 열. 행 안 순서는 들여쓰기 → 펼침 표시 → (체크) → 라벨·설명 | `react/src/tree.tsx` |
|
|
76
|
+
| 고정·스크롤 | 넓은 Web 화면의 옆 열(파일 트리·카테고리 탐색)이나 본문 패널에 둔다. 트리 자체는 높이 상한·스크롤이 없어 담는 열·패널이 세로 스크롤을 맡는다. 폭을 사용자가 바꾸게 하려면 [Splitter](splitter.md) 한쪽에 넣는다 | `.hjm-tree` |
|
|
77
|
+
| 좁은 폭·큰 글자 | 깊은 트리는 좁은 폭에서 라벨 폭이 줄어 줄바꿈된다. 깊이가 깊고 폭이 좁은 화면이면 단계별 목록 이동을 검토한다 | `.hjm-tree__label`(overflow-wrap) |
|
|
78
|
+
|
|
79
|
+
## 꼭 지킬 것
|
|
80
|
+
|
|
81
|
+
- `label`과 `composeAccessibleName`은 필수다. 깊이·위치·펼침을 읽는 순서는 제품 i18n이 정한다.
|
|
82
|
+
- 노드의 `textValue`는 필수이고 typeahead 대상이다. `children`은 없거나 하나 이상이다(빈 배열 금지).
|
|
83
|
+
id는 트리 전체에서 유일해야 한다.
|
|
84
|
+
- 행 안에 버튼·링크를 넣지 않는다. 노드 하나가 유일한 포커스 대상이다(roving tab stop).
|
|
85
|
+
- 배치는 `layoutStyle`로 한다. `className`으로 들여쓰기·행 모양을 덮지 않는다(recipe 소유).
|
|
86
|
+
|
|
87
|
+
## 함정
|
|
88
|
+
|
|
89
|
+
- 비제어 선택(`defaultSelectedKey(s)`)은 첫 렌더의 값만 쓴다. 나중에 default 값을 바꿔도 표시가 따라가지 않는다.
|
|
90
|
+
외부에서 선택을 바꿔야 하면 `selectedKey(s)` 제어형으로 쓴다(제어형은 `null`도 제어 값이다).
|
|
91
|
+
- `checkedStates`만 주고 `onCheckedToggle`을 빼면 체크 표시는 보이지만 클릭은 선택·펼침으로 동작한다.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# UploadItem
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [UploadItem](../../upload-item.md), `src/upload-item.ts`(`uploadItemRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/데이터 표시/업로드 항목`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
사용자가 고른 파일 **한 개**의 업로드 상태(대기·전송 중·완료·실패)를 한 행으로 보여 줄 때 쓴다.
|
|
14
|
+
전송 중에는 취소, 실패하면 재시도 버튼이 상태에서 자동으로 정해져 나온다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 파일을 고르는 입력 | [FilePicker](file-picker.md) |
|
|
21
|
+
| 파일과 무관한 작업 진행률 | [Progress](progress.md) |
|
|
22
|
+
| 업로드가 아닌 일반 목록 행 | [ListRow](list-row.md) |
|
|
23
|
+
|
|
24
|
+
## 공개 이름과 import
|
|
25
|
+
|
|
26
|
+
| 이름 | 역할 | Web | Native |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `UploadItem` | 기본 | `@hjmds/react`, `/display`, `/upload-item` | `@hjmds/react-native`, `/upload-item` |
|
|
29
|
+
|
|
30
|
+
descriptor 타입과 `validateUploadItemList`는 `@hjmds/design-contracts/components/upload-item`에 있다.
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { UploadItem } from "@hjmds/react/upload-item";
|
|
37
|
+
|
|
38
|
+
<UploadItem
|
|
39
|
+
descriptor={{ id: file.id, name: file.name, sizeLabel: file.sizeLabel, state: file.state }}
|
|
40
|
+
labels={{
|
|
41
|
+
pending: t("upload.pending"),
|
|
42
|
+
uploading: t("upload.uploading"),
|
|
43
|
+
success: t("upload.success"),
|
|
44
|
+
cancel: t("upload.cancel"),
|
|
45
|
+
retry: t("upload.retry"),
|
|
46
|
+
}}
|
|
47
|
+
onCancel={cancelUpload}
|
|
48
|
+
onRetry={retryUpload}
|
|
49
|
+
/>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// Native
|
|
54
|
+
import { UploadItem } from "@hjmds/react-native/upload-item";
|
|
55
|
+
|
|
56
|
+
<UploadItem descriptor={descriptor} labels={labels} onCancel={cancelUpload} onRetry={retryUpload} />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 축과 기본값
|
|
60
|
+
|
|
61
|
+
| prop | 값 | 기본값 | 설명 |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| `descriptor` | `{ id, name, sizeLabel?, state }` | — (필수) | — |
|
|
64
|
+
| `state` | `{ status: "pending" }` · `{ status: "uploading", progress: number \| null, progressLabel? }` · `{ status: "success" }` · `{ status: "error", message: string }` | — | 아래 행 참고 |
|
|
65
|
+
| `state.status` | `pending` · `uploading` · `success` · `error` | — | discriminated union. 취소는 `uploading`일 때만, 재시도는 `error`일 때만 나온다. 별도 boolean prop은 없다 |
|
|
66
|
+
| `state.progress` | 0~1 비율 · `null` | — | `uploading`에서. 측정 불가면 `null` |
|
|
67
|
+
| `state.progressLabel` | 문자열 | — | 있으면 그 문장을 낭독. 없으면 반올림 퍼센트, `progress`가 `null`이면 `labels.uploading` |
|
|
68
|
+
| `state.message` | 문자열 | — | `error`에서 필수. 문제와 다음 행동을 함께 적는다 |
|
|
69
|
+
| `labels` | `{ pending, uploading, success, cancel, retry }`(모두 `string`) | — (필수) | — |
|
|
70
|
+
| `onCancel` / `onRetry` | `(id: string) => void` | — | descriptor `id`를 받는다. `uploading`·`error` 상태에서는 각각 필수 |
|
|
71
|
+
| `leading` | `ReactNode` | — | 아이콘·썸네일(장식) |
|
|
72
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | — | 행 바깥 배치. Web·Native 모두 |
|
|
73
|
+
| `style`(Native) | `StyleProp<ViewStyle>` | — | deprecated — `layoutStyle`. 개발 모드에서 한 번 경고하고 다음 major에서 제거된다 |
|
|
74
|
+
|
|
75
|
+
## 배치
|
|
76
|
+
|
|
77
|
+
| 항목 | 값 | 근거 |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| 크기 | 최소 높이 `layout.rowHeight.twoLine` 68, 위아래 여백 `row.paddingVertical` `spacing.xs` 8(두 플랫폼). 행동 버튼 최소 44×44. 테두리 1px, `radius.md` 12인 카드 모양 | `uploadItemRecipe.row`, `.hjm-upload-item`, `react-native/src/upload-item.tsx` |
|
|
80
|
+
| 간격 | 요소 사이 `spacing.sm` 12, 좌우 여백 `spacing.md` 16. 행 사이 간격·목록 틀은 제품이 정한다(HJM 목록 레이아웃 없음) | `uploadItemRecipe.row` |
|
|
81
|
+
| 순서·정렬 | `leading`(아이콘·썸네일) → 이름·메타·진행/상태 → 끝의 행동 버튼(취소·재시도). 여러 개면 고른 순서대로 세로로 쌓는다 | `react/src/upload-item.tsx`, `.hjm-upload-item__action`(margin-inline-start: auto) |
|
|
82
|
+
| 고정·스크롤 | 파일을 고르는 [FilePicker](file-picker.md) 바로 아래, 본문과 함께 스크롤 | — |
|
|
83
|
+
| 좁은 폭·큰 글자 | Web은 본문 블록이 14rem 아래로 줄면 행동 버튼이 다음 줄 끝으로 내려간다. Native는 한 줄을 유지하고 이름이 줄바꿈된다 | `.hjm-upload-item__body`(flex: 1 1 14rem), `.hjm-upload-item`(flex-wrap) |
|
|
84
|
+
|
|
85
|
+
## 꼭 지킬 것
|
|
86
|
+
|
|
87
|
+
- `progress`에 100을 곱해 넘기지 않는다. renderer가 내부 Progress에 `value={progress * 100}`으로 바꾼다.
|
|
88
|
+
- `uploading` 상태에서 `onCancel`이, `error` 상태에서 `onRetry`가 없으면 렌더 중 `TypeError`가 난다.
|
|
89
|
+
- 바이트 포맷(`sizeLabel`)·상태 문구·`message`·업로드 요청·재시도 로직은 제품 소유다. 행 모양·상태 색·액션 결정은 HJM 소유다.
|
|
90
|
+
- 목록에서는 `validateUploadItemList`로 id 중복을 막는다. 목록 레이아웃·일괄 재시도는 제품이 조합한다.
|
|
91
|
+
- 빈 문자열 문구·이름·id는 `TypeError`다.
|
|
92
|
+
|
|
93
|
+
## 플랫폼 차이
|
|
94
|
+
|
|
95
|
+
| 항목 | Web | Native |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| 루트 | `role="group"`, `aria-label`=파일명, `HTMLAttributes`·`ref`·`layoutStyle` 전달 | 일반 `View`, `layoutStyle`(`style`은 deprecated) |
|
|
98
|
+
| 상태 낭독 | 상태 문장 live region | 파일 정보 묶음이 한 요소(`busy` state, value=상태 문장), 액션은 별도 버튼 |
|
|
99
|
+
| `leading` | `aria-hidden` | 접근성 트리에서 숨김 |
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# VirtualList
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [VirtualList](../../virtual-list.md), 계산 `resolveVirtualWindow`(`src/virtual-list.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/데이터 표시/가상 목록`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
행 높이가 **모두 같은** 긴 목록(수백~수천 행)을 정해진 높이 안에서 스크롤할 때 쓴다.
|
|
14
|
+
Web은 보이는 범위와 앞뒤 overscan만 마운트하고, Native는 `FlatList`에 위임한다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 행 높이가 내용·글자 크기에 따라 달라짐, 짧은 목록 | [List](list.md) |
|
|
21
|
+
| 다음 페이지를 네트워크에서 가져옴 | [LoadMore](load-more.md)를 목록 아래에 조합 |
|
|
22
|
+
| 높이가 다른 카드 격자 | [Masonry](masonry.md) |
|
|
23
|
+
| 열·정렬이 있는 표 | [DataTable](data-table.md) |
|
|
24
|
+
| 비어 있을 때의 안내 화면 | `empty` 슬롯에 [EmptyState](empty-state.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `VirtualList` | 기본 | `/virtual-list`만(root 없음) | `/virtual-list`만(root 없음) |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { VirtualList } from "@hjmds/react/virtual-list";
|
|
37
|
+
|
|
38
|
+
<VirtualList
|
|
39
|
+
items={contacts}
|
|
40
|
+
keyExtractor={(contact) => contact.id}
|
|
41
|
+
renderItem={(contact) => <ContactRow contact={contact} />}
|
|
42
|
+
rowHeight={56}
|
|
43
|
+
height={480}
|
|
44
|
+
label={t("contacts.listLabel")}
|
|
45
|
+
empty={<EmptyContacts />}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// Native
|
|
51
|
+
import { VirtualList } from "@hjmds/react-native/virtual-list";
|
|
52
|
+
|
|
53
|
+
<VirtualList
|
|
54
|
+
items={contacts}
|
|
55
|
+
keyExtractor={(contact) => contact.id}
|
|
56
|
+
renderItem={(contact) => <ContactRow contact={contact} />}
|
|
57
|
+
rowHeight={64}
|
|
58
|
+
height={windowHeight - headerHeight}
|
|
59
|
+
label={t("contacts.listLabel")}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 축과 기본값
|
|
64
|
+
|
|
65
|
+
| prop | 값 | 기본값 | 설명 |
|
|
66
|
+
| --- | --- | --- | --- |
|
|
67
|
+
| `items` | `readonly T[]` | — | 필수 |
|
|
68
|
+
| `keyExtractor` | `(item: T) => string` | — | 필수. 데이터의 stable id |
|
|
69
|
+
| `renderItem` | `(item: T, index: number) => ReactNode` | — | 필수. 행 하나를 그린다 |
|
|
70
|
+
| `rowHeight` | 0보다 큰 유한수 | — | 필수. 모든 행의 높이 |
|
|
71
|
+
| `height` | 0보다 큰 유한수 | — | 필수. 목록 창 높이 |
|
|
72
|
+
| `label` | 문자열 | — | 필수 |
|
|
73
|
+
| `empty` | `ReactNode` | — | 선택. `items`가 비었을 때 |
|
|
74
|
+
| `overscan` | 0 이상 정수 | 3 | 선택 |
|
|
75
|
+
| `layoutStyle`(Web) | `HjmCompositionStyleProp` | — | 목록 창 바깥 배치. Native에는 없다 |
|
|
76
|
+
|
|
77
|
+
두 renderer의 Props는 Web의 `layoutStyle` 하나만 다르다. `style`·`className`은 없다.
|
|
78
|
+
|
|
79
|
+
## 배치
|
|
80
|
+
|
|
81
|
+
| 항목 | 값 | 근거 |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| 크기 | 높이는 `height`로 고정(Native는 늘거나 줄지 않음), 폭은 담는 영역 전체. 행 높이는 모두 `rowHeight`. 한 줄 행이면 `layout.rowHeight.singleLine` 56, 두 줄이면 `twoLine` 68이 기준 | `react/src/virtual-list.tsx`, `react-native/src/virtual-list.tsx`, `foundations.ts` `layout.rowHeight` |
|
|
84
|
+
| 간격 | 행 안 여백·구분선·터치 영역(최소 `control.minTouchTarget` 44)은 `renderItem`이 그리는 행 컴포넌트가 정한다 | — |
|
|
85
|
+
| 순서·정렬 | `items` 순서대로 위→아래 | `virtualListRecipe.readingOrder` |
|
|
86
|
+
| 고정·스크롤 | 그 자체가 **세로 스크롤 영역**이다. 다른 세로 스크롤 영역(ScrollView, 스크롤되는 페이지 본문) 안에 넣지 말고, 고정 머리·하단 바 사이의 남은 높이를 측정해 `height`로 넘긴다 | Web `overflowY: auto`, Native `FlatList` |
|
|
87
|
+
| 좁은 폭·큰 글자 | 큰 글자에서 행 내용이 늘어나지 않으므로 글자 배율에 맞춰 `rowHeight`를 다시 계산해 넘긴다 | — |
|
|
88
|
+
|
|
89
|
+
## 꼭 지킬 것
|
|
90
|
+
|
|
91
|
+
- `label`이 비어 있거나 key가 비었거나 중복이면 `TypeError`를 던진다. key는 데이터의 stable id로 만든다.
|
|
92
|
+
- `rowHeight`·`height`는 0보다 큰 유한수, `overscan`은 0 이상 정수다. 아니면 `TypeError`.
|
|
93
|
+
- `rowHeight`는 현재 글자 배율에서 행 내용이 들어가는 값으로 제품이 정한다. 행 내용이 넘쳐도 잘리거나 겹칠 뿐
|
|
94
|
+
늘어나지 않는다. 높이를 확정할 수 없으면 List를 쓴다.
|
|
95
|
+
- 행 내용·데이터 가져오기는 제품 소유, 창 계산·키보드 탐색은 HJM 소유다.
|
|
96
|
+
|
|
97
|
+
## 플랫폼 차이
|
|
98
|
+
|
|
99
|
+
| 항목 | Web | Native |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| 구현 | 직접 창 계산, `role="list"`/`listitem`, `aria-setsize`·`aria-posinset` | `FlatList`(`getItemLayout` 고정) |
|
|
102
|
+
| 키보드 | ArrowUp/Down·Home/End로 행 focus 이동, focus된 행은 창 밖에서도 유지 | 없음(보조기술 스크롤은 FlatList 소유) |
|
|
103
|
+
| `overscan` | 창 앞뒤 마운트 행 수 | 첫 렌더 행 수(`initialNumToRender`)에만 더함 |
|
|
104
|
+
| `label` | 목록의 `aria-label` | `FlatList`의 `accessibilityLabel` |
|
|
105
|
+
| `layoutStyle` | 있음 | 없음 |
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# VisuallyHidden
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [VisuallyHidden](../../visually-hidden.md), `react/src/layout.tsx`
|
|
9
|
+
- 스토리북: `배포/컴포넌트/기반 기능/화면 읽기 도구용 글자`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
Web에서 화면에는 보이지 않지만 스크린 리더는 읽어야 하는 **문맥 문구**를 덧붙일 때 쓴다.
|
|
14
|
+
예: 아이콘·숫자만 보이는 상태 옆의 설명, 표의 압축된 셀에 붙는 완전한 문장.
|
|
15
|
+
자식은 DOM과 접근성 트리에 남고 1px clip으로 시각에서만 빠진다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 아이콘 버튼의 이름 | [IconButton](icon-button.md)의 `label` |
|
|
22
|
+
| 본문으로 건너뛰기 링크 | [SkipNav](skip-nav.md) |
|
|
23
|
+
| 포커스 가능한 컨트롤(input·link·button)을 숨김 | 쓰지 않는다. 계약상 금지 |
|
|
24
|
+
| Native에서 추가 문맥 | 컨트롤의 `accessibilityLabel`·`accessibilityHint` |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `VisuallyHidden` | 기본 | `@hjmds/react`, `/layout` | — |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { VisuallyHidden } from "@hjmds/react/layout";
|
|
37
|
+
|
|
38
|
+
<>
|
|
39
|
+
<span aria-hidden="true">{unreadCount}</span>
|
|
40
|
+
<VisuallyHidden>{t("inbox.unreadCount", { count: unreadCount })}</VisuallyHidden>
|
|
41
|
+
</>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Native: 없음. 계약이 Web 전용으로 정했고, 보이지 않는 `Text`를 따로 mount하면 읽기 순서와 중복 낭독이
|
|
45
|
+
달라지므로 host 컨트롤의 `accessibilityLabel`·`accessibilityHint`를 쓴다.
|
|
46
|
+
|
|
47
|
+
## 축과 기본값
|
|
48
|
+
|
|
49
|
+
| prop | 값 | 기본값 | 설명 |
|
|
50
|
+
| --- | --- | --- | --- |
|
|
51
|
+
| `children` | `ReactNode`(문구) | — (필수) | 읽힐 문맥 문구 |
|
|
52
|
+
| 나머지 | `HTMLAttributes<HTMLSpanElement>`, `ref` | — | 루트 `span`에 전달 |
|
|
53
|
+
|
|
54
|
+
`layoutStyle`은 없다. 화면 자리를 차지하지 않아 배치할 대상이 없다.
|
|
55
|
+
|
|
56
|
+
## 배치
|
|
57
|
+
|
|
58
|
+
| 항목 | 값 | 근거 |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| 크기 | 화면 자리를 차지하지 않는다(1px 클립) | `.hjm-visually-hidden` |
|
|
61
|
+
| 간격 | `position: absolute`라 flex·grid 부모의 간격(`gap`)·정렬에 끼지 않는다. 이웃 사이 간격을 맞추려고 따로 여백을 덧대지 않는다 | `.hjm-visually-hidden` |
|
|
62
|
+
| 순서·정렬 | 문맥을 보태는 보이는 요소 **바로 뒤**(DOM 순서상 이웃)에 둬서 읽기 순서가 이어지게 한다. 부모 끝이나 화면 다른 곳에 모아 두지 않는다 | — |
|
|
63
|
+
| 고정·스크롤 | — | — |
|
|
64
|
+
| 좁은 폭·큰 글자 | — | — |
|
|
65
|
+
|
|
66
|
+
## 꼭 지킬 것
|
|
67
|
+
|
|
68
|
+
- 자식은 문구(i18n 키)만 넣는다. 숨긴 상태로 포커스되는 컨트롤을 넣지 않는다.
|
|
69
|
+
- 보이는 문구와 같은 내용을 다시 넣지 않는다. 보이는 쪽을 `aria-hidden`으로 빼거나 추가 문맥만 넣어 중복 낭독을 막는다.
|
|
70
|
+
- 루트는 `span`이고 `HTMLAttributes`·`ref`를 전달한다. `className`은 `hjm-visually-hidden`에 덧붙으며,
|
|
71
|
+
그 클래스 규칙은 `!important`라 위치·크기를 덮어 다시 보이게 만들 수 없다.
|
|
72
|
+
- 숨김 CSS는 `@hjmds/react`의 `styles.css`에 있다. 스타일시트를 불러오지 않은 화면에서는 문구가 그대로 보인다.
|