@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,86 @@
1
+ # ColorPicker
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [ColorPicker — Web sRGB 색상 입력](../../color-picker.md), recipe `colorPickerRecipe`(`src/color-picker.ts`)
9
+ - 스토리북: `배포/컴포넌트/입력/색상 선택기`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 사용자가 콘텐츠 색(라벨 색, 태그 색, 테마 편집기의 사용자 값 등)을 sRGB HEX로 고르는 폼 입력에 쓴다.
14
+ 브라우저 색상 선택기, HEX 텍스트 입력, 선택적 불투명도 슬라이더, 프리셋 견본을 한 fieldset으로 묶는다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 정해진 몇 가지 색 중 하나만 고름 | [RadioGroup](radio-group.md), [SegmentedControl](segmented-control.md) |
21
+ | 숫자 하나를 범위에서 고름 | [Slider](slider.md) |
22
+ | 제품 브랜드·테마 색을 화면마다 바꾸기 | 컴포넌트가 아니다. 제품 테마 토큰(`brandPalette`)으로 한다 |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `ColorPicker` | 기본 | `/color-picker` | 없음 |
29
+ | `ColorPickerLabels`(타입) | 보조(문구 묶음) | `/color-picker` | 없음 |
30
+
31
+ 루트 barrel에는 없다. React Native 구현은 없다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { ColorPicker } from "@hjmds/react/color-picker";
38
+
39
+ <ColorPicker
40
+ label={t("label.color")}
41
+ labels={{
42
+ color: t("color.choose"), hex: t("color.hex"),
43
+ opacity: t("color.opacity"), invalid: t("color.invalid"),
44
+ }}
45
+ value={color}
46
+ onValueChange={setColor}
47
+ presets={["#b94627", "#338844"]}
48
+ />
49
+ ```
50
+
51
+ Native: 없음. 제품의 색 선택 화면이나 플랫폼 피커를 쓴다.
52
+
53
+ ## 축과 기본값
54
+
55
+ | prop | 값 | 기본값 | 설명 |
56
+ | --- | --- | --- | --- |
57
+ | `value` + `onValueChange` | 소문자 `#rrggbb`(`alpha`면 `#rrggbbaa`) + `(value: string) => void` | 필수(controlled 전용) | 정규화한 값이 바뀔 때만 부른다 |
58
+ | `label` | `string` | 필수 | fieldset legend |
59
+ | `labels` | `{ color: string; hex: string; opacity: string; invalid: string }` | 필수 | 견본 입력·HEX 입력·불투명도·오류 문구 |
60
+ | `alpha` | `boolean` | `false` | 켜면 0–100% 불투명도 슬라이더가 생긴다. 3·6자리 HEX를 치면 현재 불투명도를 유지한다 |
61
+ | `disabled` | `boolean` | `false` | fieldset으로 모든 입력과 프리셋을 막는다 |
62
+ | `presets` | `readonly string[]`(HEX) | `[]` | 정규화 후 중복을 없앤다. 선택된 견본은 `aria-pressed` |
63
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 `fieldset` 배치 |
64
+ | HEX 입력 | — | — | Enter·blur에 확정, Escape는 마지막 `value`로 되돌린다. 잘못된 입력은 `labels.invalid`를 alert로 보이고 외부 값은 바꾸지 않는다 |
65
+
66
+ ## 배치
67
+
68
+ | 항목 | 값 | 근거 |
69
+ | --- | --- | --- |
70
+ | 크기 | 부모 폭을 채우는 fieldset(테두리 1px, `radius.md` 12). 입력·프리셋 버튼 최소 높이 44(`minTargetSize`), 색 견본 입력 폭 3rem(48), HEX 입력 기준 폭 10rem(160)에서 늘어난다. 미리보기 띠 높이 1.5rem(24) | `colorPickerRecipe.minTargetSize`, `.hjm-color-picker*` |
71
+ | 간격 | 안쪽 1rem(16, `spacing.md`와 같은 값), 색 견본↔HEX 0.75rem(12), 불투명도·미리보기·프리셋 위 0.75rem(12), 프리셋 사이 0.5rem(8). CSS가 spacing 변수 대신 rem 값을 쓴다 | `.hjm-color-picker*` |
72
+ | 순서·정렬 | 위→아래 [legend] → [색 견본 입력][HEX 입력] 한 줄 → [오류(alert)] → [불투명도(`alpha`일 때)] → [미리보기] → [프리셋 줄]. 설정 폼 안에 블록으로 둔다 | `react/src/color-picker.tsx` |
73
+ | 고정·스크롤 | 고정 영역이 없다 | — |
74
+ | 좁은 폭·큰 글자 | 견본·HEX 줄과 프리셋 줄이 줄바꿈된다(`flex-wrap: wrap`). 문구는 `overflow-wrap: anywhere` | `.hjm-color-picker__row`, `.hjm-color-picker__presets` |
75
+
76
+ ## 꼭 지킬 것
77
+
78
+ - `label`과 `labels`의 네 문구는 모두 비어 있지 않아야 한다. 하나라도 비면 렌더 중 `TypeError`가 난다.
79
+ - `value`와 `presets`는 유효한 HEX여야 한다. `alpha={false}`인데 불투명하지 않은 8자리 값을 주면 투명도를 버리지 않고
80
+ `TypeError`를 던진다. 저장된 값을 넘기기 전에 제품 어댑터에서 형식을 맞춘다.
81
+ - 고른 색을 본문·배경에 쓸 때의 대비 검사는 제품이 한다. 견본 색은 콘텐츠 데이터이며 HJM 토큰이 아니다.
82
+ - 배치는 `layoutStyle`(바깥 `fieldset`)로 한다. `className`·`style` prop은 없다.
83
+
84
+ ## 함정
85
+
86
+ - 브라우저 색상 팝업은 RGB만 고른다. 팝업 모양과 동작은 OS·브라우저 소유이며 HJM 검증 대상이 아니다.
@@ -0,0 +1,137 @@
1
+ # Combobox
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: recipe `comboboxRecipe`(`src/component-recipes.ts`), behavior `combobox`
9
+ - 스토리북: `배포/컴포넌트/입력/검색형 선택`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 주어진 목록에서 하나를 고르는데 목록이 길어 입력으로 좁혀야 할 때 쓴다. 나라·도시·카테고리 고르기가
14
+ 여기에 속한다. 입력창이 곧 검색어이고, 확정된 값은 목록 항목 하나다. Native는 서버 검색 결과
15
+ (`filtering="external"`)도 받는다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 항목이 짧아 입력 없이 고름 | [Select](select.md) |
22
+ | 목록 밖 값을 만들거나 여러 개를 고름 | [TagsInput](tags-input.md) ([경계](../../tags-input.md)) |
23
+ | 결과가 화면 전체를 차지하는 검색 | [SearchField](search-field.md) |
24
+ | 명령 실행 팔레트(Web) | [CommandPalette](command-palette.md) |
25
+ | 본문 중간의 `@` 언급 | [Mentions](mentions.md) |
26
+
27
+ ## 공개 이름과 import
28
+
29
+ | 이름 | 역할 | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `Combobox` | 기본 | `@hjmds/react`, `/forms` | `@hjmds/react-native`, `/forms` |
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { Combobox } from "@hjmds/react/forms";
38
+
39
+ <Combobox
40
+ label={t("profile.city")}
41
+ items={cities.map((c) => ({ value: c.id, label: c.name }))}
42
+ value={cityId}
43
+ onValueChange={setCityId}
44
+ emptyMessage={t("profile.city.empty")}
45
+ loadingMessage={t("common.loading")}
46
+ selectionRequiredMessage={t("profile.city.required")}
47
+ />
48
+ ```
49
+
50
+ ```tsx
51
+ // Native
52
+ import { Combobox } from "@hjmds/react-native/forms";
53
+
54
+ <Combobox
55
+ label={t("profile.city")}
56
+ items={cities.map((c) => ({ id: c.id, label: c.name, textValue: c.name }))}
57
+ selectedKey={cityId}
58
+ onSelectionChange={setCityId}
59
+ emptyMessage={t("profile.city.empty")}
60
+ loadingMessage={t("common.loading")}
61
+ clearLabel={t("common.clear")}
62
+ dismissLabel={t("common.close")}
63
+ />
64
+ ```
65
+
66
+ ## 축과 기본값
67
+
68
+ | prop | 값 | 기본값 | 설명 |
69
+ | --- | --- | --- | --- |
70
+ | Web `items` | `readonly { value: string; label: string; keywords?: readonly string[]; disabled?: boolean }[]` | 필수 | 라벨과 `keywords`로 부분 일치 필터링한다 |
71
+ | Web `value` · `defaultValue` + `onValueChange` | `string` + `(value: string) => void` | `""` | 확정 값. 입력을 고치면 `""`로 지워진다 |
72
+ | Native `items` · `sections` · `source` | `readonly { id: Key; label: string; textValue: string; description?: string; disabled?: boolean }[]` · 구획 목록 · `{ items } \| { sections }` | — | 정확히 하나만 준다 |
73
+ | Native `selectedKey` · `defaultSelectedKey` + `onSelectionChange` | `Key \| null` + `(key: Key \| null) => void` | `null` | 확정 값. 목록 밖 키는 `selectedItem` 스냅샷이 필요하다 |
74
+ | Native `onCommit` · `onCommitAfterDismiss` | `(key: Key \| null, reason: "selection" \| "clear") => void` · `(key: Key, reason: "selection") => void \| Promise<void>` | — | 시트가 닫힌 뒤 할 일은 `onCommitAfterDismiss`에서 한다 |
75
+ | `inputValue` · `defaultInputValue` + `onInputValueChange` | `string` + `(value: string) => void` | 선택된 항목의 라벨 | 입력어를 따로 제어할 수 있다 |
76
+ | `open` · `defaultOpen` + `onOpenChange` | `boolean` + `(open: boolean, reason) => void` | `defaultOpen` `false` | reason: Web `"focus" \| "input" \| "keyboard" \| "selection" \| "escape" \| "blur"`, Native `"trigger" \| "keyboard" \| "selection" \| "escape" \| "outside" \| "blur" \| "programmatic"` |
77
+ | `size` | `medium` · `large` | `medium` | — |
78
+ | `density` | `comfortable` · `compact` | `comfortable` | — |
79
+ | `openOnFocus` | `boolean` | `true` | — |
80
+ | Native `filtering` | `"local"`(`label`·`textValue`) · `"external"` | `"local"` | `external`이면 제품이 `items`를 걸러 넘긴다 |
81
+ | Native `asyncState` | `{ status: "idle" }` · `{ status: "loading" \| "loadingMore" \| "empty" \| "error"; message: string }` | — | 결과 시트의 상태 문구. `error`면 `onRetry: () => void`·`retryLabel`로 다시 시도 버튼이 생긴다 |
82
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 필드 바깥 배치 |
83
+
84
+ ## 배치
85
+
86
+ | 항목 | 값 | 근거 |
87
+ | --- | --- | --- |
88
+ | 크기 | 입력 높이 `medium` 44(`fieldFrameContract`) · `large` 52(`control.buttonHeight.large`), 폭은 폼 열을 채운다. Web 목록 최대 높이 22.5rem(360, recipe `popover.maxHeight`), 선택지 최소 높이 `compact` 44 · `comfortable` 56(3.5rem). Native 시트 최대 높이 화면의 75% | `selectRecipe.sizes`·`popover`, `.hjm-combobox__listbox`·`__option`, `react-native/src/forms.tsx` |
89
+ | 간격 | 라벨·설명·오류는 [Field](field.md) 규칙(`formSupportContract.gap` `spacing.xs` 8). Web 목록 안쪽 `spacing.xs` 8(`comboboxRecipe.popover.padding` = Select와 같은 표면. 2026-10-06까지 4, 1.12.1 이후 미게시), 선택지 안쪽 위아래 `spacing.xs` 8 · 좌우 `spacing.sm` 12. Native 시트 안쪽 `spacing.md` 16, 아래는 `spacing.md` + 하단 안전 영역, 요소 사이 `spacing.sm` 12 | `.hjm-combobox__option`, `react-native/src/forms.tsx` |
90
+ | 순서·정렬 | 폼 안에서 다른 입력과 같은 열에 둔다. Web은 입력 바로 아래(공간이 없으면 위, `data-placement`)에 목록이 붙는다. Native는 화면 아래에서 시트가 올라오고 [제목·닫기] → [상태 문구 또는 선택지 목록] 순서다 | `react/src/advanced-forms.tsx`(`AnchoredPortal`), `CollectionSheetHeader` |
91
+ | 고정·스크롤 | 목록은 그 안에서 스크롤한다. Web 목록은 `position: fixed`(z-index `layer.dropdown` 400)로 떠서 화면 스크롤과 무관하다. Native 시트는 배경막(`backdrop.modal`)과 함께 모달로 뜬다 | `.hjm-combobox__listbox`, `react-native/src/forms.tsx` |
92
+ | 좁은 폭·큰 글자 | 선택지 문구는 줄바꿈되고(`overflow-wrap: anywhere`) 높이가 늘어난다. 좁은 폭에서도 Web 목록은 입력에 붙는다 | `.hjm-combobox__option` |
93
+
94
+ ```text
95
+ Web Native
96
+ ┌──────────────────────────┐ ┌──────────────────────────┐
97
+ │ 라벨 │ │ 라벨 │
98
+ │ [ 입력어 _____________ ▾ ]│ │ [ 입력어 _____________ ▾ ]│
99
+ │ ┌──────────────────────┐ │ │ ▒▒▒▒ 배경막 ▒▒▒▒▒▒▒▒▒▒▒▒ │
100
+ │ │ 선택지 1 (44/56) │ │ ← fixed │ ┌──────────────────────┐ │
101
+ │ │ 선택지 2 │ │ 스크롤 │ │ 제목 [닫기]│ │
102
+ │ └──────────────────────┘ │ │ │ 선택지 목록(스크롤) │ │ ← 최대 75%
103
+ │ 설명·오류 │ │ │ ░ 하단 안전 영역 ░ │ │
104
+ └──────────────────────────┘ └─┴──────────────────────┴─┘
105
+ ```
106
+
107
+ ## 꼭 지킬 것
108
+
109
+ - 상태 문구는 필수이고 i18n 키로 넣는다. Web은 `emptyMessage`·`loadingMessage`·`selectionRequiredMessage`,
110
+ Native는 `emptyMessage`·`loadingMessage`·`clearLabel`·`dismissLabel`. Native는 빈 문자열이면 `TypeError`다.
111
+ - 선택 값은 목록에 있는 항목이어야 한다. Web은 없는 값이나 `disabled` 항목이면 `RangeError`, Native는
112
+ 목록에 없는 `selectedKey`에 `selectedItem` 스냅샷이 없으면 `RangeError`다.
113
+ - Native는 `items`·`sections`·`source` 중 정확히 하나만 준다.
114
+ - 입력을 고치면 확정 값이 지워진다(Web은 `""`, Native는 결과 sheet에서 다시 골라야 확정). 확정 값을 서버로
115
+ 보낼 때는 입력어가 아니라 `value`/`selectedKey`를 쓴다.
116
+ - 배치는 `layoutStyle`(필드 바깥)로 한다. Web `style`·`className`은 input에, `fieldClassName`은 필드 바깥에 붙는다.
117
+ Native `style`은 deprecated — `layoutStyle` 또는 `density`를 쓴다.
118
+
119
+ ## 플랫폼 차이
120
+
121
+ | 항목 | Web | Native |
122
+ | --- | --- | --- |
123
+ | 항목 형태 | `{ value, label, keywords?, disabled? }` | `{ id, label, textValue, description?, disabled? }`, `sections` 가능 |
124
+ | 선택 값 | `value`·`onValueChange(string)`, 빈 값은 `""` | `selectedKey`·`onSelectionChange(key \| null)` |
125
+ | 결과 표시 | 입력 아래 listbox(포털, `align`, `portalContainer`) | `Modal` 결과 sheet(`sheetTitle`) |
126
+ | 비동기 결과 | `loading`만 | `asyncState`, `queryValue`/`resultQuery`, `minimumQueryLength`, `onRetry` |
127
+ | 확정 콜백 | `onValueChange` | `onCommit(key, reason)`, sheet가 닫힌 뒤 `onCommitAfterDismiss` |
128
+ | 폼 제출 | `name`이면 hidden input에 값 | 없음 |
129
+ | 항목 leading | 없음 | `renderLeading(item, props)` |
130
+
131
+ ## 함정
132
+
133
+ - Native의 `onSelectionChange`·`onCommit`은 결과 `Modal`이 닫히기 전에 불린다. 닫힌 뒤에 해야 하는
134
+ 후속 작업은 `onCommitAfterDismiss`로 받는다(Modal이 닫힌 다음 실행된다). 선택 뒤 iOS가 입력에 초점을
135
+ 되돌려 키보드·결과가 다시 열리던 문제는 renderer가 막는다(2026-09-30 감사, 소스 주석).
136
+ - Web은 `style`이 보이는 필드가 아니라 안쪽 input에 붙는다. 배치용 margin·width를 `style`로 주면 필드 틀과 어긋나므로
137
+ `layoutStyle`을 쓴다.
@@ -0,0 +1,125 @@
1
+ # CommandPalette
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [CommandPalette contract](../../command-palette.md), recipe `commandPaletteRecipe`(`src/command-palette.ts`)
9
+ - 스토리북: `배포/컴포넌트/오버레이/명령 검색`
10
+
11
+ ## 언제 쓰나
12
+
13
+ ⌘K 스타일로 앱 전체의 **행동**을 검색해 실행하는 모달에 쓴다. 결과는 값이 아니라 행동이며, 실행하면
14
+ 팔레트는 항상 닫힌다. 최근 항목·명령·검색 결과를 `sections`로 한 목록에 섞을 수 있다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 검색해서 값을 고르고 필드에 남김 | [Combobox](combobox.md) |
21
+ | 버튼에 붙은 행동 목록 | [Menu](menu.md) |
22
+ | 우클릭·길게 누르기 메뉴 | [ContextMenu](context-menu.md) |
23
+ | 화면 안의 검색 입력 | [SearchField](search-field.md) |
24
+ | 확인·입력이 필요한 모달 | [Dialog](dialog.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `CommandPalette` | 기본 | `@hjmds/react`, `/command-palette` | 없음 |
31
+
32
+ Native renderer는 없다(계약이 Web 전용 모달로 정의한다).
33
+
34
+ ## 최소 사용 예
35
+
36
+ ```tsx
37
+ // Web
38
+ import { CommandPalette } from "@hjmds/react/command-palette";
39
+
40
+ <CommandPalette
41
+ descriptor={{
42
+ accessibilityLabel: t("palette.label"),
43
+ searchPlaceholder: t("palette.placeholder"),
44
+ emptyMessage: t("palette.empty"),
45
+ closeLabel: t("common.close"),
46
+ }}
47
+ source={paletteSource}
48
+ query={query}
49
+ onQueryChange={setQuery}
50
+ onActivate={runCommand}
51
+ onActivateAfterDismiss={(id) => { if (id === "new-post") openComposerDialog(); }}
52
+ open={open}
53
+ onOpenChange={(next) => { setOpen(next); if (!next) setQuery(""); }}
54
+ />
55
+ ```
56
+
57
+ `paletteSource`는 `{ sections: [{ id: "recent", label: t("palette.recent"), items: recentItems }, …] }`처럼 만들고
58
+ `useMemo`로 고정한다. 기본(`queryState` 없음)은 renderer가 `query`로 항목의 `label`·`textValue`를 부분 일치로 거른다.
59
+ 항목은 `{ id, label, textValue, description?, shortcut?, disabled?, tone? }`이다. 항목의 label·description·shortcut과
60
+ 섹션 label은 제품 i18n에서 만든다.
61
+
62
+ Native: 없음.
63
+
64
+ ## 축과 기본값
65
+
66
+ | prop | 값 | 기본값 | 설명 |
67
+ | --- | --- | --- | --- |
68
+ | `descriptor` | `{ accessibilityLabel: string; searchPlaceholder: string; emptyMessage?: string; closeLabel?: string }` | 필수 | 앞의 둘은 비면 `TypeError`. `emptyMessage`는 보이는 결과가 없을 때 한 번 알린다. `closeLabel`을 주면 검색 입력 옆에 닫기 버튼이 생긴다(`reason: "close-action"`) |
69
+ | `source` | `{ items }` 또는 `{ sections: { id; label?; accessibilityLabel?; items }[] }` | 필수 | 섹션은 `label`·`accessibilityLabel` 중 하나가 필수. `accessibilityLabel`이 있으면 그룹 이름으로 먼저 쓴다 |
70
+ | `query` + `onQueryChange` | `string` + `(query: string) => void` | 필수 | 검색어는 항상 제품이 들고 있다 |
71
+ | `onActivate` · `onActivateAfterDismiss` | `(itemId: Key, reason: "pointer" \| "keyboard") => void` | `onActivate` 필수 | 실행하면 팔레트가 닫힌다. 다른 오버레이를 여는 명령은 `onActivateAfterDismiss`에서 연다 |
72
+ | `queryState` | `{ filtering?: "local"; asyncState? }` · `{ filtering: "external"; asyncState; queryValue: string; resultQuery?: string }` | 로컬 필터링 | `external`이면 renderer가 거르지 않는다. `resultQuery`가 `queryValue`와 다르면 결과는 보이되 실행되지 않는다(늦게 온 응답 보호) |
73
+ | `queryState.asyncState` | `{ status: "idle" }` · `{ status: "loading" \| "loadingMore" \| "empty" \| "error"; message: string }` | `idle` | 목록 위에 한 번 알린다(`error`는 alert, 나머지는 status). `descriptor.emptyMessage`보다 우선한다 |
74
+ | `open` · `defaultOpen` + `onOpenChange` | `boolean` + `(open: boolean, details: { reason }) => void` | `defaultOpen` `false` | reason: `"trigger" \| "close-action" \| "outside" \| "escape" \| "activation" \| "programmatic"` |
75
+ | `trigger` | element | — | 주면 그 요소가 팔레트를 열고(`reason: "trigger"`), 닫힌 뒤 포커스 복귀 대상이 된다 |
76
+ | `dismissPolicy` | `{ dismissible?, outsideDismiss?, escapeDismiss? }`(`boolean`) | 모두 `true` | 실행(`activation`)으로 닫히는 것은 어떤 정책으로도 막을 수 없다 |
77
+ | `renderLeading` | `(itemId: Key) => ReactNode` | — | 행 앞 아이콘을 넣는다 |
78
+ | `className` · `portalContainer` | `string` · `HTMLElement` | — | `layoutStyle`은 없다(위치를 recipe가 고정하는 제외 대상) |
79
+
80
+ ## 배치
81
+
82
+ | 항목 | 값 | 근거 |
83
+ | --- | --- | --- |
84
+ | 크기 | 폭 `min(100%, 35rem)`(최대 560), 최대 높이 420. 검색 입력 최소 높이 44(`control.fieldHeight`), 항목 최소 높이 44(`control.minTouchTarget`). 모서리 `radius.lg` 16 | `commandPaletteRecipe.content`, `.hjm-command-palette*`, `react/src/command-palette.tsx` |
85
+ | 간격 | 화면 가장자리와 `spacing.md` 16(오버레이 padding), 위에서 10vh 띄운다. 검색 입력 좌우 `spacing.md` 16, 목록 안쪽 `spacing.xxs` 4, 항목 좌우 `spacing.sm` 12 · 아이콘↔문구 `spacing.xs` 8, 섹션 이름 위아래 `spacing.xs` 8 · 좌우 `spacing.sm` 12, 상태 문구 `spacing.md` 16 | `.hjm-command-palette-positioner`, `.hjm-command-palette__*` |
86
+ | 순서·정렬 | 화면 위쪽 가운데. 위→아래 [검색 입력(아래 경계선) · 닫기 버튼(`closeLabel`이 있을 때 끝 쪽)] → [상태 문구] → [섹션 이름 → 항목들]. 항목은 [leading] → [label · description] → [shortcut(끝 쪽)] | `commandPaletteRecipe.slots`, `.hjm-command-palette__copy`·`__shortcut` |
87
+ | 고정·스크롤 | 배경막(`backdrop.modal`)이 화면을 덮는 모달이다. 검색 입력은 위에 고정되고 목록만 스크롤한다(`overscroll-behavior: contain`) | `.hjm-command-palette__search`(`flex: 0 0 auto`), `.hjm-command-palette__viewport` |
88
+ | 좁은 폭·큰 글자 | 폭은 화면 − 32까지 줄어든다. 항목 문구는 줄바꿈된다(`flex-wrap`, `overflow-wrap: anywhere`) | `.hjm-overlay`, `.hjm-command-palette__copy` |
89
+
90
+ ```text
91
+ ┌──────────────────────────────────────┐
92
+ │ ▒▒▒▒▒▒▒▒▒ 배경막 ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │
93
+ │ ↕ 10vh │
94
+ │ ┌──────────────────────────────┐ │
95
+ │ │ 🔍 명령 검색… [×] │ │ ← 검색(고정), × 는 closeLabel
96
+ │ ├──────────────────────────────┤ │
97
+ │ │ 섹션 이름 │ │
98
+ │ │ (아이콘) 명령 이름 설명 ⌘K │ │ ← 항목 44
99
+ │ │ … (스크롤) │ │ ← 최대 높이 420
100
+ │ └──────────────────────────────┘ │
101
+ │ 최대 폭 560 │
102
+ └──────────────────────────────────────┘
103
+ ```
104
+
105
+ ## 꼭 지킬 것
106
+
107
+ - `descriptor`의 두 문구는 필수이고 빈 문자열이면 `TypeError`가 난다.
108
+ - 기본은 renderer가 `query`로 `source`를 거른다(`label`·`textValue` 부분 일치, 대소문자 무시). 서버 검색·퍼지 정렬처럼
109
+ 제품이 이미 거른 결과는 `queryState={{ filtering: "external", asyncState, queryValue, resultQuery }}`로 넘긴다.
110
+ - 결과 없음 문구는 `descriptor.emptyMessage`로 준다. 없으면 빈 목록에 아무 안내도 없다. 로딩·실패는
111
+ `queryState.asyncState`로 알린다.
112
+ - 마우스 사용자가 닫을 수 있도록 `descriptor.closeLabel`을 준다. 없으면 Escape·바깥 누름·실행으로만 닫힌다.
113
+ - 전역 단축키(⌘K)와 그 범위는 제품이 정하고 바인딩한다. HJM은 키를 듣지 않는다.
114
+ - 다른 오버레이를 여는 명령은 `onActivateAfterDismiss`에서 연다. `onActivate`에서 열면 두 모달이 겹친다.
115
+ - 배치 prop은 `className`뿐이다(`layoutStyle` 제외 대상). 색·크기를 덮지 않는다.
116
+
117
+ ## 함정
118
+
119
+ - 현재 renderer는 `query`·`source`·`queryState`의 참조가 바뀔 때마다 활성 행을 첫 활성 항목으로 되돌린다(`useEffect` 의존성).
120
+ `source`나 `queryState`를 JSX 안에서 객체 리터럴로 만들면 렌더마다 참조가 바뀌어 화살표 키·마우스로 옮긴 활성 행이
121
+ 바로 첫 행으로 돌아간다. 둘 다 `useMemo`로 고정한다.
122
+ - 닫을 때 `query`를 지우는 것은 제품 몫이다. 비우지 않으면 다음에 열 때 이전 검색어가 남는다.
123
+ - `filtering: "external"`인데 `resultQuery`를 갱신하지 않으면 결과가 보이기만 하고 Enter·클릭이 먹지 않는다(`aria-disabled`).
124
+ - 현재 Web 스토리는 제품 쪽에서 `label.includes(query)`로 직접 거르고 결과가 없을 때 `asyncState` `empty`로 안내하며
125
+ `closeLabel`이 없다. 새 코드는 로컬 필터링 기본값과 `emptyMessage`·`closeLabel`을 쓴다.
@@ -0,0 +1,125 @@
1
+ # CommentThreadScreen
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
+ 게시물·콘텐츠 아래의 댓글 화면 한 장에 쓴다. 최상위 댓글과 한 단계 답글, 답글 펼침, 좋아요·답글 버튼,
14
+ 하단 작성창을 controlled로 합성한다. 별도 보조 기능(supplemental)이라 `/screen-flows` subpath로만 import 한다.
15
+ 서버 정렬·권한·전송·성공 후 초안 정리는 제품 소유다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 1:1·그룹 대화 타임라인 | [ChatScreen](chat-screen.md) + [ChatMessage](chat-message.md) |
22
+ | 작성창만 필요 | [MessageComposer](message-composer.md) |
23
+ | 일반 목록/상세 화면 | [ListDetailScreen](list-detail-screen.md) |
24
+ | 신고·차단 흐름 | [ModerationScreen](moderation-screen.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `CommentThreadScreen` | 화면 조합 | `/screen-flows` | `/screen-flows` |
31
+ | `CommentThreadItem`(타입) | 댓글 한 개의 데이터 | `/screen-flows` | `/screen-flows` |
32
+
33
+ 루트 barrel에는 없다. optional native peer를 요구하지 않는다.
34
+
35
+ ## 최소 사용 예
36
+
37
+ ```tsx
38
+ // Web
39
+ import { CommentThreadScreen } from "@hjmds/react/screen-flows";
40
+ import { MessageComposer } from "@hjmds/react/screens";
41
+
42
+ <CommentThreadScreen
43
+ title={t("comments.title")}
44
+ items={comments.map(toCommentItem)}
45
+ expandedIds={expanded}
46
+ onExpandedChange={toggleExpanded}
47
+ onLike={like}
48
+ onReply={id => setReplyTo(id)}
49
+ replyLabel={t("comments.reply")}
50
+ repliesLabel={(count, open) => t(open ? "comments.hideReplies" : "comments.showReplies", { count })}
51
+ composer={<MessageComposer value={draft} label={t("comments.input")} sendLabel={t("comments.send")}
52
+ sendIcon={<ArrowUpIcon />} sendPresentation="circle" onValueChange={setDraft} onSend={send} />}
53
+ threadFooter={hasMore ? <LoadMoreButton /> : null}
54
+ />
55
+ ```
56
+
57
+ ```tsx
58
+ // Native — props·콜백 이름은 Web과 같다
59
+ import { CommentThreadScreen } from "@hjmds/react-native/screen-flows";
60
+ import { MessageComposer } from "@hjmds/react-native/screens";
61
+
62
+ <CommentThreadScreen
63
+ title={t("comments.title")}
64
+ items={comments.map(toCommentItem)}
65
+ expandedIds={expanded}
66
+ onExpandedChange={toggleExpanded}
67
+ onLike={like}
68
+ onReply={id => setReplyTo(id)}
69
+ replyLabel={t("comments.reply")}
70
+ repliesLabel={(count, open) => t(open ? "comments.hideReplies" : "comments.showReplies", { count })}
71
+ composer={<MessageComposer value={draft} label={t("comments.input")} sendLabel={t("comments.send")}
72
+ sendIcon={<ArrowUpIcon />} sendPresentation="circle" onValueChange={setDraft} onSend={send} />}
73
+ threadFooter={hasMore ? <LoadMoreButton /> : null}
74
+ />
75
+ ```
76
+
77
+ ### 제품이 공급하는 것
78
+
79
+ | prop | 내용 |
80
+ | --- | --- |
81
+ | `title`(필수), `header`·`leading`·`actions`, `state`·`stateAction`, `notice` | `ScreenLayout`과 같다([ScreenLayout](screen-layout.md)) |
82
+ | `items`(필수) | 서버 순서대로 정렬된 `CommentThreadItem[]`. 아래 표 |
83
+ | `expandedIds`·`onExpandedChange(id)` | 답글을 펼친 최상위 id 목록과 토글 |
84
+ | `onLike(id)`·`onReply(id)` | 기본 좋아요 버튼·답글 버튼의 콜백 |
85
+ | `replyLabel`·`repliesLabel(count, expanded)` | 지역화 문구. 복수형·펼침 상태는 제품 i18n이 만든다. 펼침 버튼 이름이 이 문구이므로 `expanded`에 따라 "답글 3개 보기"/"답글 숨기기"처럼 상태가 드러나게 만든다 |
86
+ | `composer` | 작성창(보통 `MessageComposer`의 `replyTo`·`sendIcon`). `ready`·`empty`일 때만 보인다 |
87
+ | `threadFooter` | 더 보기·커서 페이지네이션 |
88
+
89
+ `CommentThreadItem`: `id`, `parentId`(`null`이면 최상위), `author`, `body`(노드), `timeLabel`, `likeCountLabel`,
90
+ `likeIcon`, `likeLabel`은 필수. 선택은 `bodyText`(작성자와 본문을 한 줄 흐름으로), `avatar`, `likeAction`(기본
91
+ 좋아요 버튼 교체, `null`이면 생략), `actions`(신고·수정 등), `canReply`(기본: 최상위만 true), `replyDisabled`.
92
+
93
+ ## 배치
94
+
95
+ | 항목 | 값 | 근거 |
96
+ | --- | --- | --- |
97
+ | 크기 | ScreenLayout 폭(최대 720); 본문 열이 남은 폭을 채우고(`flex: 1`, 최소 폭 0) 오른쪽 끝에 좋아요 `IconButton` 하나; 답글·펼침 버튼은 `Button size="small"` ghost | Web·Native `CommentThreadScreen` |
98
+ | 간격 | 화면 padding `spacing.md` 16; 최상위 댓글 묶음 사이 `spacing.lg` 20; 댓글 행–답글 묶음 `spacing.xs` 8; 답글 묶음 들여쓰기 `sectionGap`(`spacing.xl`) 24, 펼침 버튼·답글 사이 `spacing.md` 16; 행 안 아바타–본문–좋아요 `spacing.sm` 12, 본문 줄 사이 `spacing.xxs` 4, 시각·좋아요 수·답글 버튼 사이 `spacing.sm` 12 | `screen-flows.tsx` `Stack gap`, `screenPatternRecipe.sectionGap` |
99
+ | 순서·정렬 | 최상위 댓글(아바타 → 작성자·본문 → 시각·좋아요 수·답글 → `actions` → 좋아요) → 답글 펼침 버튼 → 펼친 답글 → … → `threadFooter` → composer(footer) | 렌더 순서 |
100
+ | 고정·스크롤 | 헤더·composer 고정, 댓글은 본문 화면 스크롤(`scroll` 기본 `"screen"`); composer는 `ready`·`empty`에서만 | `ScreenLayout`, `screen-flows.tsx` |
101
+ | 좁은 폭·큰 글자 | 시각·좋아요 수·답글 버튼 줄과 작성자 줄은 줄바꿈(`flexWrap: "wrap"`); 답글은 한 단계만 들여써 좁은 폭에서도 본문 폭을 지킨다 | `screen-flows.tsx` |
102
+
103
+ ## 꼭 지킬 것
104
+
105
+ - id는 비어 있지 않고 유일해야 하며, 답글의 `parentId`는 `items` 안의 **최상위** 댓글이어야 한다. 아니면 렌더 중
106
+ `TypeError`가 난다. 답글의 답글은 표현하지 않으므로 제품이 최상위로 평탄화한다.
107
+ - 모든 문구·시각·좋아요 수 라벨은 제품 i18n에서 만든다. `likeCountLabel`이 빈 문자열이면 그리지 않는다.
108
+ - 좋아요 저장·길게 누르기·이모지 선택, 답글 권한(`canReply`/`replyDisabled`)은 제품 데이터로 정한다.
109
+ - 기본 `scroll`은 `ScreenLayout`과 같은 `screen`이다. 댓글을 가상화 목록으로 그리면 직접 `scroll="content"`를 준다.
110
+
111
+ ## 플랫폼 차이
112
+
113
+ | 항목 | Web | Native |
114
+ | --- | --- | --- |
115
+ | 배치 | `layoutStyle`, `className` | `layoutStyle` |
116
+ | 새로고침·키보드 스크롤 | 없음 | `scrollProps`(`refreshControl`, `keyboardDismissMode` 등), `scrollRef` |
117
+ | 답글 펼침 상태 | 펼침 버튼이 `aria-expanded`를 노출한다(미게시(1.12.1 이후)) | 펼침 버튼이 `accessibilityState.expanded`를 노출한다(미게시(1.12.1 이후)) |
118
+
119
+ ## 함정
120
+
121
+ - 펼침 상태는 두 플랫폼 모두 버튼의 expanded 상태로 전한다(2026-10-06 Web `aria-expanded` 추가). 그래도 `repliesLabel(count, expanded)`가
122
+ 버튼 이름이므로 문구에서도 펼침 여부가 드러나게 만든다.
123
+
124
+ - 작성창 노출 조건이 ChatScreen과 다르다. ChatScreen은 `ready`에서만, CommentThreadScreen은 `ready`·`empty`에서 보인다.
125
+ `loading`·`error`·`restricted`에서는 둘 다 숨는다.
@@ -0,0 +1,98 @@
1
+ # Container
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Container contract](../../container.md), recipe `containerRecipe`(`src/container.ts`)
9
+ - 스토리북: `배포/컴포넌트/레이아웃/컨테이너`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 화면 본문의 최대 폭과 좌우(논리 방향) 여백을 맞출 때 쓴다. 글 읽기 화면은 `reading`,
14
+ 일반 제품 화면은 `content`, 지도·갤러리처럼 의도적으로 가득 채우는 영역은 `full`을 고른다.
15
+ 가운데 정렬은 항상 inline 축 기준이라 RTL에서도 그대로 동작한다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 화면 전체 틀(상단 바·스크롤·하단 행동) | [Layout](layout.md) |
22
+ | 자식 사이 간격·정렬 | [Stack](stack.md) |
23
+ | 반응형 열 배치 | [Grid](grid.md) |
24
+ | 배경·테두리가 있는 영역 | [Surface](surface.md), [Card](card.md) |
25
+ | 비율이 고정된 미디어 틀 | [AspectRatio](aspect-ratio.md) |
26
+
27
+ ## 공개 이름과 import
28
+
29
+ | 이름 | 역할 | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `Container` | 기본 | `@hjmds/react`, `/layout` | `@hjmds/react-native`, `/primitives` |
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { Container } from "@hjmds/react/layout";
38
+ import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
39
+
40
+ const gutter = resolveWindowClass(window.innerWidth) === "compact" ? "compact" : "regular";
41
+
42
+ <Container size="reading" gutter={gutter}>
43
+ <article>{body}</article>
44
+ </Container>
45
+ ```
46
+
47
+ ```tsx
48
+ // Native
49
+ import { ScrollView, useWindowDimensions } from "react-native";
50
+ import { Container } from "@hjmds/react-native/primitives";
51
+ import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
52
+
53
+ const { width } = useWindowDimensions();
54
+ const gutter = resolveWindowClass(width) === "compact" ? "compact" : "regular";
55
+
56
+ <ScrollView>
57
+ <Container size="content" gutter={gutter}>{children}</Container>
58
+ </ScrollView>
59
+ ```
60
+
61
+ Native는 `ScrollView`가 세로 스크롤을, 그 안의 Container가 좌우 여백을 맡는다. `ScrollView`에 좌우 padding을 직접 주지 않는다.
62
+
63
+ ## 축과 기본값
64
+
65
+ | prop | 값 | 기본값 | 설명 |
66
+ | --- | --- | --- | --- |
67
+ | `size` | `"reading"`(최대 720) · `"content"`(최대 1200) · `"full"`(최대 폭 없음) | `"content"` | — |
68
+ | `gutter` | `"none"`(0) · `"compact"`(16) · `"regular"`(20) · `"spacious"`(24) | `"regular"` | 구간 객체(`{ compact: … }`)는 받지 않는다. 폭 600 미만(`resolveWindowClass` `compact`)은 `"compact"`, 그 이상은 `"regular"`를 골라 넘긴다 |
69
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 배치 |
70
+ | 허용 값 검사 | — | — | 허용하지 않는 값은 `resolveContainerDescriptor`가 `TypeError`로 막는다 |
71
+
72
+ ## 배치
73
+
74
+ | 항목 | 값 | 근거 |
75
+ | --- | --- | --- |
76
+ | 크기 | 부모 폭을 채우되 최대 폭 `reading` 720(`layout.readingMaxWidth`) · `content` 1200(`layout.contentMaxWidth`) · `full` 제한 없음. 높이는 내용이 정한다 | `containerRecipe.maxWidths` |
77
+ | 간격 | 좌우 gutter `none` 0 · `compact` `spacing.md` 16 · `regular` `spacing.lg` 20 · `spacious` `spacing.xl` 24. 위아래 여백·자식 사이 간격은 없다([Stack](stack.md)이 정한다) | `containerRecipe.gutters` |
78
+ | 순서·정렬 | 페이지 본문·섹션 바깥에서 가로 가운데 정렬(inline 축, RTL에서도 같다). 글 위주 화면은 `reading`, 목록·대시보드는 `content` | `containerRecipe.alignment` |
79
+ | 고정·스크롤 | 고정 영역도 스크롤도 만들지 않는다. 스크롤은 화면이 소유한다 | `.hjm-container` |
80
+ | 좁은 폭·큰 글자 | 최대 폭보다 좁으면 부모 폭을 그대로 쓰고 gutter만 남는다. 좁은 폭에서는 `compact` gutter를 고려한다 | `resolveContainerDescriptor` |
81
+
82
+ ## 꼭 지킬 것
83
+
84
+ - 최대 폭과 여백은 `size`·`gutter`로만 고른다. 임의 px `maxWidth`나 좌우 padding을 제품마다
85
+ 새로 정하지 않는다(배제 이유는 [계약](../../container.md#배제한-축)).
86
+ - 바깥 배치(margin·flex)는 `layoutStyle`로 한다. `style`로 `maxWidth`·`padding`을 덮으면
87
+ 계약 폭이 사라진다(두 renderer 모두 `style`이 recipe 값 뒤에 합쳐진다). Native `style`은 deprecated —
88
+ `layoutStyle` 또는 `size`·`gutter`를 쓴다.
89
+ - Container는 배경·테두리·스크롤을 갖지 않는다. 필요하면 바깥 컴포넌트가 맡는다.
90
+
91
+ ## 플랫폼 차이
92
+
93
+ | 항목 | Web | Native |
94
+ | --- | --- | --- |
95
+ | 폭 | `max-inline-size` | `maxWidth` + `width: "100%"` |
96
+ | 여백 | `padding-inline` | `paddingHorizontal` |
97
+ | 가운데 정렬 | `hjm-container` 스타일 | `alignSelf: "center"` |
98
+ | root | `<div>`(ref 전달) | `View` |