@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
package/docs/progress.md CHANGED
@@ -20,5 +20,23 @@
20
20
  `max`를 생략하면 두 renderer 모두 `progressRecipe.defaults.max`(100)를 쓴다. 그 전에는 Web 100,
21
21
  Native 1이라 같은 `value={76}`이 Web에서는 76%, Native에서는 RangeError였다(STEA 후보 검토의
22
22
  수치 요약 구성을 Native 시뮬레이터에서 띄우다 발견). 100을 고른 이유는 Web 기존 동작과 문서 예제가
23
- 백분율이기 때문이다. 0–1 분수가 자연스러운 곳(UploadItem의 업로드 비율)은 `max={1}`을 쓰거나
23
+ 백분율이기 때문이다. Progress에 0–1 비율을 직접 넘길 때는 `max={1}`을 쓰거나
24
24
  백분율로 바꿔 넘긴다. Native 기본값이 바뀌는 호환 파괴 변경이지만, 1.11.0과 같이 사용자가 관리 소비 앱 전수 이관을 결정해 1.12.0 minor에 싣고 이관표에 기록했다.
25
+
26
+ ## 값의 단위와 합성 경계
27
+
28
+ 2026-10-03 리포트 대조에서 직접 Progress 호출과 UploadItem descriptor의 단위가 다름을
29
+ 재확인했다. 숫자만 일괄 치환하면 정상적인 업로드 상태까지 깨지므로, 호출 경계에서 단위를
30
+ 선택한다. 근거와 검증 범위는 [당일 반영 기록](../../../docs/plans/daily-design-research-2026-10-03.md)에 있다.
31
+
32
+ | 입력 경계 | 64%를 나타내는 입력 | 처리 |
33
+ | --- | --- | --- |
34
+ | Progress에 백분율 직접 전달 | `<Progress label="업로드 진행" value={64} />` | 생략한 max는 Web/Native 모두 100 |
35
+ | Progress에 0–1 비율 직접 전달 | `<Progress label="업로드 진행" value={0.64} max={1} />` | value와 max를 같은 단위로 전달 |
36
+ | UploadItem descriptor | `state: { status: "uploading", progress: 0.64 }` | 두 renderer가 내부 Progress에 64로 변환해 전달 |
37
+ | 진행량을 모르는 Progress | value 생략 | 측정한 백분율로 표시하지 않음 |
38
+ | 진행량을 모르는 UploadItem | `state: { status: "uploading", progress: null }` | 불확정 진행으로 표시 |
39
+
40
+ `<Progress value={0.64} />`는 기본 max에서 **0.64%**다. [[upload-item]]의 `progress`는
41
+ 계속 0–1 계약이므로 64로 바꾸거나, 제품에서 먼저 100을 곱해 descriptor에 넣지 않는다.
42
+ 이 단위는 선형·원형 표현 모두에 적용된다.
@@ -38,3 +38,16 @@
38
38
 
39
39
  **값의 출처.** 각 제공자의 공개 브랜드 가이드라인(2026-09 확인). 가이드라인이 바뀌면
40
40
  `authProviderPalettes` 한 곳만 고친다.
41
+
42
+ ## 제공자 색과 글자 대비
43
+
44
+ 2026-10-06 전수 브라우저 검사에서 네이버의 지정 녹색과 흰색 라벨이 3.09:1로 측정됐다. 같은 날 한때 모든 제공자 라벨을
45
+ `typography.titleLarge`(20/28, heavy)로 키워 [WCAG 1.4.3](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html)의
46
+ 큰 굵은 글자 3:1 기준을 맞췄으나, 사용자 위임 결정으로 되돌렸다. 라벨·색 표현은 제공자 가이드가 소유하고(위 소유 경계),
47
+ HJM이 가이드 밖의 글자 크기·굵기를 정하면 네 버튼이 제품의 나머지 행동 버튼보다 커져 목록 균형이 깨진다.
48
+
49
+ - 라벨은 `authProviderButtonRecipe.label = typography.body`(14/20)다.
50
+ - 네이버 녹색 위 흰 라벨의 대비는 **제공자 색 예외**로 기록한다. 색은 제공자가 규정한 값이라 HJM이 고치지 않는다.
51
+ 제품이 대비를 더 높여야 하면 제공자 가이드가 허용하는 변형(예: 제공자가 규정한 다른 배경)을 쓰고
52
+ `authProviderPalettes` 한 곳에서 바꾼다. 버튼별 글자 크기 덮어쓰기로 해결하지 않는다.
53
+ - 이 예외는 제공자 버튼에만 적용된다. 일반 행동 버튼의 대비 기준을 완화하는 근거로 쓰지 않는다.
package/docs/result.md CHANGED
@@ -92,6 +92,9 @@ Web/Native 모두 아이콘 + 제목 + 설명 + action 슬롯을 세로로 쌓
92
92
  그대로 조합합니다 — Result 자체의 recipe는 아이콘 tone, 타이포그래피 위계, 슬롯 사이
93
93
  gap만 제공합니다.
94
94
 
95
+ 행동 줄은 **보조 → 주** 순서로 그린다(`actions` 배열은 여전히 첫 번째가 primary). 다른 모든 가로 행동 줄과 Dialog footer가
96
+ 주 행동을 줄 끝에 두는데, Result만 primary를 먼저 그려 반대였다(2026-10-06 후속 점검, [Button 지침](usage/components/button.md)).
97
+
95
98
  ## 제품 화면 검증
96
99
 
97
100
  제품에서 채택할 때는 primary-only, primary+secondary, action-없음 중 실제 사용하는
@@ -1,5 +1,7 @@
1
1
  # 화면 제목과 마지막 행동
2
2
 
3
+ 검토일: 2026-10-05
4
+
3
5
  2026-09-16: 앱 감사에서 Native 전용 TopBar/BottomCTA 때문에 같은 제품의 Web에
4
6
  별도 헤더와 푸터가 생기는 공백을 확인했다. 기존 Native recipe를 React에 연결한다.
5
7
  TDS의 [ListRow](https://tossmini-docs.toss.im/tds-mobile/components/ListRow/list-row-overview/)와
@@ -26,6 +28,14 @@ Web의 headingLevel은 페이지 구조가 정한다. div root이므로 Dialog
26
28
  짧은 행동 문구를 압축하던 동일 폭 좌우 열 대신 콘텐츠 폭을 보장하고 제목이 줄바꿈한다.
27
29
  큰 글자에서는 제목을 다음 행으로 내려 행동과 겹치지 않게 한다.
28
30
 
31
+ Native는 OS 글자 크기가 변경되어 큰 글자/compact 구조가 전환될 때 해당 내부
32
+ subtree를 새로 구성한다. 2026-10-05 번뚝의 iOS 26.5 개발 런타임에서 최대 글자를
33
+ 일반 크기로 줄인 뒤 full-width 행 host가 compact leading 슬롯으로 재사용되며
34
+ 제목이 좁은 오른쪽 칸에 남았다. 정적 스타일만 검사하거나 앱에서 폭을 덮는 대신
35
+ 구조가 다른 host의 identity를 분리한다. 전환 시 슬롯 내부의 로컬 상태·포커스는
36
+ 재생성될 수 있으므로 유지할 제품 상태는 TopBar 밖에서 소유한다. Web은 동일한
37
+ DOM 슬롯을 유지하며 CSS로 배치하므로 이 Native host 교체를 적용하지 않는다.
38
+
29
39
  BottomCTA는 primaryAction 하나와 선택적인 secondaryAction, description을 받는다.
30
40
  loading은 표시 문구를 시각적으로 숨기되 원래 폭과 접근성 이름을 유지하고 중복 실행을 막는다. 큰 글자에서 세로로 쌓을 때
31
41
  가로 배치용 flex-basis를 해제한다. 가로 배치 값이 세로 높이로 해석되어 거대한 공백을
@@ -0,0 +1,376 @@
1
+ # 반복 화면 조합
2
+
3
+ 검토일: 2026-10-06 · 상태: Storybook 배포(2026-10-06 사용자 승인) · 패키지 게시/제품 적용 전
4
+
5
+ 사용자가 로그인·설정·알림·채팅 화면을 미리 일반화해 개발 생산성을 높이도록 요청했다.
6
+ 기존 AuthScreenLayout, Section, ListRow, TopBar, Layout, EmptyState, action-session,
7
+ NotificationBell과 두 renderer의 exports를 비교했다. 새 primitive catalog를 늘리는 대신
8
+ `@hjmds/react/screens`, `@hjmds/react-native/screens`의 supplemental 화면 조합으로 제공한다.
9
+
10
+ ## 현재 제품에서 확인한 반복과 차이
11
+
12
+ | 근거 파일(app-portfolio 기준) | 반복 구조 | 제품에 남기는 것 |
13
+ | --- | --- | --- |
14
+ | apps/burntok/apps/mobile/src/app/login.tsx | AuthScreenLayout의 hero/main/footer | OAuth, 제공자 자산, 심사자 폼 조건 |
15
+ | apps/diairy/apps/mobile/src/features/settings/SettingsScreen.tsx | 제목, 카드 그룹, 계정 행동 | CozySky, 섹션 이동, 계정 병합·탈퇴 |
16
+ | apps/spint/apps/mobile/src/features/settings/SettingsScreen.tsx | 프로필, 언어, 알림, 도움말, 계정 그룹 | 닉네임 변경 제한, 재인증, 서버 저장 |
17
+ | apps/burntok/apps/web/src/app/notifications/page.tsx | 헤더, 필터/도구, 상태, 알림 목록 | focus 복구, 읽음 확인, 라우팅 |
18
+ | apps/utilverse/apps/mobile/src/features/NotificationInboxScreen.tsx | 필터, 알림, 더 보기, 읽음 상태 | 계정 scope, 요청 취소, 페이지 합치기 |
19
+ | apps/utilverse/apps/mobile/src/features/ConversationScreen.tsx | 헤더, 타임라인, 작성창 | 채널 권한, 도구 카드, 답글, 전송 영수증 |
20
+
21
+ Flutter 앱에는 React 컴포넌트를 주입할 수 없다. 화면 역할과 상태 계약을 참조할 수 있지만
22
+ 별도 Dart renderer가 생기기 전에는 적용된 것으로 간주하지 않는다.
23
+
24
+ ## 선택 기준과 소유권
25
+
26
+ - 로그인: 기존 `auth-screen`의 `AuthScreenLayout` + `AuthProviderButton`을 그대로 쓴다.
27
+ 새 LoginScreen alias나 제공자 preset은 만들지 않는다. 카드 유지·중앙 로딩·정책 고지는
28
+ [로그인 계약](auth-screen.md)을 따른다.
29
+ - `ScreenLayout`: 제목, 뒤로 가기 슬롯, 도구, 안내, 본문, 하단 행동을 하나의 화면으로 배치한다.
30
+ `Layout`은 앱 전체 navigation/sidebar를, `TopBar`는 개별 상단 UI를 소유한다. 화면 전체의
31
+ 남는 높이와 상태 교체는 이 조합이 소유하므로 앱 shell과 중복되지 않는다.
32
+ - `SettingsScreen`: `profile`과 안정적인 id를 가진 `sections`를 받는다. 각 섹션은 기존
33
+ `Section`을 사용한다. 내부에는 ListRow·Switch·Select·TextField를 그대로 합성한다.
34
+ 저장 방식·낙관적 갱신·실패 복구·탈퇴 확인은 [action-session](action-session.md)과 제품이 소유한다.
35
+ - `NotificationInboxScreen`: 필터는 상태가 바뀌어도 유지하고 본문만 교체한다. `NotificationItem`은
36
+ 기존 ListRow에 지역화한 읽음 상태와 시각을 조합한다. href/onClick(Web), onPress(Native)를
37
+ 그대로 사용하며 표시·탭·스크롤 자체가 읽음 처리를 실행하지 않는다.
38
+ - `ChatMessage`: 발신/수신 정렬, 작성자, 아바타, 답장 인용, 전송 상태와 시각을 합성한다.
39
+ 기존 Asset/VoiceNote 등은 children에 넣고 영수증·반응·재시도는 제품이 넘긴다. 전체 타임라인을
40
+ live region으로 만들지 않으므로 과거 메시지 로딩 때 읽기 순서를 가로채지 않는다.
41
+ - `MessageComposer`: 기존 TextArea와 Button을 조합한다. 빈 입력·pending·disabled에서는
42
+ 전송하지 않고 Enter는 줄 바꿈으로 유지한다(IME 조합과 충돌 방지). 제품 onSend에 원문을
43
+ 전달하며 초안 초기화는 서버 영수증을 받은 제품만 수행한다. 1~5줄은 짧은 작성창이 타임라인을
44
+ 밀어내지 않도록 정한 기본값이며 더 긴 입력은 TextArea 안에서 스크롤한다.
45
+ - `ChatScreen`: `composer`와 타임라인을 분리한다. 기본 `scroll="content"`로 FlatList 등
46
+ 제품 타임라인의 가상화·이전 메시지 위치·새 메시지 배지를 보존한다. 작은 예제만
47
+ `scroll="screen"`을 쓴다. 새 채팅 데이터 모델이나 전송 엔진은 만들지 않는다.
48
+
49
+ 모든 문구·접근성 이름·상대시간·날짜는 제품 i18n에서 전달한다. 문자열을 renderer에서
50
+ 번역하거나 정해진 메뉴·제공자·정책 URL을 번들하지 않는다. 새 직접 의존성은 없다.
51
+
52
+ ## 상태, 키보드, 접근성
53
+
54
+ `state`는 `ready | loading | empty | error | restricted`의 구별된 union이다. ready 외에는
55
+ 비어 있지 않은 지역화 title이 필요하다. 2026-10-06 전체 로딩 재점검에서 화면 골격에 문구가 남아 있는 것을 발견해, loading은 공통 Spinner만 표시하고 title·description은 접근성 이름으로 전달하도록 맞췄다. `stateAction`에는 retry/login 등 실제 제품 행동을 넣는다.
56
+ 본문 전체 상태는 헤더와 footer를 제외한 남은 영역 가운데에 놓고, 긴 내용은 스크롤한다.
57
+ 새로고침 실패와 저장 오류는 `ready` + `notice`로 전달한다. 초기 로딩으로 바꾸면 본문이
58
+ unmount되므로 입력 초안을 보존해야 하는 갱신에는 사용하지 않는다.
59
+
60
+ Web은 제목이 연결된 main을 만든다. 이미 main인 제품 shell 안에서는 `as="section"`을 쓴다.
61
+ error는 alert, 그 밖의 초기 상태는 status다. 행동은 live region 바깥에 두어 안내 재방송이
62
+ 버튼 탐색을 방해하지 않게 한다. Native는 header와 accessibilityLiveRegion을 사용한다.
63
+ 읽음 상태는 색이나 글자 굵기뿐 아니라 statusLabel로도 제공한다.
64
+
65
+ Web host는 실제 남은 높이를 제공해야 한다(예: flex route의 `height:100%`, shell 없는 예제는
66
+ `height:100dvh`). Native host는 safe area와 탭/상단 navigation inset을 먼저 제외한다.
67
+ HJM이 기종별 높이나 중첩 safe area를 추측하지 않는다. Native ChatScreen은 기존
68
+ `KeyboardAvoiding` 또는 제품 keyboard adapter 하나로 감싸며 둘을 동시에 적용하지 않는다.
69
+ ready 이외의 채팅에는 작성창을 숨겨 접근 제한·초기 로딩에서 전송 조작이 노출되지 않게 한다.
70
+
71
+ ## 사용 예시
72
+
73
+ ```tsx
74
+ import { SettingsScreen } from '@hjmds/react/screens';
75
+
76
+ <SettingsScreen title={t('settings.title')} sections={[
77
+ { id: 'preferences', title: t('settings.preferences'), children: <Preferences /> },
78
+ { id: 'account', title: t('settings.account'), children: <AccountActions /> },
79
+ ]} />
80
+ ```
81
+
82
+ Native는 import를 `@hjmds/react-native/screens`로 바꾼다. 기존 제품 wrapper 내부를 교체할 때
83
+ 라우트·query·mutation·계정 scope·초안·키보드·스크롤 복구는 유지한다. 배포된 exact npm train을
84
+ 받기 전 제품의 dependency나 lockfile을 로컬 source로 우회하지 않는다. 이 additive API는
85
+ 기존 앱 코드를 자동 변경하지 않는다.
86
+
87
+ ## 검증 범위
88
+
89
+ 계약 검증, Web 브라우저 행동/배치, Native host mock, 두 Storybook은 각각 다른 근거다.
90
+ 실제 기기 키보드·스크린리더·제품 화면 적용은 별도 확인이 필요하다. 공통 화면 예제는
91
+ 2026-10-06 사용자 승인으로 `실험/화면/공통 화면`에서 `배포/화면/<분류>/<항목>`(소개·계정·설정·검색·콘텐츠·소통·화면 틀과 도구)으로 옮겼다.
92
+ 새 화면 스토리는 `실험/화면/<분류>/<항목>`에 최종 이름으로 두고 검토하며, 스토리북 배포 분류 승인은 구현 완료·npm 게시와 별개다.
93
+ 진행 및 미검증 항목은 [구현 기록](../../../docs/plans/reusable-screens-2026-10-05.md)을 따른다.
94
+
95
+ ## 시각 설계 근거
96
+
97
+ 첫 시안의 빈약한 화면에 대한 사용자 피드백 후 [제품 비교 조사](../../../docs/plans/screen-reference-study-2026-10-05.md)를
98
+ 추가했다. 실제 앱 캡처 3종, 현재 제품 소스, 외부 13개 제품/시스템의 공개 근거를 구분했다.
99
+ 설정의 그룹과 현재값, 알림의 사람/활동/시간 위계, 채팅의 양방향 정렬과 짧은 작성창을 반영한다.
100
+ 개발용 상태 버튼은 기본 UI에 두지 않고 Storybook의 상태 story 및 복구 예제에서 제공한다.
101
+
102
+ ## 설정 화면 시각 구성 (2026-10-05 개편)
103
+
104
+ 사용자가 회색 배경을 금지했으므로 설정 그룹은 채워진 카드 대신 투명 배경·구분선·여백으로 구획한다.
105
+ 프로필, 화면과 언어, 알림과 소리, 계정과 도움말 순으로 구성한다. 선택 값은 행 설명에 두어
106
+ 큰 글자에서 제목과 가로 폭을 경쟁하지 않게 한다. 테마·언어는 세로 선택 시트, 프로필은
107
+ 저장/닫기가 가능한 입력 시트를 쓴다. 테마는 예제 provider에 즉시 반영하고 계정 저장은 제품이 소유한다.
108
+
109
+ ## 기본 화면 조합 확장
110
+
111
+ 댓글·검색·프로필은 아래 `screen-flows` 공개 조합을 소비하는 Web/Native 화면 예제로 제공한다.
112
+ 저장 목록은 기존 `ScreenLayout` 조합 예제를 유지한다. 댓글은 reply context와 composer를 사용하며 서버 전송/권한은
113
+ 제품 소유다. 검색은 로컬 fixture이고, 저장 해제는 되돌릴 수 있으며 프로필 수정은 저장 전까지
114
+ 별도 초안이다. 각 화면은 기본·다크·큰 글자·로딩·빈 상태·오류·제한 상태를 제공한다.
115
+
116
+ 알림은 번뚝의 날짜 구획과 판 없는 활동 행을 참고한다. 열람 즉시 서버 읽음 처리 정책까지
117
+ 공통 컴포넌트로 옮기지 않는다. 알림 설정과 원문 진입은 소비 화면 callback이 소유한다.
118
+
119
+ 채팅·댓글 입력창은 수동 resize를 제공하지 않고 1~5줄 범위에서 내용에 맞춰 커진다.
120
+ Web은 CSS content sizing, Native는 content-size event와 recipe line bounds를 사용한다.
121
+ 최대 높이 뒤에는 내부 스크롤하며, 빈 초안은 다시 한 줄로 돌아온다. 한 줄 시작은 MessageComposer
122
+ 내부 전용이다(Web `.hjm-message-composer__row`의 변수 재정의, Native 비공개 `hjmCompactMultiline`). 공개
123
+ TextArea의 명시적 `minVisibleLines`는 Web·Native 모두 일반 편집기의 80pt(`fieldRecipe.multilineMinHeight`)
124
+ 하한을 유지한다. 2026-10-06 리뷰에서 이 하한을 한 줄 control 최솟값(44pt)으로 낮춘 미게시 변경이 기존
125
+ `minVisibleLines={2}` 필드를 80pt에서 64pt로 줄여 되돌렸다.
126
+
127
+ 댓글 조합 예제는 부모 id로 답글을 묶고 펼침 상태를 별도로 유지한다. 답글 작성 시 대상과
128
+ 취소 동작을 표시하며 전송 후 초안을 정리한다. 예제 검색은 300ms 디바운스로 마지막 입력만
129
+ 갱신한다. 서버 검색의 요청 취소/응답 경합과 실제 댓글 전송 성공 판정은 제품 계층의 책임이다.
130
+
131
+
132
+ ## 기본 흐름 공개 조합 (2026-10-05)
133
+
134
+ `@hjmds/react/screen-flows`와 `@hjmds/react-native/screen-flows`는 선택적 진입점이다.
135
+ 루트 barrel에는 추가하지 않는다. 화면 전체의 반복되는 상태 연결을 재사용하되, 기존 Grid,
136
+ UploadItem, RadioGroup, AlertDialog, TextField와 ScreenLayout의 계약을 그대로 합성한다.
137
+ 기존 컴포넌트는 개별 UI가 필요한 경우, 이 진입점은 화면 수준 흐름이 필요한 경우 선택한다.
138
+
139
+ | API | 제공하는 흐름 | 제품이 연결할 것 |
140
+ | --- | --- | --- |
141
+ | ListDetailScreen | 목록 유지, 상세 전환, 새로고침/추가 로딩 action | 데이터, 페이지 커서, 라우팅 |
142
+ | EditorScreen | 수정 중 닫기 확인, 저장 busy, 초안 안내 | 검증, 영속 초안, 저장, 라우터/OS 뒤로가기 guard |
143
+ | ProfileScreen | 요약, 수정 진입, 계정 action 슬롯 | 계정 정보, 인증, 탈퇴/로그아웃 |
144
+ | ModerationScreen | 사유 선택, 신고 활성 조건, 차단 확인 | 서버 신고/권한, 차단 mutation |
145
+ | MediaSelectionScreen | 썸네일 격자, 업로드 상태, 순서/삭제/재시도 | 실제 picker, 권한, 이미지 URI 수명, 업로드 |
146
+ | SearchScreen | 300ms debounce, 이전 요청 AbortSignal, 검색/필터 슬롯, 기본 입력의 확정 신호(`onSubmit`)·검색 중 표시(`searching`)·label 숨김, 한 줄 가로 스크롤 필터 줄, 두 단계 검색 단계(검색 전·입력 중·결과)와 확정 경로, 최근·추천 검색어·제안 배치, 결과 개수·정렬, 적용 필터 칩, 초안/적용 필터 시트, 0건 원인별 복구, 개수 낭독 | API, 제안·결과·개수 조회, 오류 문구, 필터 정의와 계산, 정렬 값, 최근 검색 저장소, 늦은 응답 무시 |
147
+ | PermissionScreen | prompt/denied/granted/unavailable에 맞는 action | OS 요청과 설정 이동, 앱 복귀 후 실제 권한 조회 |
148
+ | OnboardingScreen | 단계 범위 검증, 이전/다음/건너뛰기 | 선택 데이터와 완료 여부 저장 |
149
+ | CommentThreadScreen | 부모/답글, 펼침, 좋아요/답글, 작성창 | 실제 댓글/권한/전송과 성공 후 초안 정리 |
150
+
151
+ `MediaSelectionScreen.actionLabels`는 짧은 표시 문구이며 removeLabel/moveUpLabel/moveDownLabel은
152
+ 각 사진 이름을 포함한 접근성 이름이다. 사진을 카드의 작은 leading 아이콘으로 줄이면 선택 내용을
153
+ 확인하기 어려워 preview를 독립적인 큰 썸네일로 둔다. 격자는 compact 2열, expanded 3열이다.
154
+ SearchScreen은 검색창과 필터만 상단에 유지하며 최근 검색은 본문과 함께 스크롤한다.
155
+ 필터 예제는 시트 내부 초안과 적용값을 분리해 취소하면 기존 결과를 유지한다.
156
+
157
+ ### 소비 예시
158
+
159
+ ```tsx
160
+ import { SearchScreen } from "@hjmds/react/screen-flows";
161
+
162
+ <SearchScreen
163
+ title={t("search.title")}
164
+ queryLabel={t("search.query")}
165
+ query={query}
166
+ onQueryChange={setQuery}
167
+ onSearch={(value, { signal }) => {
168
+ void searchApi(value, { signal }).then(result => {
169
+ if (!signal.aborted) setResults(result);
170
+ }).catch(error => {
171
+ if (!signal.aborted) setError(error);
172
+ });
173
+ }}
174
+ filters={<ProductFilters />}
175
+ >
176
+ <ProductResults items={results} />
177
+ </SearchScreen>
178
+ ```
179
+
180
+ Native는 import만 해당 renderer의 `screen-flows`로 바꾸고 플랫폼별 자식/host를 제공한다.
181
+ React Query 등을 쓰는 제품은 이 화면의 callback을 기존 제품 query 상태와 연결하며 별도 캐시를 만들지 않는다.
182
+ 필터 변경은 debounce query 변경과 독립적이므로 제품 query key에 필터도 포함한다.
183
+ 예제 미디어는 로컬 샘플 사진과 모의 업로드이며 기기 갤러리·네트워크 업로드를 실행하지 않는다.
184
+ 예제 권한은 상태 분기만 보여 주며 실제 OS 권한을 바꾸지 않는다.
185
+
186
+
187
+ ### 사진 라이브러리와 선택 이후의 구분
188
+
189
+ 2026-10-05 사용자가 파일 관리 카드처럼 보이는 사진 선택 화면의 재개편을 요청했다.
190
+ [Android Photo Picker의 실제 화면](https://developer.android.com/training/data-storage/shared/photo-picker)을
191
+ 확인해 조밀한 3열 격자, 사진 위 선택 표시, 고정된 하단 완료 영역을 실험 예제에 적용했다.
192
+ 선택 순서 숫자는 Apple Photos picker의 ordered selection 개념도 참고했다.
193
+ 회색 카드 면은 사용하지 않는다. 원본 UI의 브랜드 자산이나 구현 코드를 복사하지 않는다.
194
+
195
+ `MediaSelectionScreen.library`는 제품이 공급하는 사진 라이브러리/virtualized picker 슬롯이며,
196
+ 지정하면 선택 이후의 UploadItem 목록 대신 이 슬롯을 표시한다. `selectionSummary`는 하단의
197
+ 선택한 사진·개수·오류 안내 슬롯이다. 기존 props는 호환되며, library를 생략하면 업로드 검토
198
+ 흐름을 계속 쓸 수 있다. 실제 OS picker는 제품 host에서 실행해야 한다.
199
+ 실험은 12개 로컬 사진의 선택/해제/순번/5장 상한/앨범 필터/실패 복구를 모델링한다.
200
+ 검색 예제의 필터 행은 inline Stack의 center 정렬을 명시해 버튼과 정렬 요약의 다른 높이를 맞춘다.
201
+
202
+ ## 기본 화면의 레퍼런스 적용
203
+
204
+ 사용자의 후속 요청으로 12개 기본 화면을 익숙한 제품 패턴으로 정리했다.
205
+ `EditorScreen.submitPlacement="header"`는 상단 저장을 지원하며 기본값은 footer로 호환된다.
206
+ `ModerationScreen.reasonPicker`는 단계형 사유 목록 슬롯이며 기존 선택 유효성 검사·차단 확인을 유지한다.
207
+ ScreenLayout은 작은 화면에서 back/title/action을 한 행에 두고 글자 확대 시에만 여유 있게 줄바꿈하도록
208
+ 제목 폭을 recipe에서 조정한다. 참고 출처·공개 API 선택 근거·검증 범위는
209
+ [기본 화면 재구성 기록](../../../docs/plans/basic-screens-reference-refresh-2026-10-05.md)에 있다.
210
+
211
+ ## DM 반응과 여러 사진 작성 (2026-10-05)
212
+
213
+ 사용자가 제품에 구현한 DM 길게 누르기와 여러 사진 작성을 디자인 시스템에도 반영하도록 요청했다.
214
+ 공개 API map의 `ChatMessage`, `MessageComposer`, `TextArea`, `ReactionPicker`를 비교한 뒤
215
+ 새 입력기·이모지 컴포넌트 대신 기존 조합을 확장했다. 하단 상시 이모지 입력 행은 제공하지 않는다.
216
+
217
+ - `MessageComposer.sendIcon`을 주면 빈 입력에서는 `attachmentAction`을, 글이나 사진이 있으면
218
+ 전송 아이콘을 입력창 안에 표시한다. 생략하면 기존 텍스트 전송 버튼을 유지한다.
219
+ - `attachments`는 `{ id, preview, removeLabel }[]`, 삭제는 `onRemoveAttachment(id)`다.
220
+ id는 비어 있지 않고 유일해야 하며 삭제 이름은 제품에서 번역한다. 여러 사진은 가로로 표시한다.
221
+ 사진만 있어도 전송 가능하고 pending/disabled일 때 전송·사진 선택·삭제를 잠근다.
222
+ - `onSend(value)`는 현재 문자열을 그대로 전달한다. 사진 목록은 제품의 controlled state에서 읽는다.
223
+ 성공 여부를 HJM이 알 수 없으므로 글과 사진을 자동 삭제하지 않는다. 제품이 서버 성공 후 정리한다.
224
+ - `ChatMessage.reactions`는 기존 `ReactionPickerProps`와 `closeLabel`을 받는다.
225
+ 450ms 길게 누르면 열리고 Web에서는 10px 이동·pointer cancel로 보류를 취소한다.
226
+ 키보드 Enter/Space·우클릭, Native 접근성 activate도 지원한다. 같은 반응 재선택은 null이다.
227
+ 집계 배지는 제품 데이터에 맞게 기존 `actions` 슬롯에서 표시한다.
228
+ - `ReactionPicker.layout="strip"`은 줄바꿈 대신 가로 스크롤한다. 기본 `wrap`은 유지한다.
229
+ Native 메시지 자식은 메뉴에서 미리보기로도 렌더링되므로 표시용 콘텐츠로 제공한다.
230
+
231
+ Web의 Escape/외부 클릭/포커스 복귀는 기존 Popover 계약을 따른다. Native는 Modal의 뒤로가기·
232
+ 외부 누르기·닫기 버튼을 사용한다. iOS는 닫힘 완료, Android는 modal 제거 후 접근성 포커스를 복귀한다.
233
+ 기기 사진 권한, 최대 첨부 수, URI 수명, 업로드, 반응 API·권한·낙관적 업데이트는 제품 소유다.
234
+ 실험 스토리는 로컬 사진만 쓰며 실제 갤러리나 서버에 접근하지 않는다.
235
+
236
+ 이 API는 현재 소스·스토리 단계다(스토리는 2026-10-06 스토리북 배포, 패키지는 미게시). Utilverse·BurnTok의 1.12.1 설치본을 이 소스와 동일하다고
237
+ 보지 않는다. 공식 패키지 게시 후 중앙 `sync-design-system.mjs` 계획 검토·release record 갱신,
238
+ 제품 dependency/lock/contract 갱신과 각 표면 회귀 검증을 거쳐 소비 코드를 교체한다.
239
+ 로컬 file 의존성으로 게시 절차를 우회하지 않는다.
240
+
241
+ 번들 측정에서 Web screens는 기존 기록 9모듈/62.2kB raw/14.2kB gzip에서
242
+ 13모듈/97.7kB/22.3kB로 늘었다. 반응 helper·ReactionPicker·Popover·portal을 합성한 비용이다.
243
+ 포커스/충돌 처리를 재구현하는 대신 해당 선택 진입점의 구조 기준을 갱신했다.
244
+ Native는 13모듈/160.0kB/32.2kB이며 바이트 기준은 유지한다. 선택 peer·루트 barrel 유입은 없다.
245
+ 이 수치는 import graph 측정이며 실기기 프레임·터치 반응 성능을 의미하지 않는다.
246
+
247
+ ### 추가 이모지와 메시지 답장
248
+
249
+ 2026-10-05 후속 요청으로 `ReactionPicker.more={label, options}`를 추가했다. + 버튼은 전체 전달
250
+ 목록을 펼치며 선택하면 접힌다. `options`와 `more.options`의 id는 합쳐서 유일해야 한다.
251
+ 빠른 목록에 없는 반응도 선택값으로 유지/해제할 수 있다. 이모지 목록과 지역화 이름은 제품이
252
+ 공급한다. 실험에는 5개 빠른 반응과 32개 추가 예제가 있으며 Unicode 전체를 번들한 것은 아니다.
253
+
254
+ 기존 `SwipeActions`는 행 작업을 펼치는 컴포넌트이며 Native optional gesture peer를 요구한다.
255
+ 채팅 답장은 손을 놓을 때 바로 대상을 지정하는 다른 동작이어서 core `ChatMessage.replyAction`
256
+ (label/onPress/disabled)으로 제공한다. Web pointer, Native PanResponder는 공통 `isReplySwipe`의
257
+ 60px·세로 이동 대비 2배 이상 조건을 사용한다. 세로 스크롤·취소에는 답장하지 않는다. 사용자 지시에 따라 눈에 보이는 답장 버튼은 두지 않는다.
258
+ 키보드는 메시지에 포커스 후 Alt+좌/우 방향키, Native 접근성은 메시지의 답장 custom action을 쓴다.
259
+ 애니메이션 없는 직접 이동 피드백이며 일반 터치에서는 좌우 스와이프로만 답장을 시작한다.
260
+
261
+ `MessageComposer.replyTo`는 author/excerpt/cancelLabel/onCancel이며 취소는 제품 callback만
262
+ 호출한다. 글·첨부·reply id를 서버에 보내고 성공 후 지우는 책임은 제품에 있다.
263
+ `ChatMessage.replyLink`(label/onPress)를 주면 기존 reply 슬롯을 반응 trigger 밖의 버튼으로
264
+ 제공해 중첩 버튼을 피한다. 원문 id·페이지 추가 로딩·삭제된 메시지 안내·가상 목록의 scrollToIndex는
265
+ 제품이 처리한다. 기본 Native scroll="screen"에서는 `scrollRef`를 사용할 수 있다.
266
+ 실험은 모든 메시지가 로컬에 있는 작은 목록이며 인용 선택 시 원문 이동·잠깐 강조를 보여 준다.
267
+
268
+ ### 사진 없는 댓글 입력
269
+
270
+ 2026-10-05 사용자 요청으로 댓글 실험도 `MessageComposer`를 재사용한다. `sendIcon`만 전달하고
271
+ attachmentAction/attachments는 전달하지 않는다. 빈 입력에는 전송 아이콘이 없고 글을 입력하면
272
+ 입력창 오른쪽 안에 나타난다. 높이는 DM과 동일하게 1~5줄로 자동 조절한다. 별도 댓글 전송 버튼을 없앴다.
273
+ `inputRef`는 기존 TextArea의 host ref를 전달해 댓글 답글 선택 후 포커스를 유지한다.
274
+ 답글 대상 취소는 초안을 지우지 않으며, 댓글 목록의 답글 탐색 동작은 그대로다.
275
+
276
+ 사용자가 첨부한 댓글 화면 사진이 코드 추정보다 우선한다. 댓글은 `sendPresentation="circle"`로
277
+ 입력창 안의 primary 파란 원형 버튼에 흰색(onPrimary) ArrowUp 20px / strokeWidth 2를 표시한다.
278
+ 댓글과 DM 실험 모두 이 표현을 사용한다. 기존 소비자의 호환성을 위해 sendPresentation 기본값은 inline을 유지한다.
279
+
280
+ Attachment previews mask only the photo; removal controls remain outside that rounded mask, aligned to the top and trailing edges. The close mark is fixed-size iconography rather than scalable body text.
281
+
282
+ When a product route already owns navigation and safe-area gutters, `header` preserves that navigation and `contentInset="none"` prevents double padding. Shared layout still owns screen states and the content/footer boundary.
283
+
284
+ MessageComposer exposes maxLength, sendDisabled, additionalContent and leadingAction for product validation and tool sharing. A disabled send leaves draft editing available; additionalContent activates tool-only sends without inserting fabricated text.
285
+
286
+ SearchScreen accepts a queryField slot for an existing accessible SearchField with clear/busy controls; its debounce and request cancellation remain shared.
287
+ 2026-10-06 search redesign: optional `onSubmit` (Enter / keyboard search key on the default field, ignored with `queryField`, IME composition ignored on Web) separates typing from committing, and optional `filtersOverflow` (`wrap` default, `scroll`) keeps the filter chips on one edge-to-edge scrolling line so large text cannot grow the pinned area row by row. Both are additive; defaults keep the previous behavior. Layout and states: [usage/screens/common-search.md](usage/screens/common-search.md).
288
+
289
+ ### SearchScreen 두 단계 검색
290
+
291
+ 2026-10-06 사용자 위임 결정(권장안 채택): 검색 화면 개편의 상태·흐름이 Storybook 미리보기 코드에만 있어 소비 앱이
292
+ 가져다 쓸 수 없었다(AGENTS.md "Storybook 예제만으로 제공 완료가 아니다"). 그래서 그 로직을 `SearchScreen`의 선택 prop으로
293
+ 올렸다. 모두 추가이며, 새 prop을 주지 않으면 이전과 같은 DOM·동작이다. 설계 근거(참고 서비스 조사, 쟁점별 선택)는
294
+ 같은 날의 검색 화면 개편 설계를 따른다. 요약은 다음과 같다.
295
+
296
+ | 결정 | 이유 | 버린 대안 |
297
+ | --- | --- | --- |
298
+ | `committedQuery`를 주면 두 단계 검색. 단계는 `resolveSearchScreenPhase(query, committedQuery)`가 정한다(공백 = 검색 전, 확정값과 다름 = 입력 중, 같음 = 결과, 앞뒤 공백 무시) | 참고 서비스 공통의 "입력 중 제안 → 확정 → 결과" 구분. 기존 한 단계 검색(`committedQuery` 없음)은 그대로 둔다 | 입력 즉시 결과만 지원(제안 단계가 없어 최근 검색 저장 시점이 없음) |
299
+ | 모든 확정(Enter·검색 키, "‘q’ 검색" 행, 제안, 최근 검색, 추천 검색어)은 `onSubmit` 하나로 간다. 값은 `resolveSearchCommit`으로 앞뒤 공백을 지우고 공백만이면 부르지 않는다. 고른 값이 입력과 다르면 먼저 `onQueryChange`를 부른다 | "확정한 검색만 최근 검색에 넣는다"를 API가 지킨다. debounce된 `onSearch`는 `onSubmit`에 닿지 않는다 | 행마다 다른 콜백(제품이 저장 규칙을 경로마다 다시 맞춤) |
300
+ | 입력 중에는 `filters`·필터 시트 trigger를 숨기고, 검색어 때문에 0건이면(`resolveSearchEmptyCause` = `query`) 레일을 숨긴다. 필터 때문에 0건이면 레일을 남기고 "모두 해제"를 EmptyState 행동으로 둔다 | 입력 중 고정 영역을 줄인다. 필터가 원인인데 레일을 숨기면 원인을 지울 길이 사라진다 | 참고 서비스처럼 0건이면 항상 숨김 |
301
+ | 필터 시트는 열 때 `value`를 초안으로 복사하고, 어떤 방식으로 닫혀도 초안을 버리며, 주 행동에서만 `onApply(draft)`. 주 행동 문구는 `count(draft)`로 실시간, 0이면 비활성. 초기화는 항상 같은 자리, 초안이 기본값이면 비활성 | 기존 `여러 조건 적용과 초기화` 규칙과 eBay·Baymard 권고("N개 결과 보기"). 위치 흔들림 방지 | 시트 안에서 즉시 적용(조합 필터는 결과가 계속 바뀜) |
302
+ | 시트 열림은 제품이 제어(`open`/`onOpenChange`). 섹션만 여는 패싯 칩은 제품이 자기 범위를 정하고 `open`을 켠다 | 필터 정의·범위는 제품 소유. HJM에 범위 개념을 넣으면 제품 필터 모델을 강제한다 | `scope` 문자열을 HJM이 소유 |
303
+ | 레일 첫 칩(`filterSheet.trigger`)은 HJM이 그리고 이름에 적용 개수를 넣는다(Web `aria-haspopup="dialog"`) | 적용 칩·모두 해제 뒤 포커스가 돌아갈 고정 대상이 필요하다 | 제품이 trigger를 그림(포커스 복귀 대상을 HJM이 모름) |
304
+ | Web: 적용 칩·최근 검색 행을 지우면 같은 자리 다음 항목, 없으면 trigger(최근 검색은 입력)로 포커스. 목록이 실제로 줄어든 뒤에 옮긴다(`resolveFocusAfterRemoval`). Native는 옮기지 않는다 | 지운 버튼이 사라져 포커스가 `<body>`로 떨어지는 것을 막는다. Native는 프로그램 포커스 이동이 화면을 다시 읽게 해 낭독 커서를 그대로 둔다 | 항상 입력으로 이동(칩 줄 탐색이 끊김) |
305
+ | 개수 낭독은 HJM: 입력 중 "제안 N개", 결과 개수, `searching`이면 `searchingLabel`. Web은 숨긴 `role="status"`, Native는 `announceForAccessibilityWithOptions(queue)`. 문구가 바뀔 때만 | 제품마다 live region을 다시 만들면 중복·누락 낭독이 생긴다 | 제품 소유 |
306
+ | `resultSummary.count === null`이면 결과 머리 개수 자리에 Skeleton, `children` 대신 로딩 행 4개(`searchScreenRecipe.loadingRows`). Web은 ListRow `loading`(행 높이 유지) | 결과가 도착할 때 목록이 뛰지 않는다 | spinner로 본문 교체(`state="loading"`, 입력·필터·이전 결과가 사라짐) |
307
+ | 정렬은 결과 머리 끝 Menu(single). 바꾸면 본문을 맨 위로 | Menu 지침이 정렬을 용도로 명시. 시트 안 정렬은 정렬 하나에 시트 왕복이 필요 | 필터 시트 안 정렬 |
308
+ | `queryLabelVisibility="hidden"`: 보이는 label을 빼고 `queryLabel`을 접근성 이름(Web `aria-label`, Native `accessibilityLabel`)과 placeholder로 쓴다 | 참고 서비스 공통 모양. 1배 약 22, 2배 약 40만큼 고정 영역을 아낀다(2026-10-06 실측). placeholder가 검색 대상을 알린다(Apple HIG) | placeholder 없이 숨김(보이는 이름이 없는 입력) |
309
+ | `searching`+`searchingLabel`: Web `loading`, Native `busy`+`busyLabel`을 한 이름으로. 둘 다 주거나 둘 다 생략(타입) | 2026-10-06 utilverse 적용 조사: 진행 표시 하나 때문에 제품이 입력을 `queryField`로 통째로 바꿨다 | 플랫폼별 이름 그대로 노출 |
310
+
311
+ 2026-10-06 릴리스 전 정리에서 Native `SearchField`도 `busy`인 동안 입력을 받게 했다(Web `loading`과 같다). 그 전에는 입력 변경을 무시해,
312
+ `searching`을 입력 중 제안 조회에 켜면 그동안 친 글자가 사라졌고 지침이 "확정 결과 요청에만"으로 제한했다. 진행 표시가 입력을
313
+ 막을 이유가 없고(값을 저장하는 중이 아니라 결과를 불러오는 중) 두 플랫폼 동작이 달라 제품이 분기해야 했기 때문이다.
314
+ 입력 잠금을 유지하고 지침으로 제한하는 안은 플랫폼 차이를 제품에 떠넘겨 버렸다([search-field 지침](usage/components/search-field.md) 플랫폼 차이).
315
+
316
+ Chat message reactions may supply a localized menuAction for product edit/history/report tools. Native invokes it after dismissing the reaction modal, preventing two iOS modal surfaces from competing. Reply remains gesture-only.
317
+
318
+ Comment rows support product-owned actions and an explicit likeAction slot (null omits the default button). This preserves emoji receipt/report/edit controls without nesting interactive buttons; canReply/replyDisabled carry product permissions. threadFooter owns cursor pagination. Native ScreenLayout accepts host refreshControl/keyboard scroll props.
319
+
320
+ ## 댓글 본문 흐름 · 2026-10-05 실캡처 보정
321
+
322
+ Utilverse 실제 화면을 사용자 사진과 비교하자 이름·시각·본문을 각각 쌓고 반응 버튼을
323
+ 아래에 배치해 댓글 한 개가 불필요하게 높아졌다. `CommentThreadItem.bodyText`를 전달하면
324
+ 작성자와 본문을 하나의 줄 흐름으로 연결하고 `likeAction`은 오른쪽 끝에 유지한다.
325
+ `body`는 사진·숨김 안내 등 추가 콘텐츠용이다. 기존 rich body 소비자는 bodyText를
326
+ 생략하면 기존 구조를 유지한다. Native는 nested Text, Web은 inline span으로 같은 의미를
327
+ 구현한다. likeAction의 실제 저장·긴 누르기·이모지 선택은 제품 계약을 유지한다.
328
+
329
+ The 2026-10-05 device audit also found repeated metadata making message groups look separate. Hosts may send empty author/time strings within a group; both renderers omit those empty rows. Native state titles and descriptions center wrapped text so denied and empty views keep the shared alignment.
330
+
331
+ Photo albums and tool links set `ChatMessage.interactiveContent` so native accessibility keeps their child controls. A separate reaction target retains menu and reply actions. Web keeps the message as a keyboard-focusable group and ignores nested control presses, including Enter/Space on a nested link or button (only a key pressed on the bubble itself opens reactions; 2026-10-06 review). The visually hidden reaction popover header becomes visible while its Close action holds focus, so Shift+Tab never lands on an invisible target. The native reaction modal scrolls large previews while its Close control remains inside the safe area; the 2026-10-05 album capture exposed both issues.
332
+
333
+
334
+ ## 사진 촬영과 앨범 선택 (2026-10-05)
335
+
336
+ `PhotoSourceSheet`는 `Sheet`와 `Button`의 보조 조합이며 `MediaSelectionScreen`이나
337
+ 업로드 컴포넌트를 대체하지 않는다. 사용자 요청으로 사진 버튼 하나에서 앨범·촬영을 선택한다.
338
+ `labels`는 제품 지역화 문구이며 색은 현재 HJM provider/theme을 따른다.
339
+ `cameraAvailable=false`는 촬영 경로를 숨긴다. HJM은 Expo/브라우저 카메라 SDK를 의존하지 않는다.
340
+
341
+ Native는 시트가 실제 닫힌 뒤 `onSelect`를 호출한다. Web은 브라우저 사용자 활성화를 보존하기
342
+ 위해 클릭 콜스택에서 호출한다. `onSelect`에서 Native는 카메라 권한을 요청한 뒤 촬영,
343
+ Web은 별도 `input[type=file][capture=environment]`를 클릭한다. 브라우저/기기가 capture를
344
+ 지원하지 않으면 OS 파일 선택 화면으로 fallback할 수 있으며 실제 촬영을 보장하지 않는다.
345
+ 앨범 input은 capture 없이 유지해 촬영 강제가 앨범 선택을 막지 않도록 한다.
346
+ 취소·권한 거부·기기 부재는 기존 초안과 첨부를 보존한다. 권한 거부에는 앨범 대안을 안내한다.
347
+ 사진 처리·EXIF 제거·크기/개수 제한·업로드·세션 수명은 제품 계약을 그대로 사용한다.
348
+ 댓글처럼 사진을 허용하지 않는 입력에는 이 조합을 추가하지 않는다.
349
+
350
+
351
+ ## 저장 컬렉션 화면
352
+
353
+ 2026-10-06 사용자 요청으로 SavedItemsScreen을 실험 API(`./saved-items`)로 추가했다.
354
+ ListDetailScreen의 mount 유지·뒤로가기와 ScreenLayout 상태, Grid의 반응형 배치를 재사용한다.
355
+ 일반 목록과 다른 컬렉션 표지 2열·게시물 3열·전체/컬렉션 해석이 공통 규격이므로 별도 화면 이름을 둔다.
356
+ `resolveSavedItems`는 Web·Native가 같은 소실 항목/삭제 컬렉션 정책을 사용하도록 contracts에 둔다.
357
+ 저장소나 서버 요청을 포함하지 않으며 제품의 상태를 연결한다. [사용 지침](usage/components/saved-items-screen.md).
358
+
359
+ 헤더 슬롯은 단계마다 소유자가 다르다. 컬렉션 홈에서는 HJM이 `actions`(새 컬렉션 버튼)를,
360
+ 제품이 `leading`을 가진다. 컬렉션 안에서는 HJM이 `leading`(뒤로)을, 제품이 `actions`를 가진다.
361
+ 그래서 홈에서는 제품 `actions`가 표시되지 않는다. `onBack`은 상세와 컬렉션 양쪽에서 호출되며
362
+ 제품이 한 단계만 되돌린다(상세가 열려 있으면 `selectedItemId`, 아니면 `collectionId`를 비운다).
363
+ Web·Native가 같은 규칙이라 홈 actions 병합이나 콜백 분리는 양 플랫폼 API 결정으로 남긴다(2026-10-06 리뷰).
364
+
365
+ ## 2026-10-06 Web 리뷰 보정
366
+
367
+ - ListDetailScreen은 처음부터 detail이 열린 채 mount(딥링크)되면 focus를 옮기지 않는다.
368
+ 목록→상세 사용자 전환에서만 뒤로 버튼으로 옮기고, 닫을 때 진입점으로 되돌린다.
369
+ - ReactionPicker는 카탈로그 전용 이모지를 고르면 접히면서 그 버튼이 사라지므로 focus를
370
+ 더보기(+) 버튼으로 옮긴다. 빠른 반응은 mount가 유지돼 focus도 그대로다. Popover 안에서는
371
+ Popover가 trigger로 되돌린다.
372
+ - ReactionPicker 이모지 크기는 `layout="strip"`에서만 `--hjm-type-title-size`(Native strip의
373
+ `Text variant="title"`과 같은 단계)로 키운다. 기본 `wrap`은 게시된 버튼 글자 크기를 유지한다.
374
+ - `./screens`, `./screen-flows`, `./saved-items`의 화면 루트는 ScreenLayout을 통해
375
+ `layoutStyle`을 받는다(Native ScreenLayout과 같음). ListDetailScreen·SavedItemsScreen은
376
+ 목록·상세를 감싸는 바깥 host에 적용한다. PhotoSourceSheet는 portal Sheet라 제외한다.
package/docs/sheet.md CHANGED
@@ -63,3 +63,15 @@ focus/dismiss와 텍스트 배치 회귀를 직접 확인하도록 위 테스트
63
63
  [ScrollView](https://reactnative.dev/docs/scrollview).
64
64
 
65
65
  Native accessibility follow-up (2026-10-01): at 200% text scale, the close glyph was clipped inside the fixed IconButton frame. Dialog and Sheet now render that decorative glyph at a fixed icon size, matching Toast; title/body text still scales and the named close action and touch target are preserved. `sheet-viewport.test.tsx` checks both renderers and close callbacks.
66
+
67
+ ## Backdrop focus preservation (2026-10-03)
68
+
69
+ Diairy QA W16 reproduced Chrome default backdrop blur undoing Dialog focus return; Sheet used the same handler. Both Web renderers prevent the backdrop-only mousedown default while retaining dismissal policy and busy guards. Inner controls keep their default pointer behavior. Native has no DOM mousedown default and its host focus path is unchanged. `modal-outside-focus.browser.test.tsx` uses real pointer input rather than synthetic event dispatch to verify return focus, next Tab order, and focus containment while busy.
70
+
71
+ ## 열림 높이 `size`와 여백 (2026-10-06)
72
+
73
+ - `sheetRecipe.sizes.full`(1)은 위쪽 안전 영역 안의 전체 높이다. `content.maxHeightRatio`(0.9)는 `size="auto"`만 제한한다.
74
+ 이전에는 두 renderer 모두 `full`도 90%에서 멈췄다(Web `max-block-size: 90dvh`, Native `maxHeight` 0.9).
75
+ - Web 여백은 Native와 같은 recipe 값이다: 위아래 `content.paddingTop/Bottom`(sm 12), 좌우 `paddingHorizontal`(lg 20),
76
+ 머리·본문·footer 사이 `body.gap`(md 16), footer 위 `footer.paddingTop`(sm 12). Web은 Dialog 여백(20)을 쓰고 있었다.
77
+ 본문 스크롤 상자는 자식 포커스 링이 잘리지 않도록 4px 안쪽 여백과 같은 크기의 음수 margin을 둔다(보이는 간격은 recipe 값).
package/docs/splitter.md CHANGED
@@ -67,9 +67,15 @@ Native `unsupported`다. 제품 채택과 실제 보조기기 실측은 승격
67
67
  크기를 저장하는 owner가 의미 없는 쓰기를 하지 않도록.
68
68
  - 구현 중 실제 결함을 하나 잡았다: `onValueChangeEnd?.(commit(next))`는 handler가 없으면
69
69
  인자 평가까지 통째로 건너뛰어 키보드 조절이 조용히 죽는다. commit을 먼저 하고 알린다.
70
- - 로컬 검증: `test/splitter.browser.test.tsx` 6개(separator 의미·수직 방향과 44px hit
70
+ - **pane은 넘칠 때만 Tab 정지점이 된다(2026-10-06 리뷰).** pane은 `overflow: auto`라
71
+ focusable 콘텐츠가 없으면 키보드로 스크롤할 수 없다(axe `scrollable-region-focusable`).
72
+ 항상 `tabIndex=0`을 주면 separator 앞뒤에 이름 없는 빈 정지점 두 개가 생겨, 크기와
73
+ 자식 변화를 관찰해 실제로 넘칠 때만 `tabIndex=0`을 붙인다. pane 이름(prop)을 요구하는
74
+ 대안은 공개 API 추가·필수화가 필요해 택하지 않았다.
75
+ - 로컬 검증: `test/splitter.browser.test.tsx` 7개(separator 의미·수직 방향과 44px hit
71
76
  target, 방향키 step과 Home/End 경계, 드래그 스냅과 드래그당 1회 end, RTL 드래그·키보드,
72
- disabled, 실제 Tab focus와 focused keyboard resize)와 `컴포넌트/레이아웃/Splitter`.
77
+ disabled, 실제 Tab focus와 focused keyboard resize, 넘칠 때만 생기는 pane 정지점)와
78
+ `컴포넌트/레이아웃/Splitter`.
73
79
 
74
80
  **검증 범위.** Web Chromium renderer matrix가 긴 pane 콘텐츠·환경·접근성 증거를 제공한다.
75
81
  제품 vertical slice, screen reader 실측, 모든 OS 조합은 보증하지 않으며 소비 앱 릴리스 QA에서 확인한다.
package/docs/theming.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # 테마 주입 — 내 브랜드색으로 시작하기
2
2
 
3
+ 검토일: 2026-10-06 (`value` 중심 예시를 1.5.0 `brandPalette` prop 경로로 정정)
4
+
3
5
  HJM은 `theme`(light/dark/system) 같은 **환경**과, 그 환경이 해석된 **값**을 분리해서
4
6
  받는다. 제품 브랜드색은 값 쪽에 넣는다. 이 문서는 새 제품이 처음 부딪히는 그 경로만
5
7
  설명한다. **무엇을 어디까지 바꿀 수 있는지와 대비 검사 규칙은 [brand-boundary.md](./brand-boundary.md)가
@@ -21,42 +23,47 @@ import { HjmProvider } from "@hjmds/react/provider";
21
23
  `theme="system"`이면 provider가 OS 설정을 읽고, `textScale`·`reducedMotion`도 같은
22
24
  방식으로 환경에서 해석한다. 이 경로는 HJM 기본 팔레트를 쓴다.
23
25
 
24
- ### 2. 제품 팔레트 주입 (`brandPalette`)
26
+ ### 2. 제품 팔레트 주입 (`brandPalette` prop)
25
27
 
26
28
  브랜드색은 별도 토큰 층을 만들지 않고 **HJM semantic key 위에 덮는다**. 넘긴 key만
27
29
  교체되고 나머지는 기본값을 유지하므로 recipe와 대비 규칙이 그대로 적용된다.
30
+ 1.5.0부터 Provider가 `brandPalette`를 prop으로 직접 받는다. 이 경로에서 Provider는 계속
31
+ OS theme·글자 크기·reduced motion을 관찰하고, 중첩 Provider는 가장 가까운 상위 `brandPalette`를 물려받는다.
28
32
 
29
33
  ```tsx
30
- import { resolveDesignSystemProviderValue } from "@hjmds/design-contracts/components/design-system-provider";
31
- import { HjmProvider } from "@hjmds/react/provider";
32
- import { useMemo } from "react";
33
-
34
- function ProductProvider({ preference, systemDark, children }) {
35
- const value = useMemo(
36
- () => resolveDesignSystemProviderValue(
37
- { theme: preference },
38
- {
39
- systemTheme: systemDark ? "dark" : "light",
40
- systemDirection: "ltr",
41
- systemTextScale: 1,
42
- systemReducedMotion: false,
43
- // 브랜드가 소유하는 key만 덮는다. 중성색·상태색은 HJM 기본값을 쓴다.
44
- brandPalette: {
45
- light: { primary: "#0F6FFF", contentBrand: "#0B57C7", borderControl: "#C9D3E0" },
46
- dark: { primary: "#5AA2FF", contentBrand: "#8CC0FF", borderControl: "#3A4757" },
47
- },
48
- },
49
- ),
50
- [preference, systemDark],
51
- );
52
- return <HjmProvider value={value}>{children}</HjmProvider>;
53
- }
34
+ import { HjmProvider, type HjmBrandPalette } from "@hjmds/react/provider";
35
+
36
+ // 제품이 정한 브랜드 값. 아래 hex는 형식 예시일 뿐 HJM이 권하는 기본값이 아니다.
37
+ const PRODUCT_BRAND_PALETTE = {
38
+ light: { primary: "#…", contentBrand: "#…", borderControl: "#…" },
39
+ dark: { primary: "#…", contentBrand: "#…", borderControl: "#…" },
40
+ } satisfies HjmBrandPalette;
41
+
42
+ <HjmProvider theme={preference} brandPalette={PRODUCT_BRAND_PALETTE}>
43
+ <App />
44
+ </HjmProvider>
54
45
  ```
55
46
 
56
- React Native는 `HjmNativeProvider`가 같은 `value`를 받는다. `value`를 넘기면 Provider가 OS 설정 관찰을
57
- 멈추므로, 위 예시처럼 system theme 등의 신호를 제품이 구독해 resolver에 넣는다. 실제 사용 예는 BurnTok의
58
- `apps/web/src/components/ThemeProvider.tsx`(경계선 두 key)와 `apps/mobile/src/components/ThemeProvider.tsx`
59
- (경계선 두 key + 표면 두 key)다.
47
+ React Native도 같은 모양이다(`<HjmNativeProvider theme={preference} brandPalette={…}>`,
48
+ 타입은 `HjmNativeBrandPalette`). 앱 안에서 사용자가 고른 light/dark/system 설정은 `theme` prop으로
49
+ 넘기면 되고, 그것 때문에 `value`로 내려갈 필요는 없다.
50
+
51
+ **값은 제품 것이다.** 2026-10-05 사용자 규칙: Showcase·Storybook·이 문서의 예시 색과 자산을 제품
52
+ 기본값으로 복사하지 않는다. 제품의 기존 디자인(`docs/DESIGN.md` 등)에서 브랜드 key를 정하고, 필요한 key만
53
+ 넘긴다. 중성색·상태색은 HJM 기본값을 쓴다.
54
+
55
+ #### `value` prop은 언제 쓰는가
56
+
57
+ `value`(`resolveDesignSystemProviderValue` 결과 전체)는 1.4까지 브랜드를 넣는 유일한 방법이었고, 지금도
58
+ 타입상 지원한다. 하지만 `value`를 넘기면 Provider가 OS 설정 관찰을 멈추고 `brandPalette` 상속도 끊기므로
59
+ 브랜드 경로로 쓰지 않는다([brand-boundary.md §1](./brand-boundary.md#1-지원하는-경로는-brandpalette-하나다)).
60
+ 남은 용도는 다음뿐이다.
61
+
62
+ - 테스트·스토리에서 환경을 결정적으로 고정할 때(SSR·테스트만 필요하면 `systemTheme` prop으로도 충분한지 먼저 본다).
63
+ - 이미 해석된 값을 다른 렌더 트리에 그대로 옮기는 임베딩(예: 상위 앱이 해석한 값을 별도 root에 미러링).
64
+
65
+ BurnTok의 `apps/web/src/components/ThemeProvider.tsx`·`apps/mobile/src/components/ThemeProvider.tsx`는
66
+ 2026-10-06 확인 시점에도 1.4식 `value` 경로를 쓴다. 이관 대상 사례로만 참고하고 새 제품의 출발점으로 복사하지 않는다.
60
67
 
61
68
  팔레트를 바꾸면 제품 테스트에서 대비 검사를 돌린다.
62
69
 
@@ -19,3 +19,10 @@ single 모드를 넣지 않는 것이 규칙이다.
19
19
  **tab stop.** 각 토글이 자기 tab stop이다. 도구 모음식 roving focus를 쓰지 않는 이유는
20
20
  묶음이 대개 2~4개로 짧고, roving은 "그룹 안에서 화살표로 이동"이라는 추가 학습을
21
21
  요구하기 때문이다. 항목이 많아지는 실제 화면이 나오면 그때 축을 연다.
22
+
23
+
24
+ ### 카테고리 필터 표현
25
+
26
+ 2026-10-06 요청에 따라 단일 선택 카테고리는 `SegmentedControl presentation="pills"`로 제공한다.
27
+ 복수 선택 ToggleGroup의 계약은 바꾸지 않는다. 필터 UI가 서로 비슷하더라도 선택 개수를 합치면 해제·키보드 의미가 달라지기 때문이다.
28
+ 자세한 크기·테마·배치는 [SegmentedControl 사용 지침](usage/components/segmented-control.md)을 따른다.