@hjmds/design-contracts 1.12.0 → 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,112 @@
1
+ # ChatScreen
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screens.tsx`·`src/screen-flows.tsx`; 기존 개별 지침을 새 규격으로 통합. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
+ - 스토리북: `배포/화면/소통/채팅`
10
+
11
+ ## 언제 쓰나
12
+
13
+ DM·대화방처럼 헤더, 메시지 타임라인, 하단 작성창으로 이루어진 화면 한 장에 쓴다. `ScreenLayout`에
14
+ `composer`를 footer로 고정하고 기본 `scroll="content"`로 제품 타임라인이 스크롤을 갖게 한다.
15
+ 별도 보조 기능(supplemental)이라 `/screens` subpath로만 import 한다. 새 채팅 데이터 모델이나 전송 엔진은 없다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 메시지 한 개 | [ChatMessage](chat-message.md) |
22
+ | 부모/답글 댓글 화면 | [CommentThreadScreen](comment-thread-screen.md) |
23
+ | 작성창이 없는 일반 화면 | [ScreenLayout](screen-layout.md) |
24
+ | 알림 목록 화면 | [NotificationInboxScreen](notification-inbox-screen.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `ChatScreen` | 화면 조합 | `/screens` | `/screens` |
31
+ | `ChatMessage`, `MessageComposer`, `ScreenLayout` | 함께 쓰는 조각 | `/screens` | `/screens` |
32
+
33
+ 루트 barrel에는 없다. `/screens`는 optional native peer를 요구하지 않는다.
34
+
35
+ ## 최소 사용 예
36
+
37
+ ```tsx
38
+ // Web — host가 실제 남은 높이를 준다(예: height: 100dvh)
39
+ import { ChatMessage, ChatScreen, MessageComposer } from "@hjmds/react/screens";
40
+
41
+ <ChatScreen
42
+ title={t("chat.title")}
43
+ state={query.isPending ? { kind: "loading", title: t("chat.loading") } : { kind: "ready" }}
44
+ composer={<MessageComposer value={draft} label={t("chat.input")} sendLabel={t("chat.send")}
45
+ pending={sending} onValueChange={setDraft} onSend={send} />}
46
+ >
47
+ <Timeline messages={messages} renderItem={m => <ChatMessage {...toMessageProps(m)}>{m.text}</ChatMessage>} />
48
+ </ChatScreen>
49
+ ```
50
+
51
+ ```tsx
52
+ // Native — 키보드 adapter는 하나만
53
+ import { FlatList } from "react-native";
54
+ import { KeyboardAvoiding } from "@hjmds/react-native/keyboard";
55
+ import { ChatScreen, MessageComposer } from "@hjmds/react-native/screens";
56
+
57
+ <KeyboardAvoiding>
58
+ <ChatScreen title={t("chat.title")} composer={<MessageComposer value={draft} label={t("chat.input")}
59
+ sendLabel={t("chat.send")} onValueChange={setDraft} onSend={send} />}>
60
+ <FlatList data={messages} renderItem={renderMessage} />
61
+ </ChatScreen>
62
+ </KeyboardAvoiding>
63
+ ```
64
+
65
+ ### 제품이 공급하는 것
66
+
67
+ | 슬롯·prop | 내용 | 비고 |
68
+ | --- | --- | --- |
69
+ | `title`(필수) | 지역화한 대화 제목 | `header`를 주면 Web은 `aria-label`로만 쓴다 |
70
+ | `header` / `leading` / `actions` / `description` | 기존 navigation 헤더, 또는 뒤로 가기·도구 슬롯 | `header`를 주면 기본 헤더를 그리지 않는다 |
71
+ | `children` | 타임라인(가상화 목록 + `ChatMessage`) | 자동 스크롤·새 메시지 배지·이전 메시지 위치는 제품 |
72
+ | `composer`(필수) | `MessageComposer` 등 작성창 | `state.kind === "ready"`일 때만 보인다 |
73
+ | `state` / `stateAction` | `loading`·`empty`·`error`·`restricted` + 지역화 `title` | ready 외에는 본문을 교체한다 |
74
+ | `notice` | 새로고침 실패 같은 비차단 안내 | 초안 유지가 필요하면 `state` 대신 이것 |
75
+
76
+ ## 축과 기본값
77
+
78
+ | prop | 값 | 기본값 | 설명 |
79
+ | --- | --- | --- | --- |
80
+ | `composer` | `ReactNode` | 필수 | footer에 고정되는 작성창. `state.kind === "ready"`일 때만 보인다 |
81
+ | `scroll` | `"content"` · `"screen"` | `"content"` | 가상화 목록이 스크롤을 소유하므로 작은 예제 외에는 바꾸지 않는다 |
82
+ | `contentInset` | `"default"` · `"none"` | `"default"` | 이미 gutter를 주는 route 안에서는 `"none"`으로 이중 여백을 막는다 |
83
+ | `state` | `ScreenContentState`(`{ kind: "ready" }` 또는 `{ kind, title, description? }`) | `{ kind: "ready" }` | ready 외에는 본문을 교체하고 composer를 숨긴다 |
84
+ | `as` (Web) | `"main"` · `"section"` | `"main"` | 제품 shell이 이미 `main`이면 `"section"` |
85
+ | 나머지 | `ScreenLayout`과 같음(`footer` 제외) | — | [ScreenLayout](screen-layout.md) |
86
+
87
+ ## 배치
88
+
89
+ | 항목 | 값 | 근거 |
90
+ | --- | --- | --- |
91
+ | 크기 | 폭 최대 `layout.readingMaxWidth` 720, 가운데 정렬; 높이는 host가 준 남은 높이(Web `block-size: 100%`, Native `flex: 1`) | `screenPatternRecipe.maxWidth`, Web `.hjm-screen`, Native `ScreenLayout` |
92
+ | 간격 | 헤더·본문·footer padding `spacing.md` 16; 헤더 안 간격 Web 16 / Native `spacing.sm` 12; 메시지 사이 간격은 타임라인(제품) 소유 | `screenPatternRecipe.padding`·`itemGap`, Web `.hjm-screen__header` |
93
+ | 순서·정렬 | 헤더 → notice → 타임라인 → composer(footer) | `ScreenLayout` 렌더 순서 |
94
+ | 고정·스크롤 | 헤더·composer 고정, 타임라인이 content 스크롤; footer 위 테두리 1, Web은 하단 safe area만큼 padding을 늘린다 | Web `.hjm-screen__footer`, Native footer `borderTopWidth` |
95
+ | 좁은 폭·큰 글자 | 제목 열 최소 폭 `headerMinWidth` 120 × 글자 배율, 모자라면 actions가 다음 줄로 내려간다; Native safe area·키보드는 host | `screenPatternRecipe.headerMinWidth` |
96
+
97
+ ## 꼭 지킬 것
98
+
99
+ - 모든 문구·상태 title은 제품 i18n에서 넘긴다. ready 외 상태에 빈 title을 주면 `TypeError`가 난다.
100
+ - 전송 성공 판정과 초안 초기화는 서버 영수증을 받은 제품이 한다. `onSend`는 원문만 넘긴다.
101
+ - Native는 safe area와 탭/상단 navigation inset을 host가 먼저 뺀다. `KeyboardAvoiding`과 제품 keyboard adapter를
102
+ 동시에 감싸지 않는다.
103
+ - 가상화 목록을 `scroll="screen"` 안에 넣어 스크롤 컨테이너를 중첩하지 않는다.
104
+
105
+ ## 플랫폼 차이
106
+
107
+ | 항목 | Web | Native |
108
+ | --- | --- | --- |
109
+ | 배치 | `layoutStyle`, `className`(시각 override 금지) | `layoutStyle` |
110
+ | 높이 | host가 남은 높이 제공 | `flex: 1` |
111
+ | 스크롤 연결 | 없음 | `scrollRef`, `scrollProps`(`refreshControl`, `keyboardDismissMode` 등) — `scroll="screen"`이거나 상태 교체 중일 때만 ScrollView가 있다 |
112
+ | 테스트 id | 없음 | `testID` |
@@ -0,0 +1,104 @@
1
+ # CheckboxGroup
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: recipe `selectionGroupRecipe`·`selectionControlRecipe`(`src/component-recipes.ts`), behavior `checkboxGroup`
9
+ - 스토리북: `배포/컴포넌트/입력/체크박스 그룹`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 한 질문에 대한 여러 선택지 중 0개 이상을 고르게 할 때 쓴다. 관심사·알림 종류·필터 조건처럼
14
+ 각 선택지가 자기 줄과 설명을 가질 만한 목록이 여기에 속한다. 선택 상태는 `Set`으로 다룬다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 독립된 항목 하나 | [Checkbox](checkbox.md) |
21
+ | 하나만 고름 | [RadioGroup](radio-group.md) |
22
+ | 가로 버튼 줄로 짧은 옵션을 켜고 끔 | [ToggleGroup](toggle-group.md) ([경계](../../toggle-group.md)) |
23
+ | 약관 동의(필수가 제출 가능 여부를 정함) | [Agreement](agreement.md) ([이유](../../agreement.md)) |
24
+ | 칩 모양의 필터 줄 | [Chip](chip.md) `selectionMode="multiple"` |
25
+ | 목록 밖 값을 직접 입력 | [TagsInput](tags-input.md) |
26
+
27
+ ## 공개 이름과 import
28
+
29
+ | 이름 | 역할 | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `CheckboxGroup` | 기본 | `@hjmds/react`, `/selection` | `@hjmds/react-native`, `/inputs` |
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { CheckboxGroup } from "@hjmds/react/selection";
38
+
39
+ <CheckboxGroup
40
+ label={t("settings.notify.title")}
41
+ items={[
42
+ { id: "comment", label: t("settings.notify.comment") },
43
+ { id: "like", label: t("settings.notify.like"), description: t("settings.notify.likeHint") },
44
+ ]}
45
+ value={channels}
46
+ onValueChange={setChannels}
47
+ />
48
+ ```
49
+
50
+ ```tsx
51
+ // Native
52
+ import { CheckboxGroup } from "@hjmds/react-native/inputs";
53
+
54
+ <CheckboxGroup
55
+ label={t("settings.notify.title")}
56
+ items={items}
57
+ value={channels}
58
+ onValueChange={setChannels}
59
+ />
60
+ ```
61
+
62
+ ## 축과 기본값
63
+
64
+ | prop | 값 | 기본값 | 설명 |
65
+ | --- | --- | --- | --- |
66
+ | `items` | `readonly { id: Key; label: string; description?: string; disabled?: boolean }[]` | 필수 | `label`·`description`은 `string`이다 |
67
+ | `value` + `onValueChange` | `ReadonlySet<Key>` + `(value: ReadonlySet<Key>) => void` | — | 제어. `value`를 주면 `onValueChange`가 필수다(타입이 `defaultValue`와 함께 쓰지 못하게 막는다) |
68
+ | `defaultValue` | `ReadonlySet<Key>` | 빈 `Set` | 비제어. 항목에서 빠진 id는 선택에서 자동으로 지워진다 |
69
+ | `label` · `accessibilityLabel` | `string` | — | 둘 중 하나는 필수 |
70
+ | `orientation` | `vertical` · `horizontal` | `vertical` | — |
71
+ | `presentation` | `card` · `plain` · `grouped` | `card` | — |
72
+ | `size` | `medium` · `small` | `medium` | — |
73
+ | `renderLeading` | Web `(item, appearance: { selected, color: "currentColor", size }) => ReactNode` · Native `(item, props: { checked, selected, disabled, readOnly, color, size }) => ReactNode` | — | 항목 아이콘 |
74
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 그룹 외곽(Web `fieldset`) 배치 |
75
+
76
+ ## 배치
77
+
78
+ | 항목 | 값 | 근거 |
79
+ | --- | --- | --- |
80
+ | 크기 | 폭을 채우는 묶음. 각 행은 [Checkbox](checkbox.md) 규격(최소 높이 `medium` 56 · `small` 44, 표시 24 · 20) | `selectionControlRecipe.sizes`, `.hjm-choice` |
81
+ | 간격 | 그룹 이름(legend)↔항목 `spacing.xs` 8. 항목 사이 세로 `card` `spacing.xs` 8 · `plain` `spacing.xxs` 4 · `grouped` 0, 가로 `card` `spacing.md` 16 · `plain` `spacing.sm` 12 · `grouped` 0. 설명·오류는 `formSupportContract.gap` `spacing.xs` 8 | `selectionGroupRecipe.orientations`, `.hjm-checkbox-group*` |
82
+ | 순서·정렬 | 위→아래 [그룹 이름(semibold)] → [설명] → [항목들] → [오류]. 항목은 시작 쪽 정렬. `grouped`는 항목들을 테두리 1px·`radius.lg` 16 카드 하나에 붙여 담는다 | `selectionGroupRecipe.slots`, `.hjm-checkbox-group[data-presentation="grouped"]` |
83
+ | 고정·스크롤 | 고정 영역이 없다. 목록이 길어도 그룹 안에서 스크롤 영역을 만들지 않는다 | `.hjm-checkbox-group__items` |
84
+ | 좁은 폭·큰 글자 | `horizontal`은 줄바꿈된다(`flex-wrap: wrap`). 좁은 폭·큰 글자에서는 `vertical`을 쓴다 | `.hjm-checkbox-group[data-orientation="horizontal"]` |
85
+
86
+ ## 꼭 지킬 것
87
+
88
+ - `label` 또는 `accessibilityLabel` 중 하나는 반드시 준다. 둘 다 없거나 빈 문자열이면 실행 중 `TypeError`다.
89
+ - 항목 id는 비지 않고 중복되지 않아야 한다. 제어 `value`에 목록에 없는 id가 있으면 `RangeError`다.
90
+ 항목이 바뀌면 제어하는 쪽이 `value`를 먼저 정리한다.
91
+ - 문구는 모두 i18n 키로 넣는다. 각 항목의 아이콘은 `renderLeading(item, appearance)`로 그린다.
92
+ - 배치는 `layoutStyle`로 한다. Native의 `style`과 행 슬롯 스타일(`controlStyle`·`labelStyle` 등)은 deprecated —
93
+ `layoutStyle` 또는 `presentation`·`size`·`renderIndicator`를 쓴다([이관 문서](../../migration-native-legacy-removal.md)).
94
+
95
+ ## 플랫폼 차이
96
+
97
+ | 항목 | Web | Native |
98
+ | --- | --- | --- |
99
+ | `description`·`error` 타입 | `ReactNode` | `string` |
100
+ | 오류 표시 | `error` | `error` 또는 `invalid`(+`invalidLabel`) |
101
+ | 폼 제출 | `name`으로 체크박스마다 `value=id` 전송 | 없음 |
102
+ | 그룹 전체 비활성 | `disabled`(fieldset) | `disabled` |
103
+ | 필수·읽기 전용 안내 | `aria-required`·`aria-readonly` | `requiredLabel`·`readOnlyLabel`을 행 hint로 읽음 |
104
+ | 선택 표시 교체 | 없음 | `indicator="none"`, `renderIndicator(item, props)` |
@@ -0,0 +1,103 @@
1
+ # Checkbox
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: recipe `selectionControlRecipe`(`src/component-recipes.ts`), behavior `checkbox`
9
+ - 스토리북: `배포/컴포넌트/입력/체크박스`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 독립된 예/아니오 하나를 고르는 항목에 쓴다. "기억하기", 목록 전체 선택처럼 부분 선택(`mixed`)이
14
+ 필요한 상위 항목도 여기에 속한다. 값은 제출이나 저장 때 반영되는 선택이다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 같은 질문의 여러 선택지를 묶어 고름 | [CheckboxGroup](checkbox-group.md) |
21
+ | 하나만 고름 | [RadioGroup](radio-group.md) |
22
+ | 누르는 즉시 적용되는 설정 켜기·끄기 | [Switch](switch.md) |
23
+ | 약관·개인정보 동의(필수/선택 구분, 전체 동의) | [Agreement](agreement.md) |
24
+ | 필터 줄의 작은 선택 | [Chip](chip.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `Checkbox` | 기본 | `@hjmds/react`, `/selection` | `@hjmds/react-native`, `/inputs` |
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { Checkbox } from "@hjmds/react/selection";
37
+
38
+ <Checkbox
39
+ label={t("signup.rememberMe")}
40
+ checked={remember}
41
+ onCheckedChange={setRemember}
42
+ />
43
+ ```
44
+
45
+ ```tsx
46
+ // Native
47
+ import { Checkbox } from "@hjmds/react-native/inputs";
48
+
49
+ <Checkbox
50
+ label={t("signup.rememberMe")}
51
+ checked={remember}
52
+ onCheckedChange={setRemember}
53
+ />
54
+ ```
55
+
56
+ ## 축과 기본값
57
+
58
+ | prop | 값 | 기본값 | 설명 |
59
+ | --- | --- | --- | --- |
60
+ | `presentation` | `card` · `plain` · `grouped` | `card` | — |
61
+ | `size` | `medium` · `small` | `medium` | — |
62
+ | `checked` · `defaultChecked` | Web `boolean` · Native `boolean \| "mixed"` | `defaultChecked` `false` | `checked`를 주면 제어, 없으면 비제어다 |
63
+ | 부분 선택 | Web `indeterminate: boolean` · Native `checked="mixed"` | Web `false` | Native에서 `mixed`를 누르면 `true`가 된다 |
64
+ | `onCheckedChange` | `(checked: boolean) => void` | — | 두 renderer 모두 `boolean`만 넘긴다(`"mixed"`는 오지 않는다) |
65
+ | Web `onChange` | `(event: ChangeEvent<HTMLInputElement>) => void` | — | 원시 이벤트가 필요할 때만. 값은 `onCheckedChange`로 받는다 |
66
+ | `readOnly` | `boolean` | `false` | 값을 바꾸지 않고 포커스·읽기는 유지한다 |
67
+ | `renderLeading` | Web `(appearance: { selected, color: "currentColor", size }) => ReactNode` · Native `(props: { checked, selected, disabled, readOnly, color, size }) => ReactNode` | — | 라벨 앞 아이콘 |
68
+ | Native `renderIndicator` · `indicator` | `(props: { checked, selected, disabled, readOnly, color, size }) => ReactNode` · `"default" \| "none"` | `indicator` `"default"` | 체크 표시 교체·숨김 |
69
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 행(Web 바깥 `label`, Native 행) 배치 |
70
+
71
+ ## 배치
72
+
73
+ | 항목 | 값 | 근거 |
74
+ | --- | --- | --- |
75
+ | 크기 | 행 최소 높이 `medium` 56(`layout.rowHeight.singleLine`) · `small` 44(`control.minTouchTarget`). 표시 `medium` 24(`control.selectionIndicator`) · `small` 20, Native hitSlop 10 · 12 | `selectionControlRecipe.sizes`, `.hjm-choice` |
76
+ | 간격 | `card`·`grouped` 안쪽 `medium` 위아래 `spacing.sm` 12 · 좌우 `spacing.md` 16, `small` `spacing.xs` 8 · `spacing.sm` 12. 표시↔라벨 `medium` `spacing.sm` 12 · `small` `spacing.xs` 8. `plain`은 안쪽 여백 0. 라벨↔설명 `spacing.xxs` 4 | `selectionControlRecipe.sizes`·`presentations`, `.hjm-choice*` |
77
+ | 순서·정렬 | 시작 쪽부터 [체크 표시] → [leading] → [라벨 / 설명]. 표시는 첫 줄에 맞춰 위쪽 정렬. 여러 개면 [CheckboxGroup](checkbox-group.md)으로 묶는다. `card`는 `canvas` 배경·테두리 1px·`radius.md` 12 | `selectionControlRecipe.slots`, `.hjm-choice__indicator` |
78
+ | 고정·스크롤 | 고정 영역이 없다 | — |
79
+ | 좁은 폭·큰 글자 | 라벨·설명이 줄바꿈되고 행 높이가 늘어난다. 표시 크기는 그대로다 | `.hjm-choice__copy` |
80
+
81
+ ## 꼭 지킬 것
82
+
83
+ - `label`(필수)과 `description`은 i18n 키로 넣는다. Native는 둘 다 `string`만 받는다.
84
+ - 선택 아이콘을 바꾸려면 `renderLeading`(Native는 `renderIndicator`도)을 쓴다. 색·테두리를 직접 칠하지 않는다.
85
+ - 배치는 `layoutStyle`로 한다. Native의 `style`·`controlStyle`·`indicatorStyle`·`leadingStyle`·`contentStyle`·
86
+ `labelStyle`·`descriptionStyle`은 deprecated — `layoutStyle` 또는 `presentation`·`size`·`renderIndicator`를 쓴다
87
+ (개발 모드 1회 경고, 다음 major 제거. [이관 문서](../../migration-native-legacy-removal.md)).
88
+
89
+ ## 플랫폼 차이
90
+
91
+ | 항목 | Web | Native |
92
+ | --- | --- | --- |
93
+ | 부분 선택 | `indeterminate` | `checked`/`defaultChecked`에 `"mixed"` |
94
+ | `label`·`description` 타입 | `ReactNode` | `string` |
95
+ | 필수·오류 표시 | 없음(HTML `required`는 input에 전달) | `required`·`invalid` + `requiredLabel`·`invalidLabel`(접근성 hint로 합침) |
96
+ | 읽기 전용 안내 | `aria-readonly` | `readOnlyLabel`을 hint로 읽음 |
97
+ | 고정 leading 노드 | 없음 | `leading` |
98
+ | 원시 change 이벤트 | `onChange` | 없음 |
99
+
100
+ ## 함정
101
+
102
+ - Web은 `className`·`layoutStyle`이 바깥 `label`에, 나머지 HTML 속성(`style`, `name`, `onFocus` 등)은 숨은
103
+ `input`에 붙는다. 배치용 `style`을 넘기면 보이는 행이 아니라 input에 적용되므로 `layoutStyle`을 쓴다.
@@ -0,0 +1,104 @@
1
+ # Chip
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Tag와의 경계](../../tag.md#chip과의-경계), recipe `chipRecipe`(`src/component-recipes.ts`), behavior `chip`
9
+ - 스토리북: `배포/컴포넌트/입력/선택 칩`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 누를 수 있는 작은 pill이다. 필터 줄(여러 개 켬), 정렬·범위처럼 하나만 고르는 칩 줄, 추천 검색어처럼
14
+ 누르면 바로 행동하는 칩에 쓴다. 선택 상태는 항상 제품이 들고 있다(숨은 내부 상태가 없다).
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 누를 수 없는 분류·속성 라벨 | [Tag](tag.md) |
21
+ | 개수·상태 표시 | [Badge](badge.md) |
22
+ | 줄 단위 선택지와 설명 | [CheckboxGroup](checkbox-group.md), [RadioGroup](radio-group.md) |
23
+ | 같은 폭의 구간 전환 | [SegmentedControl](segmented-control.md) |
24
+ | 사용자가 값을 입력해 칩을 만듦 | [TagsInput](tags-input.md) |
25
+ | 일반 텍스트 행동 | [Button](button.md) |
26
+
27
+ ## 공개 이름과 import
28
+
29
+ | 이름 | 역할 | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `Chip` | 기본 | `@hjmds/react`, `/selection` | `@hjmds/react-native`, `/inputs` |
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { Chip } from "@hjmds/react/selection";
38
+
39
+ <Chip
40
+ label={t("feed.filter.photo")}
41
+ selectionMode="multiple"
42
+ selected={filters.has("photo")}
43
+ onSelectedChange={(next) => toggleFilter("photo", next)}
44
+ />
45
+ ```
46
+
47
+ ```tsx
48
+ // Native
49
+ import { Chip } from "@hjmds/react-native/inputs";
50
+
51
+ <Chip
52
+ label={t("feed.filter.photo")}
53
+ selectionMode="multiple"
54
+ selected={filters.has("photo")}
55
+ onPress={(next) => toggleFilter("photo", next)}
56
+ />
57
+ ```
58
+
59
+ ## 축과 기본값
60
+
61
+ | prop | 값 | 기본값 | 설명 |
62
+ | --- | --- | --- | --- |
63
+ | `selectionMode` | `action`(버튼) · `single`(radio 역할) · `multiple`(checkbox 역할) | `action` | `single`·`multiple`이면 `selected`가 필수이고, `action`이면 `selected`를 줄 수 없다 |
64
+ | `selected` | `boolean` | — | 제어 전용. Chip은 내부 선택 상태를 두지 않는다 |
65
+ | Web `onSelectedChange` | `(selected: boolean) => void` | — | 선택 칩에서 필수. 다음 상태(`!selected`)를 받는다 |
66
+ | Web `onPress` | `(event: MouseEvent<HTMLButtonElement>) => void` | — | 선택 사항. `event.preventDefault()`면 `onSelectedChange`를 부르지 않는다 |
67
+ | Native `onPress` | 행동 칩 `(event: GestureResponderEvent) => void` · 선택 칩 `(selected: boolean, event: GestureResponderEvent) => void` | — | 필수 |
68
+ | `size` | `small` · `medium` | `small` | — |
69
+ | `renderSelectionIndicator` | `(props: { selected: boolean; color: string; size: number }) => ReactNode` (Web `color`는 `"currentColor"`) | 체크 표시 | 선택되면 붙는 체크 표시를 바꾼다 |
70
+ | `leading` · `trailing` | `ReactNode` | — | 장식이다(접근성 트리에서 숨는다). 의미는 `label`에 담는다 |
71
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 칩 배치. Native `leadingStyle`·`indicatorStyle`·`trailingStyle`도 배치 key만 받는다 |
72
+
73
+ ## 배치
74
+
75
+ | 항목 | 값 | 근거 |
76
+ | --- | --- | --- |
77
+ | 크기 | 내용 폭. 높이 `small` 36 · `medium` 44(`control.chipHeight`), 모서리 `radius.full`. `small`은 위아래 4를 넓혀(Native hitSlop, Web `::after`) 터치 영역을 44에 맞춘다 | `chipRecipe.sizes`, `react-native/src/inputs.tsx` |
78
+ | 간격 | 좌우 여백 `small` `spacing.sm` 12 · `medium` `spacing.md` 16, 아이콘↔라벨 `small` `spacing.xxs` 4 · `medium` `spacing.xs` 8(두 플랫폼). 칩 사이는 부모가 정한다(`spacing.xs` 8 권장) | `chipRecipe.sizes`, `.hjm-chip` |
79
+ | 순서·정렬 | 필터 줄·태그 묶음으로 가로로 나란히 둔다. 안쪽은 [선택 표시] → [leading] → [라벨] → [trailing]. 선택 상태는 브랜드 테두리·글자색 | `chipRecipe.slots`, `.hjm-chip[data-selected]` |
80
+ | 고정·스크롤 | 고정 영역이 없다. 칩이 많으면 부모가 줄바꿈하거나 가로 스크롤 영역을 둔다. 검색 화면의 필터 칩 줄은 [SearchScreen](search-screen.md) `filtersOverflow="scroll"`이 가로 스크롤을 소유한다 | `SearchScreen` |
81
+ | 좁은 폭·큰 글자 | 라벨이 줄바꿈된다(`overflow-wrap: anywhere`). 높이는 최소값이라 늘어난다 | `.hjm-chip__label` |
82
+
83
+ ## 꼭 지킬 것
84
+
85
+ - `label`은 i18n 키로 넣는다. Native는 `string`만 받고, 다른 읽기 이름이 필요하면 `accessibilityLabel`을 준다.
86
+ - `single` 줄은 제품이 하나만 `selected`가 되도록 관리한다. Chip이 형제를 해제하지 않는다.
87
+ - 색·테두리를 덮지 않는다. 배치는 두 renderer 모두 `layoutStyle`로 한다. Native `labelStyle`은 deprecated —
88
+ `size`·`selected`로 글자 모양을 정한다([이관 문서](../../migration-native-legacy-removal.md)).
89
+
90
+ ## 플랫폼 차이
91
+
92
+ | 항목 | Web | Native |
93
+ | --- | --- | --- |
94
+ | 행동 칩 이벤트 | `onPress(event)`(선택 사항) | `onPress(event)`(필수) |
95
+ | 선택 칩 이벤트 | `onSelectedChange(next)`, `onPress(event)`는 선택 사항 | `onPress(next, event)` 하나 |
96
+ | 선택 변경 취소 | `onPress`에서 `event.preventDefault()` | 없음 |
97
+ | `label` 타입 | `ReactNode` | `string` |
98
+ | 배치 | `layoutStyle` | `layoutStyle`, 슬롯별 `leadingStyle`·`indicatorStyle`·`trailingStyle`(배치 key만) |
99
+ | 선택 표시 위치 | 표시 → leading → 라벨 | leading → 표시 → 라벨 |
100
+
101
+ ## 함정
102
+
103
+ - 두 renderer의 선택 콜백 이름이 다르다. Web 코드를 옮기면서 `onSelectedChange`를 Native에 넘기면
104
+ 타입 오류가 나고, Native의 `onPress(next, event)`를 Web에 넘기면 첫 인자가 이벤트다.
@@ -0,0 +1,111 @@
1
+ # CodeBlock
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Code block](../../code-block.md), contract `src/code-block.ts`
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/코드 블록`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 코드·명령·설정 조각을 읽기 전용으로 보여 주고 사용자가 선택·복사하게 할 때 쓴다.
14
+ 편집기나 HTML 실행기가 아니며 강조 색은 표현만 바꾸고 원문을 바꾸지 않는다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 사용자가 코드를 고침 | [TextArea](text-area.md) |
21
+ | 문장 안의 짧은 강조·서식 | [Text](text.md), [TextFormat](text-format.md) |
22
+
23
+ ## 공개 이름과 import
24
+
25
+ | 이름 | 역할 | Web | Native |
26
+ | --- | --- | --- | --- |
27
+ | `CodeBlock` | 기본(별도 보조 기능, supplemental) | `/code-block` | `/code-block` |
28
+ | `ClipboardButton` | 보조(Web 복사 버튼, `copyAction` 슬롯에 넣는다) | `@hjmds/react`, `/clipboard` | 없음 |
29
+
30
+ `CodeBlock`은 root에서 export되지 않고 granular subpath로만 import 된다. 추가 peer는 없다.
31
+ 구문 분석기·하이라이터·클립보드 엔진은 포함하지 않는다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { CodeBlock } from "@hjmds/react/code-block";
38
+ import { ClipboardButton } from "@hjmds/react/clipboard";
39
+
40
+ <CodeBlock
41
+ code={snippet}
42
+ label={t("docs.installSnippet")}
43
+ language="bash"
44
+ copyAction={
45
+ <ClipboardButton
46
+ tone="secondary"
47
+ size="small"
48
+ value={snippet}
49
+ labels={{ idle: t("common.copy"), copied: t("common.copied") }}
50
+ onCopyError={showCopyError}
51
+ />
52
+ }
53
+ />
54
+ ```
55
+
56
+ ```tsx
57
+ // Native
58
+ import { CodeBlock } from "@hjmds/react-native/code-block";
59
+
60
+ <CodeBlock code={snippet} label={t("docs.installSnippet")} language="bash" />
61
+ ```
62
+
63
+ ## 축과 기본값
64
+
65
+ | prop | 값 | 기본값 | 설명 |
66
+ | --- | --- | --- | --- |
67
+ | `code` | `string` | 필수 | — |
68
+ | `label` | `string` | 필수 | 비면 `TypeError` |
69
+ | `language` | `string` | — | 헤더에 표시, 없으면 `label` 표시 |
70
+ | `wrap` | `boolean` | `false` | 긴 줄은 가로 스크롤한다. `true`면 줄바꿈한다 |
71
+ | `tokens` | `readonly { text: string; tone?: "plain" \| "keyword" \| "string" \| "comment" \| "number" }[]` | — | 없으면 전체가 plain 한 덩어리다 |
72
+ | `copyAction` | `ReactNode` | — | 머리 줄 끝 쪽 슬롯 |
73
+ | Web `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 `section` 배치. Native는 없다 |
74
+ | `ClipboardButton` `value` · `labels` | `string` · `{ idle: ReactNode; copied: ReactNode }` | 필수 | 복사할 원문과 두 상태 문구 |
75
+ | `ClipboardButton` `onCopy` · `onCopyError` | `(value: string) => void` · `(error: unknown) => void` | — | 실패(권한 거부 등)는 `onCopyError`로만 알 수 있다 |
76
+ | `ClipboardButton` `feedbackDuration` | ms | 2000 | "복사했어요" 상태 유지 시간 |
77
+ | `ClipboardButton` `tone` · `size` | [Button](button.md)과 같다 | `secondary` · `medium`(1.12.1은 `primary`) | 코드 블록 안에서는 `small`로 낮춘다 |
78
+
79
+ ## 배치
80
+
81
+ | 항목 | 값 | 근거 |
82
+ | --- | --- | --- |
83
+ | 크기 | 부모 폭을 채우고 높이는 코드 줄 수가 정한다. 모서리 `radius.lg` 16, 배경 `surface-alt` | `react/src/code-block.tsx`, `react-native/src/code-block.tsx` |
84
+ | 간격 | 머리 줄 안쪽 `spacing.md` 16, 언어 이름↔복사 버튼 `spacing.sm` 12, 코드 영역 안쪽 `spacing.md` 16 | 같은 파일 |
85
+ | 순서·정렬 | 위→아래 [언어(또는 label) — 시작 쪽 · `copyAction` — 끝 쪽] → [코드]. 본문 문단 사이에 블록으로 둔다 | 같은 파일 |
86
+ | 고정·스크롤 | `wrap` 기본 `false`면 코드 영역만 가로 스크롤한다(Web `pre` `overflow-x: auto`, Native `ScrollView horizontal`). 세로 스크롤 영역은 만들지 않는다 | 같은 파일 |
87
+ | 좁은 폭·큰 글자 | 좁은 폭·큰 글자에서 읽기를 우선하면 `wrap`을 켠다(Web `pre-wrap` + `overflow-wrap: anywhere`) | `react/src/code-block.tsx` |
88
+
89
+ ## 꼭 지킬 것
90
+
91
+ - `tokens`의 `text`를 이어 붙인 결과는 `code`와 공백까지 같아야 한다. 다르면 렌더 중 `TypeError`를 던진다.
92
+ - 토큰은 제품이 고른 하이라이터로 만든다(제품 소유). 토큰 색은 HJM semantic 색이 정한다.
93
+ - `label`은 i18n 키로 넣는다. Native 접근성 이름에는 `label`과 원문이 함께 들어간다.
94
+ - 복사 버튼과 실패 응답은 제품이 `copyAction`으로 공급한다. `onCopyError`에서 "직접 선택해 복사" 같은 안내를 보인다.
95
+ - 복사 버튼은 화면의 주 행동이 아니다. 기본 tone은 `secondary`다(미게시(1.12.1 이후). 1.12.1은 Button 기본 `primary`를
96
+ 물려받으므로 `tone="secondary"`를 명시한다). 코드 블록 안에서는 `size="small"`을 준다([Button](button.md)의 한 화면 primary 하나 규칙).
97
+ - 배치는 Web `layoutStyle`로 한다. Native CodeBlock은 배치 prop이 없으므로 감싸는 레이아웃(`Stack` 등)이 배치한다.
98
+
99
+ ## 플랫폼 차이
100
+
101
+ | 항목 | Web | Native |
102
+ | --- | --- | --- |
103
+ | 원문 요소 | 포커스 가능한 `pre`(`tabIndex=0`) | 선택 가능한 `Text`, `wrap=false`면 가로 `ScrollView` |
104
+ | 복사 | `ClipboardButton` 슬롯 | 시스템 텍스트 선택, 또는 제품의 복사 버튼 슬롯 |
105
+ | 글꼴 | stylesheet 기본 | iOS `Menlo`, 그 외 `monospace` |
106
+
107
+ ## 함정
108
+
109
+ - 복사는 화면의 주 행동과 경쟁하지 않도록 `ClipboardButton tone="secondary" size="small"`을 쓴다. Web 예제도 이 구성을 따른다.
110
+ - `ClipboardButton`은 `navigator.clipboard`가 거부되면 상태를 바꾸지 않고 `onCopyError`만 부른다. 이 콜백을 비워 두면
111
+ 사용자는 실패를 알 수 없다.
@@ -0,0 +1,112 @@
1
+ # Collapsible
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Collapsible](../../collapsible.md), `FolderPreview`는 [Folder preview](../../folder-preview.md), recipe `collapsibleRecipe`(`src/collapsible.ts`)
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/접기와 펼치기` · `배포/컴포넌트/데이터 표시/폴더 미리보기`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 이웃 없이 혼자 접었다 펴는 한 덩어리에 쓴다. "더 보기", 필터 패널, 접히는 본문이 여기에 속한다.
14
+ 닫히면 내용은 트리에서 빠진다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 서로 연결된 여러 접이식 항목 | [Accordion](accordion.md) |
21
+ | 같은 자리의 보기 전환 | [Tabs](tabs.md) |
22
+ | 화면 위에 떠서 열리는 내용 | [Popover](popover.md), [Sheet](sheet.md) |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `Collapsible` | 기본 | `@hjmds/react`, `/collapsible` | `@hjmds/react-native`, `/collapsible` |
29
+ | `FolderPreview` | 확장(미리보기 표지가 달린 묶음, optional-extension) | `/folder-preview` | `/folder-preview` |
30
+
31
+ `FolderPreview`는 granular subpath로만 import 된다. 추가 peer는 없다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { Collapsible } from "@hjmds/react/collapsible";
38
+
39
+ <Collapsible trigger={t("filters.more")} defaultOpen={false}>
40
+ <FilterFields />
41
+ </Collapsible>
42
+ ```
43
+
44
+ ```tsx
45
+ // Native
46
+ import { Collapsible } from "@hjmds/react-native/collapsible";
47
+
48
+ <Collapsible trigger={t("post.showMore")} open={expanded} onOpenChange={setExpanded}>
49
+ <PostBody />
50
+ </Collapsible>
51
+ ```
52
+
53
+ `FolderPreview`는 Web·Native가 같은 props다(Web만 `layoutStyle`을 더 받는다).
54
+
55
+ ```tsx
56
+ // Native
57
+ import { FolderPreview } from "@hjmds/react-native/folder-preview";
58
+
59
+ <FolderPreview
60
+ label={t("album.folderLabel", { count: photos.length })}
61
+ open={open}
62
+ onOpenChange={setOpen}
63
+ previews={photos.slice(0, 3).map((photo) => <PhotoThumb key={photo.id} photo={photo} />)}
64
+ >
65
+ <PhotoList photos={photos} />
66
+ </FolderPreview>
67
+ ```
68
+
69
+ ## 축과 기본값
70
+
71
+ | prop | 값 | 기본값 | 설명 |
72
+ | --- | --- | --- | --- |
73
+ | `open` + `onOpenChange` | `boolean` + `(open: boolean) => void` | — | 제어. `open`만 주고 `onOpenChange`를 빼면 `TypeError`를 던진다 |
74
+ | `defaultOpen` · `onOpenChange` | `boolean` · `(open: boolean) => void` | `defaultOpen` `false` | 비제어. `open`과 `defaultOpen`을 함께 주면 `TypeError`를 던진다 |
75
+ | `trigger` | `ReactNode` | 필수 | 버튼 문구. Native는 문자열이면 HJM `Text`로 감싼다 |
76
+ | `disabled` | `boolean` | `false` | trigger 옆의 ▸/▾ 표시는 HJM이 그리며 장식으로 숨겨진다 |
77
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 배치 |
78
+ | `FolderPreview` `open` · `onOpenChange` | `boolean` · `(open: boolean) => void` | 필수(항상 controlled) | — |
79
+ | `FolderPreview` `previews` · `label` | `readonly ReactNode[]` · `string` | 필수 | `previews`는 앞의 세 개만 그리고, `label`이 비면 `TypeError`를 던진다 |
80
+ | `FolderPreview` `layoutStyle` | margin·width·flex·`alignSelf` | — | Web만 |
81
+
82
+ ## 배치
83
+
84
+ | 항목 | 값 | 근거 |
85
+ | --- | --- | --- |
86
+ | 크기 | 트리거는 폭을 꽉 채운다. 트리거 최소 높이 44(`control.minTouchTarget`, 두 플랫폼. Native는 미게시(1.12.1 이후)), Web 위아래 `spacing.xs` 8. `FolderPreview` 표지는 최대 260×160 고정 그림 | `.hjm-collapsible__trigger`, `react-native/src/collapsible.tsx`, `react/src/folder-preview.tsx` |
87
+ | 간격 | 트리거↔내용, 트리거 안 문구↔표시 `spacing.xs` 8(Web 트리거 안은 `spacing.sm` 12) | `collapsibleRecipe.gap`, `.hjm-collapsible__trigger` |
88
+ | 순서·정렬 | 위→아래 [트리거: 문구 시작 쪽 · ▸/▾ 끝 쪽] → [내용(열렸을 때만)]. `FolderPreview`는 트리거 안에 [표지 그림(가운데)] → [label]이 쌓인다 | `react/src/collapsible.tsx`, `react/src/folder-preview.tsx` |
89
+ | 고정·스크롤 | 고정 영역이 없다. 닫히면 내용이 트리에서 빠져 아래 내용이 올라온다 | `react-native/src/collapsible.tsx` 주석 |
90
+ | 좁은 폭·큰 글자 | 트리거 문구·내용이 줄바꿈된다(`overflow-wrap: anywhere`). `FolderPreview` 표지는 폭이 260보다 좁으면 줄어든다(`maxWidth: 260`) | `.hjm-collapsible__content`, `folder-preview.tsx` |
91
+
92
+ ## 꼭 지킬 것
93
+
94
+ - `trigger`는 무엇이 열리는지 알 수 있는 i18n 문구로 넣는다. trigger 안에 다른 버튼·링크를 넣지 않는다(trigger 자체가 버튼이다).
95
+ - 닫힌 내용은 unmount된다. 닫혀도 유지해야 할 입력 상태는 바깥에서 들고 있는다.
96
+ - `FolderPreview`의 `previews`는 장식이다(접근성·터치에서 숨김). 항목 이름과 행동은 펼친 `children`에 다시 둔다.
97
+ 미리보기 이미지·라벨 문구는 제품 소유다.
98
+ - 배치는 `layoutStyle`로 한다. Native `style`은 deprecated(개발 모드 1회 경고, 다음 major 제거)다.
99
+
100
+ ## 플랫폼 차이
101
+
102
+ | 항목 | Web | Native |
103
+ | --- | --- | --- |
104
+ | 배치 | `layoutStyle`(`FolderPreview`도) | `layoutStyle`. `style`은 deprecated — `layoutStyle`을 쓴다. `FolderPreview`는 배치 prop이 없다 |
105
+ | 열린 영역 관계 | `aria-controls` + `role="region"` | 중첩 구조, `accessibilityState.expanded` |
106
+ | 문자열 trigger | 그대로 버튼 내용 | HJM `Text`로 감싼다 |
107
+ | `FolderPreview` 모션 | CSS transform, reduced motion이면 없음 | `ContentTransition`(`scale`) |
108
+
109
+ ## 함정
110
+
111
+ - 현재 랜딩 스토리(Web·Native `Landing.stories.tsx`)는 FAQ 여러 항목을 `Collapsible` 반복으로 그린다. 서로 연결된
112
+ 여러 항목은 [Accordion](accordion.md)이 규칙이다(한 번에 하나 펼침·키보드 이동·heading 위계를 Accordion이 소유한다).