@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,124 @@
1
+ # SwipeActions
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: 별도 보조 기능(supplemental), 계약 함수 `validateActions`·`RowAction`(`components/interaction-adapters`), [optional adapters](../../optional-adapters.md)
9
+ - 스토리북: `배포/구성/직접 조작과 모션/끌기·밀기·화면 전환`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 목록 행 하나에 붙은 **삭제·보관 같은 행 단위 행동**을, Native에서는 행을 밀어 드러내고 Web에서는
14
+ 행 아래 버튼으로 바로 보여 줄 때 쓴다. 스와이프 전용 기능은 없다. 같은 행동이 항상 다른 경로로도 열린다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 행을 눌러 상세로 이동, 설정 행 | [ListRow](list-row.md), [List](list.md) |
21
+ | 행 행동이 많거나 메뉴로 묶어야 함 | [Menu](menu.md), [ContextMenu](context-menu.md) |
22
+ | 순서 바꾸기 | [SortableCollection](sortable-collection.md) |
23
+ | 되돌릴 수 없는 삭제의 최종 확인 | 행동 처리 안에서 [AlertDialog](alert-dialog.md) |
24
+ | 시트·화면 닫기 스와이프 | [Sheet](sheet.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `SwipeActions` | 보조(supplemental) | `/swipe-actions` | `/swipe-actions` |
31
+
32
+ granular subpath로만 import 된다. Native는 `react-native-gesture-handler/ReanimatedSwipeable`을 import 하므로
33
+ optional peer `react-native-gesture-handler` 2.32.0과 Reanimated(`react-native-reanimated` ^4.5.1,
34
+ `react-native-worklets` ^0.10.1)를 앱에 설치하고 앱 루트의 GestureHandlerRootView·Reanimated 설정을 갖춰야 한다
35
+ ([optional adapters](../../optional-adapters.md)의 설치 절). Web은 추가 peer가 없다.
36
+
37
+ ## 최소 사용 예
38
+
39
+ ```tsx
40
+ // Web
41
+ import { SwipeActions } from "@hjmds/react/swipe-actions";
42
+
43
+ <SwipeActions
44
+ label={t("inbox.rowActions", { title: item.title })}
45
+ actions={[{ id: "archive", label: t("inbox.archive") }, { id: "delete", label: t("inbox.delete"), intent: "danger" }]}
46
+ onAction={(id) => handleRowAction(item.id, id)}
47
+ onError={showErrorToast}
48
+ >
49
+ <InboxRow item={item} />
50
+ </SwipeActions>
51
+ ```
52
+
53
+ ```tsx
54
+ // Native — 목록 전체가 openRowId 하나를 공유한다
55
+ import { SwipeActions } from "@hjmds/react-native/swipe-actions";
56
+
57
+ <SwipeActions
58
+ rowId={item.id}
59
+ openRowId={openRowId}
60
+ onOpenRowChange={setOpenRowId}
61
+ label={t("inbox.rowActions", { title: item.title })}
62
+ actionsLabel={t("inbox.showActions")}
63
+ actions={rowActions}
64
+ onAction={(id) => handleRowAction(item.id, id)}
65
+ onError={showErrorToast}
66
+ >
67
+ <InboxRow item={item} />
68
+ </SwipeActions>
69
+ ```
70
+
71
+ ## 축과 기본값
72
+
73
+ | prop | 값 | 기본값 | 설명 |
74
+ | --- | --- | --- | --- |
75
+ | `actions` | `{ id, label, intent?: "default" \| "danger", disabled? }[]` | — (필수) | `id`는 고유, `label`이 비면 `TypeError`. `danger`는 `danger` 버튼, 나머지는 `ghost` 버튼 |
76
+ | `busy` | `boolean` | — | 모든 행동을 막는다. 처리 중에는 두 번째 행동을 받지 않는다(`onAction`이 끝날 때까지) |
77
+ | `onAction` | `(id: string) => void \| Promise<void>` | — (필수) | 누른 행동의 `id`. Promise를 돌려주면 끝날 때까지 다음 행동을 받지 않는다 |
78
+ | `onError` | `(error: unknown) => void` | — (필수) | `onAction`이 던지거나 reject 하면 받는다 |
79
+ | `rowId`(Native) | `string` | — (필수) | 데이터의 안정된 행 id |
80
+ | `openRowId` + `onOpenRowChange`(Native) | `string \| null`, `(id: string \| null) => void` | — (필수) | 목록 전체가 공유하는 열린 행. 닫히면 `null` |
81
+ | `actionsLabel`(Native) | `string` | — (필수) | 스와이프 대체 버튼 이름 |
82
+ | `layoutStyle`(Web) | `HjmCompositionStyleProp` | — | 루트 `group` 배치. Native에는 없다 |
83
+
84
+ ## 배치
85
+
86
+ | 항목 | 값 | 근거 |
87
+ | --- | --- | --- |
88
+ | 크기 | 목록 행 하나를 감싼다. 행 높이·여백은 `children`(보통 [ListRow](list-row.md))이 정한다. 행동 버튼은 `medium` 높이 44의 Button(`danger` 또는 `ghost`) | `react/src/swipe-actions.tsx`, `react-native/src/swipe-actions.tsx` |
89
+ | 간격 | 행동 버튼 사이 `spacing.xs` 8, 넘치면 줄바꿈 | 같은 두 파일 |
90
+ | 순서·정렬 | Web: 행 바로 아래에 버튼 줄이 항상 보인다. Native: 버튼은 행 오른쪽 뒤(RTL은 왼쪽)에 숨어 있다가 밀면 드러난다. 행 아래에는 `actionsLabel` ghost 버튼이 항상 있고, 누르거나 reduced motion이면 그 아래에 행동 버튼 줄이 펼쳐진다. 파괴 행동(`danger`)은 마지막에 두고, 2~3개를 넘으면 [Menu](menu.md)로 묶는다 | `react-native/src/swipe-actions.tsx` |
91
+ | 고정·스크롤 | — | — |
92
+ | 좁은 폭·큰 글자 | 행동 버튼 줄은 `flexWrap: wrap`으로 줄바꿈된다 | 같은 두 파일 |
93
+
94
+ ```text
95
+ Native(LTR) Web
96
+ ┌────────────────────┬────────────┐ ┌──────────────────────────┐
97
+ │ 행(children) ← 밀기│[보관][삭제]│ │ 행(children) │
98
+ └────────────────────┴────────────┘ ├──────────────────────────┤
99
+ [ actionsLabel ] ← 스와이프 대체 경로 │ [보관] [삭제] 항상 표시 │
100
+ └──────────────────────────┘
101
+ ```
102
+
103
+ ## 꼭 지킬 것
104
+
105
+ - 행동 label·행 label은 i18n 키로 넣는다. 어떤 행동을 두는지는 제품이 정한다.
106
+ - Native는 `openRowId`/`onOpenRowChange`를 목록 단위 상태로 두어 한 번에 한 행만 열리게 한다.
107
+ - `actionsLabel` 버튼은 스와이프를 쓰지 못하는 사용자의 경로다. 숨기거나 빼지 않는다.
108
+ - 행 모양은 `children`이 소유한다. Web은 `layoutStyle`로 바깥 배치만 하고, Native는 스타일·`layoutStyle` prop이 없다.
109
+
110
+ ## 플랫폼 차이
111
+
112
+ | 항목 | Web | Native |
113
+ | --- | --- | --- |
114
+ | 표시 방식 | 행 아래 버튼을 항상 표시(`role="group"`) | 스와이프로 드러냄 + `actionsLabel` 버튼으로 펼침 |
115
+ | 필수 prop | `label`·`actions`·`onAction`·`onError` | 더해 `rowId`·`openRowId`·`onOpenRowChange`·`actionsLabel` |
116
+ | reduced motion | 해당 없음 | 스와이프를 끄고 버튼을 펼쳐 보여 줌 |
117
+ | RTL | 해당 없음 | 행동을 왼쪽에서 드러냄 |
118
+ | 백그라운드 전환 | 해당 없음 | 열린 행을 닫음 |
119
+ | `layoutStyle` | 있음 | 없음 |
120
+
121
+ ## 함정
122
+
123
+ - FlashList처럼 행 뷰를 재활용하는 목록에서도 `rowId`가 바뀌면 진행 중 표시와 펼친 메뉴가 초기화된다.
124
+ `rowId`는 데이터의 안정된 id로 넘긴다.
@@ -0,0 +1,120 @@
1
+ # Switch
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [제품 채택 1.4 §설정 한 행](../../product-adoption-1.4.md), `src/component-recipes.ts`(`switchRecipe`)
9
+ - 스토리북: `배포/컴포넌트/입력/스위치`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 켜고 끄는 즉시 반영되는 설정 하나에 쓴다. 알림 받기, 다크 모드, 자동 재생 같은 설정 화면의 행이
14
+ 대표적이다. 설정 화면에서는 `presentation="row"`로 **Switch 하나가 행 전체**가 된다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 폼 제출 때 함께 보내는 동의·선택 | [Checkbox](checkbox.md) |
21
+ | 여러 항목을 함께 고름 | [CheckboxGroup](checkbox-group.md) |
22
+ | 둘 이상 중 하나를 고름 | [SegmentedControl](segmented-control.md), [RadioGroup](radio-group.md) |
23
+ | 버튼 모양의 켜짐/꺼짐 | [Button](button.md)의 `selected`, [ToggleGroup](toggle-group.md) |
24
+ | 약관 동의 묶음 | [Agreement](agreement.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `Switch` | 기본 | `@hjmds/react`, `/selection` | `@hjmds/react-native`, `/inputs` |
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { Switch } from "@hjmds/react/selection";
37
+
38
+ <Switch
39
+ presentation="row"
40
+ label={t("settings.push.title")}
41
+ description={t("settings.push.description")}
42
+ checked={pushEnabled}
43
+ onCheckedChange={setPushEnabled}
44
+ />
45
+ ```
46
+
47
+ ```tsx
48
+ // Native
49
+ import { Switch } from "@hjmds/react-native/inputs";
50
+
51
+ <Switch
52
+ presentation="row"
53
+ label={t("settings.push.title")}
54
+ description={t("settings.push.description")}
55
+ checked={pushEnabled}
56
+ onCheckedChange={setPushEnabled}
57
+ />
58
+ ```
59
+
60
+ ## 축과 기본값
61
+
62
+ | prop | 값 | 기본값 | 설명 |
63
+ | --- | --- | --- | --- |
64
+ | `presentation` | `inline` · `row` | Web `inline`, Native `row` | 기본값이 플랫폼마다 다르다. 같은 설정 화면을 두 표면에서 맞추려면 **항상 명시**한다 |
65
+ | `size` | `small`(44×26) · `medium`(52×32) | `medium` | — |
66
+ | `checked`/`defaultChecked` | `boolean` | `defaultChecked` `false` | `checked`를 생략하면 비제어로 동작한다 |
67
+ | `onCheckedChange` | `(checked: boolean) => void` | — | 다음 상태를 받는다 |
68
+ | `labelVisibility` | `visible` · `hidden` | `visible` | `hidden`은 글자만 화면에서 숨기고 접근성 이름은 남긴다 |
69
+ | `description` | 문구 | — | 이름과 따로 연결한다(Web `aria-describedby`, Native `accessibilityHint` 기본값) |
70
+ | `disabled` | `boolean` | `false` | — |
71
+ | `layoutStyle` | `HjmCompositionStyleProp` | — | 루트(행) 배치. Web·Native 모두 |
72
+ | `style`(Native) | `StyleProp<ViewStyle>` | — | deprecated — `layoutStyle` 또는 `size`. 개발 모드에서 한 번 경고하고 다음 major에서 제거된다 |
73
+
74
+ 큰 글자(공통 large-text 기준)에서 `row`는 설명 아래로 track을 내린다.
75
+
76
+ ## 배치
77
+
78
+ | 항목 | 값 | 근거 |
79
+ | --- | --- | --- |
80
+ | 크기 | 트랙 `small` 44×26(thumb 22) · `medium` 52×32(thumb 28), 안쪽 여백 2. iOS Native는 `UISwitch` 고유 크기. 루트 최소 높이 44(`control.minTouchTarget`). 행 높이는 설명이 없으면 44, Native는 설명이 있으면 68(`rowTwoLineMinHeight`, ListRow 두 줄 행과 같다). Web은 68을 고정하지 않고 내용 높이를 따른다 | `switchRecipe.sizes`·`rowMinHeight`·`rowTwoLineMinHeight`, `styles.css` `.hjm-switch`, `react-native/src/inputs.tsx` |
81
+ | 간격 | 이름과 트랙 사이 `spacing.sm` 12, 이름과 설명 사이 `spacing.xxs` 4. 여러 개를 쌓을 때는 [List](list.md)·[ListRow](list-row.md) 행 구분을 따르고 Switch 사이에 여백을 더하지 않는다 | `styles.css` `.hjm-switch`·`.hjm-switch__copy`, `react-native/src/inputs.tsx` |
82
+ | 순서·정렬 | 설정 목록에서는 `presentation="row"`로 행 전체 폭을 쓰고 이름·설명이 앞(왼쪽), 트랙이 끝(오른쪽) | `styles.css` `.hjm-switch[data-presentation="row"]` |
83
+ | 고정·스크롤 | — | — |
84
+ | 좁은 폭·큰 글자 | 글자 배율 ≥ 1.6(`largeTextThreshold`)이면 `row`는 이름·설명 아래로 트랙을 내린다 | `switchRecipe.stackedTextScale`, `styles.css` `.hjm-root[data-large-text="true"]` |
85
+
86
+ ```text
87
+ presentation="row" 큰 글자(≥1.6)
88
+ ┌───────────────────────────────┐ ┌───────────────────────┐
89
+ │ 알림 받기 (●──) │ │ 알림 받기 │
90
+ │ 새 편지가 오면 알려요 │ │ 새 편지가 오면 알려요 │
91
+ └───────────────────────────────┘ │ (●──) │
92
+ ↑ 이름·설명 · 간격 12 · 트랙 ↑ └───────────────────────┘
93
+ ```
94
+
95
+ ## 꼭 지킬 것
96
+
97
+ - `label`·`description`은 i18n 키로 넣는다. 설정 항목과 문구는 제품 소유, 행 구조·색·접근성은 HJM 소유다.
98
+ - `row`인 Switch를 다른 Pressable·button·ListRow `onPress`로 감싸지 않는다. 행 전체가 이미 하나의 switch다.
99
+ - 기존 [ListRow](list-row.md)의 trailing control로만 둘 때는 `labelVisibility="hidden"`과 같은 `label`을 주고,
100
+ ListRow에는 `onPress`를 두지 않는다.
101
+ - 배치는 `layoutStyle`로 한다. Native `style`은 쓰지 않는다. track·thumb 색은 recipe가 정하며 덮지 않는다.
102
+ - 저장 요청 중에는 `disabled`로 막는다. 비활성 상태도 켜짐/꺼짐이 구분되게 색이 바뀐다.
103
+
104
+ ## 플랫폼 차이
105
+
106
+ | 항목 | Web | Native |
107
+ | --- | --- | --- |
108
+ | 요소 | `<button role="switch">` | `Pressable`(role `switch`) 안의 RN `Switch` |
109
+ | `label`·`description` 타입 | `ReactNode` | `string` |
110
+ | 이름 재지정 | `aria-label`/`aria-labelledby` | `accessibilityLabel`/`accessibilityHint` |
111
+ | 꺼짐 상태 hairline | 그림(track·thumb 모두 1px inset) | 그리지 않음(RN `Switch`에 테두리 hook이 없음, `*Border` 슬롯은 Web 전용) |
112
+ | iOS 크기 | 해당 없음 | `UISwitch` 고유 크기를 따른다(Android는 recipe 크기) |
113
+
114
+ ## 함정
115
+
116
+ - Native는 옛 `value`/`defaultValue`/`onValueChange`를 받으면 실행 중 `TypeError`를 던진다.
117
+ `checked`/`defaultChecked`/`onCheckedChange`를 쓴다.
118
+ - Native 꺼짐 상태에는 테두리가 없으므로, 어두운 카드 위에서 track이 배경과 섞이는지 실제 기기 다크 모드로 확인한다.
119
+ (Web은 이 hairline이 빠져 보이지 않던 일이 있었고 현재 stylesheet가 그린다.)
120
+ - Web은 `type="button"`이 기본이라 폼 안에서도 submit을 일으키지 않는다.
@@ -0,0 +1,134 @@
1
+ # Tabs
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Gooey navigation](../../gooey-navigation.md), `src/component-recipes.ts`(`tabsRecipe`), `src/behaviors.ts`(`tabsBehaviorDefaults`)
9
+ - 스토리북: `배포/컴포넌트/탐색/탭`, `배포/컴포넌트/탐색/선택 표시가 이어지는 탭`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 같은 화면 안에서 **서로 다른 패널 여러 개 중 하나를 보여 줄 때** 쓴다. 프로필의 "게시물/좋아요",
14
+ 설정의 "일반/알림"처럼 탭마다 패널이 따로 있고 화면 이동 없이 바뀌는 자리다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 같은 목록의 필터·보기 방식만 바꿈(패널이 하나) | [SegmentedControl](segmented-control.md) |
21
+ | 앱의 최상위 화면 이동 | [BottomNavigation](bottom-navigation.md), [Sidebar](sidebar.md) |
22
+ | 여러 개를 동시에 켜고 끔 | [ToggleGroup](toggle-group.md), [Chip](chip.md) |
23
+ | 순서가 있는 단계 진행 | [Steps](steps.md) |
24
+ | 접고 펼치는 여러 섹션 | [Accordion](accordion.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `Tabs` | 기본 | `@hjmds/react`, `/navigation` | `@hjmds/react-native`, `/navigation`, `/top-bar` |
31
+ | `TabPanel` | 동반(패널을 Tabs 밖에 둘 때) | `@hjmds/react`, `/navigation` | `@hjmds/react-native`, `/navigation`, `/top-bar` |
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { Tabs } from "@hjmds/react/navigation";
38
+
39
+ <Tabs
40
+ label={t("profile.tabs")}
41
+ value={tab}
42
+ onValueChange={setTab}
43
+ items={[
44
+ { id: "posts", label: t("profile.posts"), panel: <PostList /> },
45
+ { id: "likes", label: t("profile.likes"), panel: <LikeList /> },
46
+ ]}
47
+ />
48
+ ```
49
+
50
+ ```tsx
51
+ // Native
52
+ import { Tabs } from "@hjmds/react-native/navigation";
53
+
54
+ <Tabs
55
+ label={t("profile.tabs")}
56
+ defaultValue="posts"
57
+ layout="fitted"
58
+ items={[
59
+ { id: "posts", label: t("profile.posts"), panel: <PostList /> },
60
+ { id: "likes", label: t("profile.likes"), badge: "3", badgeAccessibilityLabel: t("profile.likesNew", { count: 3 }), panel: <LikeList /> },
61
+ ]}
62
+ />
63
+ ```
64
+
65
+ ## 축과 기본값
66
+
67
+ | prop | 값 | 기본값 | 설명 |
68
+ | --- | --- | --- | --- |
69
+ | `value`+`onValueChange` / `defaultValue` | 항목 `id`, `(value: string) => void`(Native는 `Value`) | 첫 활성 항목 | controlled는 둘 다 필수. 비제어에서도 `onValueChange`로 바뀐 값을 받는다 |
70
+ | `items[]` | Web `{ id, label: ReactNode, panel?, renderLeading?, disabled? }`, Native는 더해 `badge?`·`badgeAccessibilityLabel?`·`panelAccessibilityLabel?`(`label`은 `string`) | — (필수) | — |
71
+ | `renderLeading`(항목) | `(state: { selected, disabled, color, size }) => ReactNode` | — | `color`는 Web `"currentColor"`, Native 색 문자열 |
72
+ | `activationMode` | `manual` · `automatic` | `manual` | `manual`은 Web에서 방향키로 포커스만 옮기고 Enter/Space로 선택 |
73
+ | `mountPolicy` | `active` · `visited` · `always` | `active` | `active`는 선택된 패널만 mount |
74
+ | `panelMode` | `keyed` · `dynamic` | `keyed` | `dynamic`은 `mountPolicy="active"`일 때만 허용 |
75
+ | `size` | `medium` · `small` | `medium` | 최소 높이 48 · 44 |
76
+ | `layout` | `content` · `fitted` | `content` | `fitted`는 폭을 나눠 채움 |
77
+ | `overflow` | `scroll` · `clip` | `scroll` | — |
78
+ | `orientation` | `horizontal` · `vertical` | `horizontal` | — |
79
+ | `loop` | `boolean` | `true` | — |
80
+ | `appearance` | `standard` · `gooey` | `standard` | `gooey`는 가로일 때만 선택 표시가 늘어나며 이동, 세로는 standard 유지 |
81
+ | `renderPanels` | `boolean` | `true` | `false`면 패널을 그리지 않는다. 패널을 라우터·스크롤 상태와 함께 따로 둘 때 `TabPanel`에 `tabsId`(Tabs의 `id`와 같게)·`activeValue`·`value`를 넘긴다 |
82
+ | `children`(Native) | `(selectedValue: Value) => ReactNode` | — | 항목 `panel` 대신 선택 값으로 패널을 그린다 |
83
+ | `TabPanel` | `{ tabsId, activeValue, children, mode?: "keyed", value, mountPolicy? }` 또는 `{ mode: "dynamic" }`, Native는 `label` 필수 | — | — |
84
+ | `layoutStyle` | `HjmCompositionStyleProp` | — | Tabs·TabPanel 루트 배치. Web·Native 모두 |
85
+ | Native `style`·`tabListStyle`, TabPanel `style` | `StyleProp<ViewStyle>` | — | deprecated — `layoutStyle` 또는 `size`·`layout`·`overflow`·`appearance`. 개발 모드에서 한 번 경고하고 다음 major에서 제거된다 |
86
+
87
+ ## 배치
88
+
89
+ | 항목 | 값 | 근거 |
90
+ | --- | --- | --- |
91
+ | 크기 | 탭 높이 `medium` 최소 48, `small` 최소 44(`control.minTouchTarget`). 목록 아래 구분선 1(`border` 색), 선택 밑줄 2(`stroke.strong`) | `tabsRecipe.sizes`·`indicatorHeight`, `styles.css` `.hjm-tabs__tab` |
92
+ | 간격 | 좌우 안쪽 여백 `medium` `spacing.md` 16 · `small` `spacing.sm` 12, 탭 사이 `spacing.xs` 8, 아이콘과 라벨 사이 `spacing.xs` 8. Web 패널 위아래 여백 `spacing.md` 16 | `tabsRecipe`, `styles.css` `.hjm-tabs__list`·`.hjm-tabs__leading`·`.hjm-tabs__panel` |
93
+ | 순서·정렬 | 화면 상단(TopBar 아래)이나 섹션 머리에 가로로 놓고 패널이 바로 아래. 탭이 적고(2~4) 폭을 꽉 채워야 하면 `layout="fitted"`, 많거나 라벨 길이가 다르면 `content`. 세로(`orientation="vertical"`)는 Web에서 왼쪽 목록(최소 8rem)과 오른쪽 패널을 `spacing.md` 16 간격으로 나란히 두고 구분선이 목록 오른쪽에 선다 | `styles.css` `.hjm-tabs[data-orientation="vertical"]`, `react-native/src/navigation.tsx` |
94
+ | 고정·스크롤 | 기본 `overflow="scroll"`로 탭 목록이 가로 스크롤한다. 목록은 스크롤 영역 안에서 고정되지 않으며 상단에 붙여야 하면 제품이 sticky 영역에 넣는다 | `tabsRecipe.overflow`, `styles.css` `.hjm-tabs__list` |
95
+ | 좁은 폭·큰 글자 | 라벨은 줄바꿈하지 않는다(`white-space: nowrap`). 좁은 폭·큰 글자에서는 가로 스크롤로 받는다 | `styles.css` `.hjm-tabs__tab` |
96
+
97
+ ```text
98
+ ┌────────────────────────────────────┐
99
+ │ TopBar │
100
+ ├────────────────────────────────────┤
101
+ │ [전체] [내 글] [저장] →(스크롤) │ ← 최소 48, 사이 8
102
+ │ ━━━━ │ ← 선택 밑줄 2 / 구분선 1
103
+ ├────────────────────────────────────┤
104
+ │ 패널(Web 위아래 16) │
105
+ └────────────────────────────────────┘
106
+ ```
107
+
108
+ ## 꼭 지킬 것
109
+
110
+ - `label`(탭 목록 이름)은 필수이고 비어 있으면 `TypeError`다. 항목 라벨과 함께 i18n 키로 넣는다.
111
+ - `items`의 `id`는 비지 않고 겹치지 않아야 하며, 활성 항목이 하나 이상 있어야 한다(위반 시 `TypeError`).
112
+ Web controlled `value`가 비활성·없는 항목이면 `RangeError`다.
113
+ - 아이콘은 `renderLeading`으로 넣고 받은 `color`·`size`를 쓴다. 색을 직접 정하지 않는다.
114
+ - 탭을 바꿔도 화면 이동(뒤로 가기 대상)이 아니다. 주소에 남겨야 하면 제품이 controlled 값으로 동기화한다.
115
+ - 배치는 `layoutStyle`로 한다. Web 루트 `div` 속성(`className`·`style`)과 Native의 deprecated `style`·`tabListStyle`로
116
+ 색·높이·인디케이터를 덮지 않는다.
117
+
118
+ ## 플랫폼 차이
119
+
120
+ | 항목 | Web | Native |
121
+ | --- | --- | --- |
122
+ | 항목 `label` 타입 | `ReactNode` | `string` |
123
+ | 항목 배지 | 없음 | `badge`, `badgeAccessibilityLabel` |
124
+ | 패널 접근 이름 | 탭 라벨로 연결(`aria-labelledby`) | 항목 `panelAccessibilityLabel`, `TabPanel`은 `label` 필수 |
125
+ | 패널 렌더 함수 | 없음 | `children(selectedValue)` |
126
+ | 값 타입 | `string` | 제네릭 `Value extends string` |
127
+ | ref | `forwardRef`(`div`) | 없음 |
128
+ | import 경로 | `/navigation` | `/navigation`, `/top-bar` |
129
+
130
+ ## 함정
131
+
132
+ - Native `Tabs`는 예전 `options` prop을 받으면 `TypeError`("options was removed")를 던진다. `items`로 옮긴다.
133
+ - 외부 `TabPanel`을 쓸 때 Tabs에 `id`를 주지 않으면 Web은 생성 id를 써서 `tabsId`를 맞출 수 없다.
134
+ `id`를 명시하고 같은 값을 `TabPanel tabsId`에 넘긴다.
@@ -0,0 +1,84 @@
1
+ # Tag
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Tag contract](../../tag.md), `src/tag.ts`(`tagRecipe`)
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/태그`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 반복해서 나오는 **정적 메타데이터 한 조각**에 쓴다. `좌익수`, `A등급`, `2026 시즌`처럼 누르지도,
14
+ 고르지도, 지우지도 않는 짧은 라벨이다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 누르거나 선택·해제하는 조각(필터, 지울 수 있는 태그) | [Chip](chip.md) |
21
+ | 상태(새 글, 실패, 진행 중)를 알림 | [Badge](badge.md) |
22
+ | 아이콘·아바타 위 숫자 | [CounterBadge](counter-badge.md) |
23
+ | 사용자가 여러 값을 입력해 모음 | [TagsInput](tags-input.md) |
24
+ | 경고·위험 안내 | [Notice](notice.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `Tag` | 기본 | `@hjmds/react`, `/display` | `@hjmds/react-native`, `/data-display` |
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { Tag } from "@hjmds/react/display";
37
+
38
+ <Tag tone="success">{t("player.gradeA")}</Tag>
39
+ ```
40
+
41
+ ```tsx
42
+ // Native
43
+ import { Tag } from "@hjmds/react-native/data-display";
44
+
45
+ <Tag layoutStyle={{ marginTop: 4 }}>{t("player.position.leftField")}</Tag>
46
+ ```
47
+
48
+ ## 축과 기본값
49
+
50
+ | prop | 값 | 기본값 | 설명 |
51
+ | --- | --- | --- | --- |
52
+ | `children` | 문자열 | — (필수) | 문자열만 받는다. 비어 있으면 `TypeError` |
53
+ | `tone` | `neutral` · `info` · `success` · `attention` · `brand` | `neutral` | `warning`·`danger`는 없다(이유는 계약 문서) |
54
+ | `layoutStyle` | `HjmCompositionStyleProp` | — | 바깥 여백·`alignSelf` 같은 배치. Web·Native 모두 |
55
+ | `accessibilityLabel`(Native) | `string` | 라벨 | — |
56
+ | `style`·`labelStyle`(Native) | `StyleProp` | — | deprecated — `layoutStyle` 또는 `tone`. 개발 모드에서 한 번 경고하고 다음 major에서 제거된다 |
57
+
58
+ 모양은 radius `sm` 사각형, 최소 높이 20(큰 글자에서 늘어남), caption 크기 semibold 글자다.
59
+
60
+ ## 배치
61
+
62
+ | 항목 | 값 | 근거 |
63
+ | --- | --- | --- |
64
+ | 크기 | 최소 높이 20, 테두리 1, radius `sm` 8. 누르는 대상이 아니므로 44 터치 영역을 두지 않는다 | `tagRecipe.size`, `styles.css` `.hjm-tag`, `react-native/src/data-display.tsx` |
65
+ | 간격 | 좌우 여백 `spacing.xxs` 4. 제목 옆이면 간격 `spacing.xs` 8, 여러 개는 가로로 `spacing.xxs` 4~`xs` 8 | `tagRecipe.size` |
66
+ | 순서·정렬 | 제목·행 옆에 붙는 작은 메타 표시다. 제목 옆이면 제목과 세로 가운데 정렬한다 | — |
67
+ | 고정·스크롤 | — | — |
68
+ | 좁은 폭·큰 글자 | 여러 개는 줄바꿈(`wrap`)한다. 큰 글자에서는 높이가 글자에 맞춰 늘어난다(고정 높이 금지) | `tagRecipe.size.minHeight` 주석 |
69
+
70
+ ## 꼭 지킬 것
71
+
72
+ - 라벨은 i18n 키로 넣는다. 라벨 문구와 어떤 값에 어떤 tone을 줄지는 제품 소유, 색·모양은 HJM 소유다.
73
+ - 배치는 `layoutStyle`로만 한다. 색·radius·여백을 덮지 않는다.
74
+ - `onClick`/`onPress`를 붙여 누르는 Tag로 만들지 않는다. 누를 수 있어야 하면 Chip을 쓴다.
75
+ - 여러 Tag는 [Stack](stack.md) `axis="inline"`·`wrap`으로 나열한다.
76
+
77
+ ## 플랫폼 차이
78
+
79
+ | 항목 | Web | Native |
80
+ | --- | --- | --- |
81
+ | 접근 이름 | 보이는 글자 | 기본은 라벨, `accessibilityLabel`로 바꿀 수 있음 |
82
+ | 추가 style prop | `className`·`style`(HTML 속성) | `style`·`labelStyle`(deprecated) |
83
+ | ref | `forwardRef`(`span`) | 없음 |
84
+ | import 경로 | `/display` | `/data-display` |
@@ -0,0 +1,111 @@
1
+ # TagsInput
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [TagsInput](../../tags-input.md), `src/tags-input.ts`(`tagsInputRecipe`)
9
+ - 스토리북: `배포/컴포넌트/입력/태그 입력`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 사용자가 **자유 입력으로 여러 값을 모으는 필드**에 쓴다. 해시태그, 초대할 사람, 검색 필터처럼
14
+ 목록이 없거나, 후보가 있어도 목록 밖의 값을 만들 수 있는 경우다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 정해진 목록에서만 고름 | [Combobox](combobox.md), [Select](select.md) |
21
+ | 고정된 몇 개 선택지를 여러 개 켬 | [CheckboxGroup](checkbox-group.md), [Chip](chip.md) |
22
+ | 본문 중 `@사람` 언급 | [Mentions](mentions.md) |
23
+ | 값 하나만 입력 | [Field](field.md) |
24
+ | 정적 라벨 표시 | [Tag](tag.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `TagsInput` | 기본 | `@hjmds/react`, `/tags-input` | `@hjmds/react-native`, `/tags-input` |
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import type { TagsInputRejectionReason } from "@hjmds/design-contracts/components/tags-input";
37
+ import { TagsInput } from "@hjmds/react/tags-input";
38
+
39
+ // 상태 → i18n 키 상수 표. 키를 템플릿 문자열로 만들지 않는다.
40
+ const tagErrorKey: Record<TagsInputRejectionReason, string> = {
41
+ empty: "post.tagError.empty",
42
+ duplicate: "post.tagError.duplicate",
43
+ limit: "post.tagError.limit",
44
+ invalid: "post.tagError.invalid",
45
+ };
46
+
47
+ <TagsInput
48
+ label={t("post.tags")}
49
+ tags={tags}
50
+ onTagsChange={setTags}
51
+ policy={{ maxTags: 10 }}
52
+ onReject={(result) => setError(result.reason ? t(tagErrorKey[result.reason]) : undefined)}
53
+ composeRemoveLabel={(tag) => t("post.removeTag", { tag })}
54
+ />
55
+ ```
56
+
57
+ ```tsx
58
+ // Native
59
+ import { TagsInput } from "@hjmds/react-native/tags-input";
60
+
61
+ <TagsInput
62
+ label={t("post.tags")}
63
+ tags={tags}
64
+ onTagsChange={setTags}
65
+ composeRemoveLabel={(tag) => t("post.removeTag", { tag })}
66
+ />
67
+ ```
68
+
69
+ ## 축과 기본값
70
+
71
+ | prop | 값 | 기본값 | 설명 |
72
+ | --- | --- | --- | --- |
73
+ | `tags` + `onTagsChange` / `defaultTags` | `readonly string[]`, `(tags: readonly string[]) => void` | `defaultTags` `[]` | controlled 또는 uncontrolled. 콜백은 다음 태그 배열 전체를 받는다 |
74
+ | `policy.allowDuplicates` | `boolean` | 막음 | 중복 허용 여부 |
75
+ | `policy.maxTags` · `policy.isValid` | `number` · `(value: string) => boolean` | — | 거절되면 `onReject`가 결과를 받는다 |
76
+ | `onReject` | `(result: { accepted: boolean, value: string, reason?: "empty" \| "duplicate" \| "limit" \| "invalid" }) => void` | — | 사유 문장은 제품이 `reason`으로 고른다 |
77
+ | `onDraftChange` | `(draft: string) => void` | — | 입력 중 문자. 후보 필터링에 쓴다 |
78
+ | `composeRemoveLabel` | `(tag: string) => string` | — (필수) | 태그 삭제 버튼 이름 |
79
+ | `commitKeys` | `Enter` · `Comma` · `Space` · `Blur` | `["Enter"]` | Web만. Native는 Return 하나다 |
80
+ | `suggestions` · `suggestionsLabel` | 제품이 거른 후보(`id`·`label`·`value?`·`disabled?`)와 목록 이름 | — | 거르려면 `onDraftChange`로 입력 중 문자를 받는다 |
81
+ | `placeholder`·`description`·`disabled` | `string`·`string`·`boolean` | — | `disabled`는 라벨과 입력 틀을 `fieldRecipe.disabledOpacity`(0.6)로 흐리고 `description`은 그대로 둔다([Field](field.md)) |
82
+ | `layoutStyle` | `HjmCompositionStyleProp` | — | 필드 전체 배치. Web·Native 모두 |
83
+ | `style`(Native) | `StyleProp<ViewStyle>` | — | deprecated — `layoutStyle`. 개발 모드에서 한 번 경고하고 다음 major에서 제거된다 |
84
+
85
+ ## 배치
86
+
87
+ | 항목 | 값 | 근거 |
88
+ | --- | --- | --- |
89
+ | 크기 | 입력 틀 최소 높이 44(필드 틀과 같음), 모서리 `radius.md` 12. 입력 칸 최소 Web `8ch`, Native 80. 태그 칩 높이 28, 삭제 버튼 터치 영역 44(`control.minTouchTarget`). 후보 행 최소 높이 44(두 플랫폼, Native는 미게시(1.12.1 이후). 1.12.1 Native는 28) | `tagsInputRecipe`, `.hjm-tags-input__frame`·`__remove::after`·`__suggestion` |
90
+ | 간격 | 라벨과 틀 사이 Web `spacing.xxs` 4, Native `spacing.xs` 8. 틀 안 태그·입력 칸 사이 `spacing.xs` 8. 이웃 필드와의 간격은 폼이 정한다 | `.hjm-tags-input`, `react-native/src/tags-input.tsx` |
91
+ | 순서·정렬 | 라벨 → 입력 틀 → 후보 목록 → 설명. 폼 안에서 다른 [Field](field.md)와 같은 세로 줄에 두고 폭을 꽉 채운다 | `react/src/tags-input.tsx`, `react-native/src/tags-input.tsx` |
92
+ | 고정·스크롤 | 후보 목록은 틀 바로 아래 문서 흐름에 펼쳐진다(떠 있는 층이 아니다). Web은 최대 높이 12rem에서 스크롤 | `.hjm-tags-input__suggestions` |
93
+ | 좁은 폭·큰 글자 | 태그와 입력 칸이 넘치면 줄바꿈해 틀이 세로로 자란다. 태그 글자는 잘리지 않는다(`overflow-wrap: anywhere`) | `.hjm-tags-input__frame`·`__tag` |
94
+
95
+ ## 꼭 지킬 것
96
+
97
+ - `label`과 `composeRemoveLabel`은 필수다. 삭제 버튼 이름은 태그 글자를 넣은 i18n 문장으로 만든다.
98
+ - 거절 사유 문장("이미 있어요")은 제품이 `reason`으로 만든다. HJM은 판정만 한다.
99
+ - 한국어처럼 공백이 값의 일부인 도메인에서는 `Space`를 확정 키로 켜지 않는다.
100
+ - 후보 필터링·정렬은 제품이 한다. HJM은 받은 후보를 보여 주고 고른 값을 확정한다.
101
+ - 배치는 `layoutStyle`로 한다(Web `className`도 받는다). Native `style`은 deprecated다.
102
+
103
+ ## 플랫폼 차이
104
+
105
+ | 항목 | Web | Native |
106
+ | --- | --- | --- |
107
+ | 확정 키 | `commitKeys` | Return만(`commitKeys` 없음), 포커스 이탈로 확정하지 않음 |
108
+ | 빈 입력 Backspace | 첫 번째는 마지막 태그 선택, 두 번째에 삭제 | 해당 키 계약 없음, 태그 옆 삭제 버튼 |
109
+ | 후보 이동 | ArrowUp/Down(끝에서 순환) | 후보를 버튼으로 누름 |
110
+ | ref | `forwardRef`(`input`) | 없음 |
111
+ | 스타일 prop | `className`, `layoutStyle` | `layoutStyle`(`style`은 deprecated) |