@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.
Files changed (288) hide show
  1. package/dist/avatar-fallback.d.ts +11 -0
  2. package/dist/avatar-fallback.d.ts.map +1 -1
  3. package/dist/avatar-fallback.js +21 -0
  4. package/dist/avatar-fallback.js.map +1 -1
  5. package/dist/base-recipes.d.ts +17 -0
  6. package/dist/base-recipes.d.ts.map +1 -1
  7. package/dist/base-recipes.js +17 -0
  8. package/dist/base-recipes.js.map +1 -1
  9. package/dist/catalog.d.ts +25 -0
  10. package/dist/catalog.d.ts.map +1 -1
  11. package/dist/command-palette.d.ts +14 -9
  12. package/dist/command-palette.d.ts.map +1 -1
  13. package/dist/command-palette.js +8 -9
  14. package/dist/command-palette.js.map +1 -1
  15. package/dist/component-recipes.d.ts +17 -0
  16. package/dist/component-recipes.d.ts.map +1 -1
  17. package/dist/component-recipes.js +5 -0
  18. package/dist/component-recipes.js.map +1 -1
  19. package/dist/provider-button.d.ts.map +1 -1
  20. package/dist/provider-button.js +3 -0
  21. package/dist/provider-button.js.map +1 -1
  22. package/dist/reactions.d.ts +10 -0
  23. package/dist/reactions.d.ts.map +1 -1
  24. package/dist/reactions.js +7 -0
  25. package/dist/reactions.js.map +1 -1
  26. package/dist/screen-patterns.d.ts +147 -0
  27. package/dist/screen-patterns.d.ts.map +1 -0
  28. package/dist/screen-patterns.js +149 -0
  29. package/dist/screen-patterns.js.map +1 -0
  30. package/dist/slider.d.ts +8 -0
  31. package/dist/slider.d.ts.map +1 -1
  32. package/dist/slider.js +6 -1
  33. package/dist/slider.js.map +1 -1
  34. package/dist/upload-item.d.ts +5 -0
  35. package/dist/upload-item.d.ts.map +1 -1
  36. package/dist/upload-item.js +5 -0
  37. package/dist/upload-item.js.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/dist/version.js.map +1 -1
  41. package/docs/action-session.md +3 -3
  42. package/docs/agreement.md +5 -0
  43. package/docs/avatar-fallback.md +7 -0
  44. package/docs/bottom-navigation.md +6 -0
  45. package/docs/brand-boundary.md +1 -1
  46. package/docs/button-label.md +6 -0
  47. package/docs/clipboard.md +3 -0
  48. package/docs/command-palette.md +45 -2
  49. package/docs/consumer-policy.md +5 -1
  50. package/docs/data-table.md +6 -4
  51. package/docs/dialog.md +8 -2
  52. package/docs/form.md +51 -0
  53. package/docs/generated/component-maturity.md +1 -1
  54. package/docs/generated/renderer-evidence.json +3 -3
  55. package/docs/generated/renderer-evidence.md +1 -1
  56. package/docs/generated/showcase-manifest.json +1 -1
  57. package/docs/link.md +8 -0
  58. package/docs/migration-native-legacy-removal.md +45 -1
  59. package/docs/optional-adapters.md +1 -1
  60. package/docs/password-field.md +5 -0
  61. package/docs/product-composition-adoption.md +40 -0
  62. package/docs/progress.md +19 -1
  63. package/docs/provider-button.md +13 -0
  64. package/docs/result.md +3 -0
  65. package/docs/screen-chrome.md +10 -0
  66. package/docs/screen-patterns.md +376 -0
  67. package/docs/sheet.md +12 -0
  68. package/docs/splitter.md +8 -2
  69. package/docs/theming.md +36 -29
  70. package/docs/toggle-group.md +7 -0
  71. package/docs/tour.md +7 -1
  72. package/docs/tree.md +5 -2
  73. package/docs/upload-item.md +7 -0
  74. package/docs/usage/README.md +236 -0
  75. package/docs/usage/STANDARD.md +108 -0
  76. package/docs/usage/components/accordion.md +107 -0
  77. package/docs/usage/components/activity-heatmap.md +104 -0
  78. package/docs/usage/components/affix.md +86 -0
  79. package/docs/usage/components/agreement.md +129 -0
  80. package/docs/usage/components/alert-dialog.md +130 -0
  81. package/docs/usage/components/anchor.md +96 -0
  82. package/docs/usage/components/aspect-ratio.md +89 -0
  83. package/docs/usage/components/asset.md +126 -0
  84. package/docs/usage/components/auth-provider-button.md +116 -0
  85. package/docs/usage/components/auth-screen-layout.md +129 -0
  86. package/docs/usage/components/avatar.md +114 -0
  87. package/docs/usage/components/badge.md +84 -0
  88. package/docs/usage/components/bottom-cta.md +125 -0
  89. package/docs/usage/components/bottom-info.md +99 -0
  90. package/docs/usage/components/bottom-navigation.md +136 -0
  91. package/docs/usage/components/breadcrumb.md +81 -0
  92. package/docs/usage/components/button.md +118 -0
  93. package/docs/usage/components/calendar.md +122 -0
  94. package/docs/usage/components/card.md +110 -0
  95. package/docs/usage/components/carousel.md +113 -0
  96. package/docs/usage/components/celebration.md +96 -0
  97. package/docs/usage/components/chat-message.md +122 -0
  98. package/docs/usage/components/chat-screen.md +112 -0
  99. package/docs/usage/components/checkbox-group.md +104 -0
  100. package/docs/usage/components/checkbox.md +103 -0
  101. package/docs/usage/components/chip.md +104 -0
  102. package/docs/usage/components/code-block.md +111 -0
  103. package/docs/usage/components/collapsible.md +112 -0
  104. package/docs/usage/components/color-picker.md +86 -0
  105. package/docs/usage/components/combobox.md +137 -0
  106. package/docs/usage/components/command-palette.md +125 -0
  107. package/docs/usage/components/comment-thread-screen.md +125 -0
  108. package/docs/usage/components/container.md +98 -0
  109. package/docs/usage/components/content-transition.md +101 -0
  110. package/docs/usage/components/context-menu.md +136 -0
  111. package/docs/usage/components/counter-badge.md +107 -0
  112. package/docs/usage/components/data-table.md +122 -0
  113. package/docs/usage/components/date-picker.md +142 -0
  114. package/docs/usage/components/date-range-picker.md +111 -0
  115. package/docs/usage/components/description-list.md +103 -0
  116. package/docs/usage/components/design-system-provider.md +124 -0
  117. package/docs/usage/components/dialog.md +176 -0
  118. package/docs/usage/components/divider.md +89 -0
  119. package/docs/usage/components/editor-screen.md +126 -0
  120. package/docs/usage/components/effect-surface.md +120 -0
  121. package/docs/usage/components/empty-state.md +114 -0
  122. package/docs/usage/components/field.md +129 -0
  123. package/docs/usage/components/file-picker.md +114 -0
  124. package/docs/usage/components/floating-action-button.md +138 -0
  125. package/docs/usage/components/form.md +162 -0
  126. package/docs/usage/components/grid.md +99 -0
  127. package/docs/usage/components/heading.md +87 -0
  128. package/docs/usage/components/icon-button.md +126 -0
  129. package/docs/usage/components/icon.md +105 -0
  130. package/docs/usage/components/image.md +122 -0
  131. package/docs/usage/components/keyboard-avoiding.md +93 -0
  132. package/docs/usage/components/keyboard-dock.md +110 -0
  133. package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
  134. package/docs/usage/components/keyboard-motion-provider.md +86 -0
  135. package/docs/usage/components/layout.md +117 -0
  136. package/docs/usage/components/link.md +121 -0
  137. package/docs/usage/components/list-detail-screen.md +103 -0
  138. package/docs/usage/components/list-row.md +124 -0
  139. package/docs/usage/components/list.md +119 -0
  140. package/docs/usage/components/load-more.md +115 -0
  141. package/docs/usage/components/masonry.md +109 -0
  142. package/docs/usage/components/media-selection-screen.md +119 -0
  143. package/docs/usage/components/mentions.md +119 -0
  144. package/docs/usage/components/menu.md +129 -0
  145. package/docs/usage/components/menubar.md +93 -0
  146. package/docs/usage/components/message-composer.md +124 -0
  147. package/docs/usage/components/moderation-screen.md +113 -0
  148. package/docs/usage/components/notice.md +106 -0
  149. package/docs/usage/components/notification-inbox-screen.md +97 -0
  150. package/docs/usage/components/notification-item.md +98 -0
  151. package/docs/usage/components/number-field.md +131 -0
  152. package/docs/usage/components/onboarding-screen.md +106 -0
  153. package/docs/usage/components/otp-field.md +101 -0
  154. package/docs/usage/components/pagination.md +82 -0
  155. package/docs/usage/components/password-field.md +137 -0
  156. package/docs/usage/components/permission-screen.md +107 -0
  157. package/docs/usage/components/photo-source-sheet.md +119 -0
  158. package/docs/usage/components/popover.md +108 -0
  159. package/docs/usage/components/profile-screen.md +89 -0
  160. package/docs/usage/components/progress.md +122 -0
  161. package/docs/usage/components/qr-code.md +122 -0
  162. package/docs/usage/components/radio-group.md +124 -0
  163. package/docs/usage/components/radio.md +104 -0
  164. package/docs/usage/components/result.md +116 -0
  165. package/docs/usage/components/saved-items-screen.md +126 -0
  166. package/docs/usage/components/screen-layout.md +119 -0
  167. package/docs/usage/components/search-field.md +120 -0
  168. package/docs/usage/components/search-screen.md +215 -0
  169. package/docs/usage/components/section.md +111 -0
  170. package/docs/usage/components/segmented-control.md +138 -0
  171. package/docs/usage/components/select.md +142 -0
  172. package/docs/usage/components/settings-screen.md +126 -0
  173. package/docs/usage/components/shared-transition-element.md +111 -0
  174. package/docs/usage/components/shared-transition-screen.md +86 -0
  175. package/docs/usage/components/sheet.md +151 -0
  176. package/docs/usage/components/side-panel.md +104 -0
  177. package/docs/usage/components/sidebar.md +107 -0
  178. package/docs/usage/components/skeleton.md +105 -0
  179. package/docs/usage/components/skip-nav.md +76 -0
  180. package/docs/usage/components/slider.md +121 -0
  181. package/docs/usage/components/sortable-collection.md +127 -0
  182. package/docs/usage/components/spinner.md +86 -0
  183. package/docs/usage/components/splitter.md +103 -0
  184. package/docs/usage/components/stack.md +93 -0
  185. package/docs/usage/components/statistic.md +123 -0
  186. package/docs/usage/components/steps.md +110 -0
  187. package/docs/usage/components/surface.md +91 -0
  188. package/docs/usage/components/swipe-actions.md +124 -0
  189. package/docs/usage/components/switch.md +120 -0
  190. package/docs/usage/components/tabs.md +134 -0
  191. package/docs/usage/components/tag.md +84 -0
  192. package/docs/usage/components/tags-input.md +111 -0
  193. package/docs/usage/components/text-area.md +112 -0
  194. package/docs/usage/components/text-format.md +75 -0
  195. package/docs/usage/components/text-transition.md +104 -0
  196. package/docs/usage/components/text.md +101 -0
  197. package/docs/usage/components/thinking-orb.md +105 -0
  198. package/docs/usage/components/timeline.md +105 -0
  199. package/docs/usage/components/toast.md +145 -0
  200. package/docs/usage/components/toggle-group.md +95 -0
  201. package/docs/usage/components/tooltip.md +103 -0
  202. package/docs/usage/components/top-bar.md +124 -0
  203. package/docs/usage/components/top.md +89 -0
  204. package/docs/usage/components/tour.md +118 -0
  205. package/docs/usage/components/transfer-list.md +115 -0
  206. package/docs/usage/components/tree.md +91 -0
  207. package/docs/usage/components/upload-item.md +99 -0
  208. package/docs/usage/components/virtual-list.md +105 -0
  209. package/docs/usage/components/visually-hidden.md +72 -0
  210. package/docs/usage/components/watermark.md +78 -0
  211. package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
  212. package/docs/usage/compositions/action-recovery-save.md +235 -0
  213. package/docs/usage/compositions/action-recovery-undo.md +193 -0
  214. package/docs/usage/compositions/common-message.md +132 -0
  215. package/docs/usage/compositions/common-notification.md +101 -0
  216. package/docs/usage/compositions/compound-controls.md +186 -0
  217. package/docs/usage/compositions/data-layouts.md +157 -0
  218. package/docs/usage/compositions/disclosure.md +144 -0
  219. package/docs/usage/compositions/environment-matrix.md +139 -0
  220. package/docs/usage/compositions/expo-interactions.md +149 -0
  221. package/docs/usage/compositions/family-drawer.md +201 -0
  222. package/docs/usage/compositions/floating-action-button.md +197 -0
  223. package/docs/usage/compositions/input-sheet.md +148 -0
  224. package/docs/usage/compositions/interaction-adapters.md +190 -0
  225. package/docs/usage/compositions/interaction-flow-apply.md +205 -0
  226. package/docs/usage/compositions/interaction-flow-draft.md +188 -0
  227. package/docs/usage/compositions/interaction-flow-search.md +171 -0
  228. package/docs/usage/compositions/native-renderers.md +106 -0
  229. package/docs/usage/compositions/navigation-bar-collection.md +164 -0
  230. package/docs/usage/compositions/optional-adapters.md +169 -0
  231. package/docs/usage/compositions/optional-motion.md +109 -0
  232. package/docs/usage/compositions/photo-source.md +104 -0
  233. package/docs/usage/compositions/purpose-input-comment.md +110 -0
  234. package/docs/usage/compositions/purpose-input-message.md +119 -0
  235. package/docs/usage/compositions/reference-first.md +96 -0
  236. package/docs/usage/compositions/reference-review.md +107 -0
  237. package/docs/usage/compositions/reference-settings.md +107 -0
  238. package/docs/usage/compositions/selection-scope.md +174 -0
  239. package/docs/usage/compositions/stea-event-ticket.md +166 -0
  240. package/docs/usage/compositions/stea-flip-card.md +162 -0
  241. package/docs/usage/compositions/stea-order-progress.md +184 -0
  242. package/docs/usage/compositions/stea-otp-verify.md +215 -0
  243. package/docs/usage/compositions/stea-pixel-empty.md +140 -0
  244. package/docs/usage/compositions/stea-schedule-card.md +169 -0
  245. package/docs/usage/compositions/stea-stat-summary.md +154 -0
  246. package/docs/usage/compositions/time-selection.md +174 -0
  247. package/docs/usage/compositions/toast-layout.md +128 -0
  248. package/docs/usage/compositions/visual-foundations.md +185 -0
  249. package/docs/usage/compositions/web-additions.md +146 -0
  250. package/docs/usage/compositions/web-navigation.md +143 -0
  251. package/docs/usage/screens/common-chat.md +127 -0
  252. package/docs/usage/screens/common-comments.md +108 -0
  253. package/docs/usage/screens/common-inbox.md +110 -0
  254. package/docs/usage/screens/common-login.md +98 -0
  255. package/docs/usage/screens/common-profile.md +221 -0
  256. package/docs/usage/screens/common-saved.md +127 -0
  257. package/docs/usage/screens/common-search.md +274 -0
  258. package/docs/usage/screens/common-settings.md +126 -0
  259. package/docs/usage/screens/common-shell.md +108 -0
  260. package/docs/usage/screens/dashboard.md +245 -0
  261. package/docs/usage/screens/discovery-gallery.md +306 -0
  262. package/docs/usage/screens/flow-collection.md +96 -0
  263. package/docs/usage/screens/flow-editor.md +120 -0
  264. package/docs/usage/screens/flow-media.md +111 -0
  265. package/docs/usage/screens/flow-moderation.md +120 -0
  266. package/docs/usage/screens/flow-onboarding.md +193 -0
  267. package/docs/usage/screens/flow-permission.md +103 -0
  268. package/docs/usage/screens/landing.md +347 -0
  269. package/docs/usage/screens/mockup-studio.md +190 -0
  270. package/docs/usage/screens/notification-settings.md +206 -0
  271. package/docs/usage/screens/reference-comparison.md +159 -0
  272. package/docs/usage/templates/component.md +61 -0
  273. package/docs/usage/templates/composition.md +47 -0
  274. package/docs/usage/templates/screen.md +56 -0
  275. package/docs/usage/templates/token.md +32 -0
  276. package/docs/usage/tokens/color.md +142 -0
  277. package/docs/usage/tokens/elevation-opacity.md +86 -0
  278. package/docs/usage/tokens/layers.md +98 -0
  279. package/docs/usage/tokens/layout.md +114 -0
  280. package/docs/usage/tokens/motion.md +88 -0
  281. package/docs/usage/tokens/radius.md +53 -0
  282. package/docs/usage/tokens/size.md +74 -0
  283. package/docs/usage/tokens/spacing.md +73 -0
  284. package/docs/usage/tokens/stroke.md +50 -0
  285. package/docs/usage/tokens/theme-studio.md +70 -0
  286. package/docs/usage/tokens/typography-studio.md +70 -0
  287. package/docs/usage/tokens/typography.md +89 -0
  288. 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`에 있다. 스타일시트를 불러오지 않은 화면에서는 문구가 그대로 보인다.