@hjmds/design-contracts 1.26.1 → 1.27.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 (278) hide show
  1. package/dist/agreement.d.ts +1 -0
  2. package/dist/agreement.d.ts.map +1 -1
  3. package/dist/agreement.js +2 -1
  4. package/dist/agreement.js.map +1 -1
  5. package/dist/auth-screen.d.ts +1 -1
  6. package/dist/behaviors.d.ts +1 -1
  7. package/dist/calendar.d.ts +1 -0
  8. package/dist/calendar.d.ts.map +1 -1
  9. package/dist/calendar.js +4 -1
  10. package/dist/calendar.js.map +1 -1
  11. package/dist/catalog.d.ts +78 -30
  12. package/dist/catalog.d.ts.map +1 -1
  13. package/dist/catalog.js +10 -5
  14. package/dist/catalog.js.map +1 -1
  15. package/dist/chart.d.ts +2 -0
  16. package/dist/chart.d.ts.map +1 -1
  17. package/dist/chart.js +1 -1
  18. package/dist/chart.js.map +1 -1
  19. package/dist/component-definitions.d.ts +1 -0
  20. package/dist/component-definitions.d.ts.map +1 -1
  21. package/dist/component-definitions.js +1 -0
  22. package/dist/component-definitions.js.map +1 -1
  23. package/dist/component-recipes.d.ts +53 -6
  24. package/dist/component-recipes.d.ts.map +1 -1
  25. package/dist/component-recipes.js +64 -10
  26. package/dist/component-recipes.js.map +1 -1
  27. package/dist/data-table.d.ts +11 -0
  28. package/dist/data-table.d.ts.map +1 -1
  29. package/dist/data-table.js +5 -0
  30. package/dist/data-table.js.map +1 -1
  31. package/dist/description-list.d.ts +3 -0
  32. package/dist/description-list.d.ts.map +1 -1
  33. package/dist/description-list.js +5 -1
  34. package/dist/description-list.js.map +1 -1
  35. package/dist/design-profile-layout.d.ts +28 -0
  36. package/dist/design-profile-layout.d.ts.map +1 -1
  37. package/dist/design-profile-layout.js +27 -3
  38. package/dist/design-profile-layout.js.map +1 -1
  39. package/dist/design-profile.d.ts.map +1 -1
  40. package/dist/design-profile.js +40 -8
  41. package/dist/design-profile.js.map +1 -1
  42. package/dist/field-group.d.ts +13 -0
  43. package/dist/field-group.d.ts.map +1 -1
  44. package/dist/field-group.js +17 -2
  45. package/dist/field-group.js.map +1 -1
  46. package/dist/foundations.d.ts +24 -11
  47. package/dist/foundations.d.ts.map +1 -1
  48. package/dist/foundations.js +40 -7
  49. package/dist/foundations.js.map +1 -1
  50. package/dist/heading.d.ts +5 -5
  51. package/dist/image-choice.d.ts +23 -0
  52. package/dist/image-choice.d.ts.map +1 -0
  53. package/dist/image-choice.js +26 -0
  54. package/dist/image-choice.js.map +1 -0
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/masonry.d.ts +8 -0
  60. package/dist/masonry.d.ts.map +1 -1
  61. package/dist/masonry.js +12 -1
  62. package/dist/masonry.js.map +1 -1
  63. package/dist/number-field.d.ts +2 -2
  64. package/dist/number-field.d.ts.map +1 -1
  65. package/dist/number-field.js +4 -2
  66. package/dist/number-field.js.map +1 -1
  67. package/dist/qr-code-recipe.d.ts +1 -0
  68. package/dist/qr-code-recipe.d.ts.map +1 -1
  69. package/dist/qr-code-recipe.js +3 -1
  70. package/dist/qr-code-recipe.js.map +1 -1
  71. package/dist/recipes.d.ts +1 -1
  72. package/dist/recipes.d.ts.map +1 -1
  73. package/dist/recipes.js +1 -1
  74. package/dist/recipes.js.map +1 -1
  75. package/dist/result.d.ts +9 -0
  76. package/dist/result.d.ts.map +1 -1
  77. package/dist/result.js +5 -0
  78. package/dist/result.js.map +1 -1
  79. package/dist/screen-patterns.d.ts +139 -2
  80. package/dist/screen-patterns.d.ts.map +1 -1
  81. package/dist/screen-patterns.js +140 -1
  82. package/dist/screen-patterns.js.map +1 -1
  83. package/dist/sheet.d.ts +15 -1
  84. package/dist/sheet.d.ts.map +1 -1
  85. package/dist/sheet.js +20 -1
  86. package/dist/sheet.js.map +1 -1
  87. package/dist/showcase.d.ts +27 -27
  88. package/dist/showcase.js +27 -27
  89. package/dist/showcase.js.map +1 -1
  90. package/dist/tag.d.ts +10 -2
  91. package/dist/tag.d.ts.map +1 -1
  92. package/dist/tag.js +3 -2
  93. package/dist/tag.js.map +1 -1
  94. package/dist/tags-input.d.ts +6 -0
  95. package/dist/tags-input.d.ts.map +1 -1
  96. package/dist/tags-input.js +3 -1
  97. package/dist/tags-input.js.map +1 -1
  98. package/dist/text-formats.d.ts +2 -2
  99. package/dist/top.d.ts +2 -2
  100. package/dist/transfer-list.d.ts +1 -1
  101. package/dist/transfer-list.d.ts.map +1 -1
  102. package/dist/transfer-list.js +2 -1
  103. package/dist/transfer-list.js.map +1 -1
  104. package/dist/upload-item.d.ts +1 -10
  105. package/dist/upload-item.d.ts.map +1 -1
  106. package/dist/upload-item.js +1 -2
  107. package/dist/upload-item.js.map +1 -1
  108. package/dist/version.d.ts +1 -1
  109. package/dist/version.js +1 -1
  110. package/dist/version.js.map +1 -1
  111. package/docs/activity-chip.md +4 -3
  112. package/docs/catalog-freeze.json +5 -2
  113. package/docs/chart.md +1 -1
  114. package/docs/consumer-policy.md +13 -6
  115. package/docs/description-list.md +4 -0
  116. package/docs/design-profile.md +28 -2
  117. package/docs/font-roles.md +1 -1
  118. package/docs/generated/component-maturity.md +5 -4
  119. package/docs/generated/renderer-evidence.json +92 -6
  120. package/docs/generated/renderer-evidence.md +5 -3
  121. package/docs/generated/showcase-manifest.json +49 -1
  122. package/docs/gravity-letters.md +7 -2
  123. package/docs/masonry.md +4 -2
  124. package/docs/screen-patterns.md +5 -2
  125. package/docs/showcase.md +7 -9
  126. package/docs/stable-promotion.md +13 -1
  127. package/docs/usage/README.md +40 -10
  128. package/docs/usage/SELECTION.md +30 -4
  129. package/docs/usage/STANDARD.md +11 -2
  130. package/docs/usage/components/account-switcher.md +98 -0
  131. package/docs/usage/components/activity-chip.md +4 -4
  132. package/docs/usage/components/badge.md +1 -1
  133. package/docs/usage/components/bezier-editor.md +99 -0
  134. package/docs/usage/components/bottom-cta.md +2 -1
  135. package/docs/usage/components/button.md +26 -12
  136. package/docs/usage/components/carousel.md +4 -4
  137. package/docs/usage/components/chart.md +1 -0
  138. package/docs/usage/components/chat-message.md +18 -12
  139. package/docs/usage/components/chat-screen.md +6 -3
  140. package/docs/usage/components/cohort-retention-table.md +83 -0
  141. package/docs/usage/components/collapsible.md +7 -2
  142. package/docs/usage/components/comment-thread-row.md +102 -0
  143. package/docs/usage/components/comment-thread-screen.md +27 -13
  144. package/docs/usage/components/composition-layout.md +1 -0
  145. package/docs/usage/components/context-menu.md +32 -15
  146. package/docs/usage/components/conversation-row.md +11 -4
  147. package/docs/usage/components/conversion-funnel.md +85 -0
  148. package/docs/usage/components/correlation-matrix.md +80 -0
  149. package/docs/usage/components/data-table.md +4 -0
  150. package/docs/usage/components/description-list.md +2 -1
  151. package/docs/usage/components/editor-screen.md +9 -5
  152. package/docs/usage/components/effect-surface.md +4 -2
  153. package/docs/usage/components/empty-state.md +2 -2
  154. package/docs/usage/components/event-attendance-card.md +104 -0
  155. package/docs/usage/components/expandable-search.md +105 -0
  156. package/docs/usage/components/file-picker.md +5 -0
  157. package/docs/usage/components/filter-recovery.md +97 -0
  158. package/docs/usage/components/follow-profile-card.md +100 -0
  159. package/docs/usage/components/heading.md +6 -2
  160. package/docs/usage/components/histogram-explorer.md +89 -0
  161. package/docs/usage/components/icon-button.md +18 -2
  162. package/docs/usage/components/icon-tile.md +12 -4
  163. package/docs/usage/components/image-choice-group.md +81 -0
  164. package/docs/usage/components/import-field-mapping.md +94 -0
  165. package/docs/usage/components/inline-edit.md +105 -0
  166. package/docs/usage/components/keyboard-avoidance-context.md +60 -0
  167. package/docs/usage/components/keyboard-avoiding.md +5 -1
  168. package/docs/usage/components/list-detail-screen.md +4 -4
  169. package/docs/usage/components/list.md +1 -1
  170. package/docs/usage/components/masonry.md +6 -7
  171. package/docs/usage/components/media-selection-screen.md +7 -5
  172. package/docs/usage/components/media-transport.md +104 -0
  173. package/docs/usage/components/message-composer.md +13 -3
  174. package/docs/usage/components/moderation-screen.md +5 -2
  175. package/docs/usage/components/notification-inbox-screen.md +3 -3
  176. package/docs/usage/components/notification-row.md +64 -8
  177. package/docs/usage/components/onboarding-screen.md +1 -1
  178. package/docs/usage/components/overview-screen.md +9 -3
  179. package/docs/usage/components/pagination.md +18 -3
  180. package/docs/usage/components/password-rules-field.md +90 -0
  181. package/docs/usage/components/permission-screen.md +1 -1
  182. package/docs/usage/components/photo-source-sheet.md +1 -1
  183. package/docs/usage/components/popover.md +6 -3
  184. package/docs/usage/components/pricing-choice.md +85 -0
  185. package/docs/usage/components/profile-summary.md +56 -8
  186. package/docs/usage/components/progress.md +3 -3
  187. package/docs/usage/components/qr-code.md +5 -9
  188. package/docs/usage/components/repeatable-field-group.md +92 -0
  189. package/docs/usage/components/reply-composer.md +83 -0
  190. package/docs/usage/components/reply-message.md +85 -0
  191. package/docs/usage/components/result.md +3 -3
  192. package/docs/usage/components/saved-items-screen.md +7 -5
  193. package/docs/usage/components/screen-layout.md +21 -10
  194. package/docs/usage/components/search-field.md +1 -1
  195. package/docs/usage/components/section.md +2 -2
  196. package/docs/usage/components/selection-action-bar.md +89 -0
  197. package/docs/usage/components/settings-screen.md +13 -11
  198. package/docs/usage/components/sheet.md +19 -7
  199. package/docs/usage/components/shortcut-recorder.md +77 -0
  200. package/docs/usage/components/split-action.md +94 -0
  201. package/docs/usage/components/streaming-answer.md +94 -0
  202. package/docs/usage/components/switch.md +6 -2
  203. package/docs/usage/components/table-column-settings.md +88 -0
  204. package/docs/usage/components/target-summary.md +79 -0
  205. package/docs/usage/components/text.md +11 -5
  206. package/docs/usage/components/toggle-feedback-button.md +94 -0
  207. package/docs/usage/components/top-bar.md +8 -12
  208. package/docs/usage/components/transfer-task.md +87 -0
  209. package/docs/usage/compositions/action-recovery-optimistic.md +18 -14
  210. package/docs/usage/compositions/action-recovery-save.md +18 -14
  211. package/docs/usage/compositions/action-recovery-undo.md +18 -14
  212. package/docs/usage/compositions/activity-status.md +1 -1
  213. package/docs/usage/compositions/adaptive-content.md +2 -1
  214. package/docs/usage/compositions/collection-detail.md +6 -5
  215. package/docs/usage/compositions/collection-editing.md +13 -6
  216. package/docs/usage/compositions/command-records.md +5 -4
  217. package/docs/usage/compositions/communication.md +7 -5
  218. package/docs/usage/compositions/compound-controls.md +7 -3
  219. package/docs/usage/compositions/content-transition-comparison.md +10 -5
  220. package/docs/usage/compositions/data-layouts.md +12 -16
  221. package/docs/usage/compositions/data-workflows.md +7 -4
  222. package/docs/usage/compositions/date-time-selection.md +5 -5
  223. package/docs/usage/compositions/design-profile-comparison.md +22 -6
  224. package/docs/usage/compositions/disclosure.md +14 -9
  225. package/docs/usage/compositions/document-resource.md +1 -1
  226. package/docs/usage/compositions/environment-matrix.md +16 -12
  227. package/docs/usage/compositions/expo-interactions.md +9 -8
  228. package/docs/usage/compositions/family-drawer.md +9 -5
  229. package/docs/usage/compositions/field-group.md +7 -3
  230. package/docs/usage/compositions/interaction-adapters.md +11 -9
  231. package/docs/usage/compositions/interaction-flow-apply.md +17 -15
  232. package/docs/usage/compositions/interaction-flow-search.md +11 -7
  233. package/docs/usage/compositions/live-list.md +12 -8
  234. package/docs/usage/compositions/navigation-bar-collection.md +8 -3
  235. package/docs/usage/compositions/optional-motion.md +2 -2
  236. package/docs/usage/compositions/paper-surface.md +11 -6
  237. package/docs/usage/compositions/purpose-input-comment.md +10 -4
  238. package/docs/usage/compositions/purpose-input-message.md +10 -4
  239. package/docs/usage/compositions/reference-first.md +4 -4
  240. package/docs/usage/compositions/reference-review.md +4 -4
  241. package/docs/usage/compositions/reference-settings.md +4 -4
  242. package/docs/usage/compositions/selection-motion.md +2 -1
  243. package/docs/usage/compositions/social-compositions.md +7 -6
  244. package/docs/usage/compositions/stea-event-ticket.md +2 -4
  245. package/docs/usage/compositions/stea-order-progress.md +19 -21
  246. package/docs/usage/compositions/stea-pixel-empty.md +26 -26
  247. package/docs/usage/compositions/task-controls.md +7 -2
  248. package/docs/usage/compositions/texture-comparison.md +5 -5
  249. package/docs/usage/compositions/toast-layout.md +7 -3
  250. package/docs/usage/compositions/toggle-feedback.md +1 -1
  251. package/docs/usage/compositions/visual-foundations.md +10 -5
  252. package/docs/usage/compositions/web-additions.md +5 -1
  253. package/docs/usage/screens/common-chat.md +5 -3
  254. package/docs/usage/screens/common-comments.md +14 -9
  255. package/docs/usage/screens/common-conversation-list.md +109 -0
  256. package/docs/usage/screens/common-inbox.md +5 -3
  257. package/docs/usage/screens/common-login.md +7 -4
  258. package/docs/usage/screens/common-profile.md +16 -12
  259. package/docs/usage/screens/common-saved.md +12 -8
  260. package/docs/usage/screens/common-search.md +16 -8
  261. package/docs/usage/screens/common-settings.md +12 -11
  262. package/docs/usage/screens/common-shell.md +12 -10
  263. package/docs/usage/screens/dashboard.md +59 -56
  264. package/docs/usage/screens/discovery-gallery.md +18 -9
  265. package/docs/usage/screens/flow-collection.md +10 -6
  266. package/docs/usage/screens/flow-editor.md +9 -6
  267. package/docs/usage/screens/flow-media.md +12 -7
  268. package/docs/usage/screens/flow-moderation.md +9 -3
  269. package/docs/usage/screens/flow-onboarding.md +13 -8
  270. package/docs/usage/screens/flow-permission.md +5 -3
  271. package/docs/usage/screens/landing.md +20 -14
  272. package/docs/usage/screens/mockup-studio.md +100 -88
  273. package/docs/usage/screens/notification-settings.md +9 -7
  274. package/docs/usage/screens/product-bento.md +22 -10
  275. package/docs/usage/screens/reference-comparison.md +2 -2
  276. package/docs/usage/tokens/font-roles.md +18 -2
  277. package/docs/usage/tokens/typography.md +22 -11
  278. package/package.json +7 -1
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-17
8
8
  - 근거: recipe `iconButtonRecipe`(`src/icon-button-recipe.ts`), NotificationBell 계약: [Compound controls](../../compound-controls.md#notificationbell)
9
9
  - 스토리북: `배포/컴포넌트/동작/아이콘 버튼` · `배포/컴포넌트/상태와 알림/알림 벨`
10
10
 
@@ -12,13 +12,22 @@
12
12
 
13
13
  보이는 글자 없이 아이콘만으로 표시하는 행동에 쓴다. 닫기, 뒤로, 더보기, 공유, 즐겨찾기 토글처럼
14
14
  모양이 널리 통하는 행동이다. 이름은 현지화한 `label`이 접근성 이름으로 읽힌다.
15
+
16
+ **도구성 행동은 글자 버튼이 아니라 IconButton으로 표시한다.** 새로고침·추가(새 기록·새 컬렉션)·편집·삭제·공유·
17
+ 검색 열기·필터·정렬·더보기·설정·복사·내려받기, 도구 막대의 닫기가 여기에 해당한다. 화면 헤더 `actions`, 목록·카드 행 끝,
18
+ 도구 막대에 둔다. 예전에 보이던 글자는 `label`로 옮겨 보조기술 이름으로 읽힌다. 아이콘은 제품이 넘긴다(HJM `Icon`
19
+ 또는 제품 아이콘 라이브러리). HJM 화면·구성이 이런 행동을 직접 그리면 아이콘 슬롯을 쓴다 —
20
+ `ScreenFlowAction.icon`(ListDetailScreen `refresh` 등), SavedItemsScreen `createCollectionIcon`, Popover `closeIcon`(Web).
21
+
22
+ 글자 버튼으로 남기는 것: 화면의 주 행동과 확인 행동(저장·완료·보내기·로그인), 상태 영역의 다시 시도,
23
+ Dialog·Sheet footer 행동, 목록 끝 "더 보기", 온보딩 다음·이전, 선택 일괄 행동처럼 결과를 글자로 확인해야 하는 행동.
15
24
  미확인 알림 수가 붙은 알림 진입점은 `NotificationBell`을 쓴다.
16
25
 
17
26
  ## 쓰지 않을 때
18
27
 
19
28
  | 상황 | 대신 쓸 것 |
20
29
  | --- | --- |
21
- | 글자 라벨이 있는 행동 | [Button](button.md) |
30
+ | 글자로 결과를 확인해야 하는 주 행동·확인 행동(저장·완료·보내기) | [Button](button.md) |
22
31
  | 다른 페이지·URL로 이동 | [Link](link.md) |
23
32
  | 화면 위에 떠 있는 주 행동 | [FloatingActionButton](floating-action-button.md) |
24
33
  | 여러 아이콘 중 하나를 고름 | [ToggleGroup](toggle-group.md), [SegmentedControl](segmented-control.md) |
@@ -102,6 +111,8 @@ TopBar
102
111
  ## 꼭 지킬 것
103
112
 
104
113
  - `label`은 모양이 아니라 행동을 말한다("돋보기"가 아니라 "검색"). Native는 빈 `label`·빈 children을 `TypeError`로 거부한다.
114
+ - 도구성 행동(새로고침·추가·편집·삭제·공유·검색·필터·정렬·더보기·설정·복사)을 헤더·행·도구 막대에 글자 버튼으로 두지 않는다.
115
+ 같은 행동이 여러 행에 있으면 `label`에 대상을 넣는다("할 일 A 삭제").
105
116
  - 안의 Icon은 장식으로 둔다. 이름은 버튼이 이미 읽는다.
106
117
  - 배치는 `layoutStyle`로만 한다. 지름·radius·배경을 `style`/`className`으로 덮지 않는다.
107
118
  - NotificationBell의 `label`에 미확인 수를 포함한 현지화 문구를 넣는다. 배지와 그림은 접근성에서 숨겨진다.
@@ -127,3 +138,8 @@ TopBar
127
138
 
128
139
  2026-10-10 사용자 요청: Button과 같은 선택 표현을 쓴다. 선택은 bg 바탕·contentBrand 아이콘으로
129
140
  표시하고 추가 선택 테두리는 없다. aria-pressed/Native selected와 키보드 focus outline은 유지한다.
141
+
142
+ ## 근거
143
+
144
+ - 도구성 행동은 IconButton: 2026-10-17 사용자 지시 — '나의 기록' 헤더의 새로고침·새 기록 글자 버튼.
145
+ - 선택 표현: 2026-10-10 사용자 요청 — Button과 같은 선택 표현을 쓴다.
@@ -3,8 +3,8 @@
3
3
  - 단계: 컴포넌트
4
4
  - 상태: 실험
5
5
  - 지원: Web · Native
6
- - 적용: 미게시(1.25.0 이후)
7
- - 검토일: 2026-10-15
6
+ - 적용: 1.26.0
7
+ - 검토일: 2026-10-16
8
8
  - 근거: `src/screen-patterns.ts`의 `iconTileRecipe`
9
9
  - 스토리북: 별도 예제 미등록
10
10
 
@@ -25,7 +25,7 @@
25
25
 
26
26
  | 이름 | 역할 | Web | Native |
27
27
  | --- | --- | --- | --- |
28
- | IconTile | 런처 항목 표시 | `@hjmds/react/screens` | `@hjmds/react-native/screens` |
28
+ | `IconTile` | 런처 항목 표시 | `@hjmds/react/screens` | `@hjmds/react-native/screens` |
29
29
 
30
30
  ## 최소 사용 예
31
31
 
@@ -59,7 +59,15 @@ import { IconTile } from "@hjmds/react-native/screens";
59
59
 
60
60
  ## 배치
61
61
 
62
- 아이콘 위·한 줄 말줄임 제목 아래로 중앙 정렬한다. Grid나 가상 목록의 host가 열 수와 외부 여백을 정한다. HJM 내부 selector를 제품에서 덮지 않는다.
62
+ | 항목 | 값 | 근거 |
63
+ | --- | --- | --- |
64
+ | 크기 | 아이콘 64×64(radius 22%), 제목 최대 폭 72 · 두 줄이면 84와 최소 높이 44 | `iconTileRecipe` |
65
+ | 간격 | 아이콘과 제목 사이 6 | `iconTileRecipe.gap` |
66
+ | 순서·정렬 | 아이콘(overlay 포함) 위 → 제목 아래, 가로 가운데, 제목 가운데 정렬 | 양 renderer |
67
+ | 고정·스크롤 | Grid나 가상 목록의 host가 열 수·외부 여백·스크롤을 정한다 | 별도 scroller 없음 |
68
+ | 좁은 폭·큰 글자 | 제목은 `labelLines`(1 또는 2)줄에서 말줄임, overlay는 잘리지 않는다 | 양 renderer |
69
+
70
+ 아이콘 위·한 줄 말줄임 제목 아래로 중앙 정렬한다. HJM 내부 selector를 제품에서 덮지 않는다.
63
71
 
64
72
  ## 꼭 지킬 것
65
73
 
@@ -0,0 +1,81 @@
1
+ # ImageChoiceGroup
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.26.1 이후)
7
+ - 검토일: 2026-10-17
8
+ - 근거: `src/image-choice.ts`의 `imageChoiceRecipe`·`resolveImageChoiceToggle`; Web `src/image-choice.tsx`, Native `src/image-choice.tsx`
9
+ - 스토리북: `배포/화면/계정/프로필`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 아바타 얼굴, 프로필 사진, 모음에 넣을 사진처럼 **그림을 보고 고르는** 단일·다중 선택에 쓴다. `selected`를 준 ghost 버튼으로 흉내 내지 않는다([조립 규칙](../SELECTION.md#조립-규칙필수)).
14
+
15
+ ## 쓰지 않을 때
16
+
17
+ | 상황 | 대신 쓸 것 |
18
+ | --- | --- |
19
+ | 글자 선택지 하나 고르기 | `Chip`(`selectionMode="single"`), `SegmentedControl`, `RadioGroup` |
20
+ | 글자 선택지 여러 개 | `Chip`(`selectionMode="multiple"`), `CheckboxGroup` |
21
+ | 기기 사진첩에서 새로 고르기 | `MediaPicker`·제품 권한 흐름 |
22
+ | 사진을 크게 보기 | `ImageViewer` |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `ImageChoiceGroup` | 그림 타일 선택 묶음 | `@hjmds/react/image-choice` | `@hjmds/react-native/image-choice` |
29
+ | `imageChoiceRecipe`, `resolveImageChoiceToggle`, `validateImageChoiceItems` | 크기·선택 규칙 | `@hjmds/design-contracts/components/image-choice` | 같음 |
30
+
31
+ ## 최소 사용 예
32
+
33
+ ```tsx
34
+ // Web
35
+ import { ImageChoiceGroup } from "@hjmds/react/image-choice";
36
+
37
+ <ImageChoiceGroup label={t("profile.face")} selectionMode="single" value={face} onValueChange={setFace}
38
+ items={faces.map(face => ({ id: face.id, label: face.label, image: <img src={face.src} alt="" /> }))} />
39
+ ```
40
+
41
+ ```tsx
42
+ // Native
43
+ import { ImageChoiceGroup } from "@hjmds/react-native/image-choice";
44
+
45
+ <ImageChoiceGroup label={t("collection.photos")} selectionMode="multiple" value={photoIds} onValueChange={setPhotoIds}
46
+ items={photos.map(photo => ({ id: photo.id, label: photo.label, image: <Image source={photo.source} style={{ width: "100%", height: "100%" }} /> }))} />
47
+ ```
48
+
49
+ ## 축과 기본값
50
+
51
+ | prop | 값 | 기본값 | 설명 |
52
+ | --- | --- | --- | --- |
53
+ | `label` | `string` | 필수 | 묶음의 접근성 이름 |
54
+ | `showLabel` | `boolean` | `false` | 타일 위에 `label`을 보이는 글자로 둔다 |
55
+ | `items` | `{ id, label, image, disabled? }[]` | 필수 | id 중복·빈 이름이면 `TypeError`. `image`는 칸을 채우는 그림 노드 |
56
+ | `selectionMode` | `"single"` · `"multiple"` | 필수 | 단일은 radio, 다중은 checkbox로 읽힌다 |
57
+ | `value`·`onValueChange` | 단일 `string \| null` / 다중 `string[]` | 필수 | 제어형. 다중은 누른 순서대로 추가·제거 |
58
+ | `disabled` | `boolean` | `false` | 묶음 전체 비활성 |
59
+
60
+ ## 배치
61
+
62
+ | 항목 | 값 | 근거 |
63
+ | --- | --- | --- |
64
+ | 크기 | 그림 72 정사각, 바깥 고리 2 + 간격 2(터치 영역 80), radius `radius.md` 12, 체크 원 20 | `imageChoiceRecipe` |
65
+ | 간격 | 타일 사이 `spacing.xs` 8, 라벨–타일 8 | `imageChoiceRecipe.gap` |
66
+ | 순서·정렬 | `items` 순서대로 시작 쪽부터 줄바꿈 배치, 체크는 타일 끝 쪽 위 모서리 | 두 렌더러 |
67
+ | 고정·스크롤 | 자체 스크롤 없음. 바깥 목록·화면이 스크롤 | 두 렌더러 |
68
+ | 좁은 폭·큰 글자 | 남은 폭에서 줄바꿈, 타일 크기는 유지 | flex wrap |
69
+
70
+ ## 꼭 지킬 것
71
+
72
+ - 타일마다 그림의 내용을 말하는 지역화 `label`을 준다. 그림 노드 자체는 보조기술에서 숨긴다(`alt=""`, `decorative`).
73
+ - 선택 결과의 저장·업로드·권한은 제품이 맡는다. 이 컴포넌트는 선택 상태만 바꾼다.
74
+
75
+ ## 상태와 상호작용
76
+
77
+ 선택된 타일은 브랜드 색 고리와 체크 원으로 구분하고, 비활성 타일은 투명도 0.5다. Web은 버튼마다 Tab으로 이동하고 Space·Enter로 고른다.
78
+
79
+ ## 근거
80
+
81
+ - 도입 계기: 2026-10-16 구성 예제 점검 — 제품들이 `selected`를 준 ghost 버튼으로 그림 선택을 흉내 내 선택 상태가 일관되지 않았다.
@@ -0,0 +1,94 @@
1
+ # ImportFieldMapping
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [데이터 해석과 가져오기 계약](../../data-workflows.md), `src/data-workflows.ts`의 `resolveImportMapping`, 양 renderer `src/data-workflows.tsx`
9
+ - 스토리북: `배포/구성/입력과 작성/가져오기 열 연결`
10
+
11
+ ## 언제 쓰나
12
+
13
+ CSV 등 원본 열을 제품의 대상 필드에 연결하고, 검증을 통과한 연결만 가져오기로 넘길 때 쓴다.
14
+ 대상 중복·필수 누락이 있으면 미리보기와 가져오기를 막고, 명시적 건너뛰기를 지원한다.
15
+ `onImport`에는 동결된 연결 snapshot을 전달하므로 가져오는 동안 연결이 바뀌어도 요청 범위는 그대로다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 단일 값 선택 | `Select` |
22
+ | 고정 입력 묶음 | `FieldGroup` |
23
+ | 가져온 데이터 표시 | `DataTable` |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `ImportFieldMapping` | 원본 열 → 대상 필드 연결·검증·미리보기·가져오기 | `@hjmds/react/data-workflows` | `@hjmds/react-native/data-workflows` |
30
+
31
+ `WorkflowField`·`ImportMapping`·`resolveImportMapping`은 `@hjmds/design-contracts/data-workflows`에 있다. root barrel에는 없다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { ImportFieldMapping } from "@hjmds/react/data-workflows";
38
+
39
+ <ImportFieldMapping label={t("import.title")} sources={csvColumns} targets={productFields}
40
+ mapping={mapping} onMappingChange={setMapping} previewRows={firstRows}
41
+ onImport={async snapshot => { await importer.run(file, snapshot); }}
42
+ importLabel={t("import.confirm")} skipLabel={t("import.skip")} errorLabel={t("import.failed")}
43
+ validationLabel={state => state.duplicateTargets.length ? t("import.duplicate") : t("import.required")} />
44
+ ```
45
+
46
+ ```tsx
47
+ // Native
48
+ import { ImportFieldMapping } from "@hjmds/react-native/data-workflows";
49
+
50
+ <ImportFieldMapping label={t("import.title")} sources={csvColumns} targets={productFields}
51
+ mapping={mapping} onMappingChange={setMapping} previewRows={firstRows}
52
+ onImport={async snapshot => { await importer.run(file, snapshot); }}
53
+ importLabel={t("import.confirm")} skipLabel={t("import.skip")} dismissLabel={t("common.close")}
54
+ errorLabel={t("import.failed")} validationLabel={validationLabel} />
55
+ ```
56
+
57
+ ## 축과 기본값
58
+
59
+ | prop | 값 | 기본값 | 설명 |
60
+ | --- | --- | --- | --- |
61
+ | label | string | 필수 | 제목, 미리보기 표 이름 |
62
+ | sources / targets | WorkflowField[] | 필수 | `{ id, label, required? }`. id는 각각 서로 달라야 함 |
63
+ | mapping / onMappingChange | `Record<sourceId, targetId \| null>` / fn | 필수 | 연결(제품 소유). null은 건너뛰기 |
64
+ | onImport | (mapping) => void \| Promise | 필수 | 동결 snapshot으로 실제 가져오기. 실패는 reject |
65
+ | importLabel / skipLabel / errorLabel | string | 필수 | 가져오기·건너뛰기·실패 문구 |
66
+ | validationLabel | (state) => string | 필수 | `duplicateTargets`·`missingTargets`로 안내 문구 선택 |
67
+ | previewRows | `{ id, values }[]` | [] | 유효할 때만 연결된 열로 미리보기 |
68
+ | dismissLabel | string | Native 필수 | Native Select 선택 창 닫기 문구(Web에는 없음) |
69
+ | disabled | boolean | false | 선택·가져오기 비활성 |
70
+
71
+ ## 배치
72
+
73
+ | 항목 | 값 | 근거 |
74
+ | --- | --- | --- |
75
+ | 크기 | Select·DataTable·Button(primary) 기본 규격 | 양 renderer |
76
+ | 간격 | 세로 md | 양 renderer |
77
+ | 순서·정렬 | 제목 → 원본 열마다 Select(`sources` 순서) → 검증 안내(가로 중앙) 또는 미리보기 표 → 가져오기 → 오류(가로 중앙) | 양 renderer |
78
+ | 고정·스크롤 | 미리보기 표의 가로 스크롤은 DataTable 소유, 세로는 바깥 host | DataTable |
79
+ | 좁은 폭·큰 글자 | Select는 본문 폭, 표는 DataTable 규칙 | 양 renderer |
80
+
81
+ ## 꼭 지킬 것
82
+
83
+ - 연결이 하나 이상이고, 대상 중복이 없고, 필수 대상이 모두 연결되어야 유효하다. 그 전에는 가져오기 버튼이 비활성이다.
84
+ - `mapping`에 없는 원본·대상 ID를 넣으면 `TypeError`다.
85
+ - CSV 파싱·인코딩·서버 검증·권한·멱등성은 제품 importer가 소유한다.
86
+
87
+ ## 상태와 상호작용
88
+
89
+ | 상태 | 모습 |
90
+ | --- | --- |
91
+ | 미완성 | 검증 안내(Web `role="status"`, Native polite), 가져오기 비활성 |
92
+ | 유효 | `previewRows`가 있으면 연결된 대상 열로 미리보기 |
93
+ | 가져오는 중 | 버튼 로딩, Select 잠금 |
94
+ | 실패 | 연결 유지, 중앙 오류, 같은 버튼으로 재시도 |
@@ -0,0 +1,105 @@
1
+ # InlineEdit
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [목록 편집 계약](../../collection-editing.md), `src/collection-editing.ts`의 `collectionEditingRecipe`, 양 renderer `src/collection-editing.tsx`
9
+ - 스토리북: `배포/구성/입력과 작성/제목 바로 편집`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 목록·상세의 제목처럼 이미 확정된 짧은 값 하나를 그 자리에서 명시적으로 편집·저장·취소할 때 쓴다.
14
+ Lucian #033 등 전수 검토 뒤 도입한 목록 편집 구성의 하나이며 TextField·Button·action-session을 합성한다.
15
+ 저장은 제품 `onSave`가 실제로 확정한 뒤에만 닫히고, 실패하면 초안을 그대로 보존한다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 항상 편집 상태인 입력 | `TextField` |
22
+ | 여러 입력을 함께 저장하는 양식 | `FieldGroup` |
23
+ | 같은 형식의 행을 추가·삭제 | `RepeatableFieldGroup` |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `InlineEdit` | 보기↔편집 전환, 저장·취소·실패 보존 | `@hjmds/react/collection-editing` | `@hjmds/react-native/collection-editing` |
30
+
31
+ root barrel에는 없다. 구성 안내는 [목록 편집 구성 지침](../compositions/collection-editing.md)에 있다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { InlineEdit } from "@hjmds/react/collection-editing";
38
+
39
+ <InlineEdit id={note.id} label={t("note.title")} value={note.title}
40
+ editLabel={t("note.editTitle")} saveLabel={t("common.save")} cancelLabel={t("common.cancel")}
41
+ errorLabel={t("note.saveFailed")}
42
+ validate={draft => draft.trim() ? undefined : t("note.titleRequired")}
43
+ onSave={async draft => { await saveTitle(note.id, draft); }} />
44
+ ```
45
+
46
+ ```tsx
47
+ // Native
48
+ import { InlineEdit } from "@hjmds/react-native/collection-editing";
49
+
50
+ <InlineEdit id={note.id} label={t("note.title")} value={note.title}
51
+ editLabel={t("note.editTitle")} saveLabel={t("common.save")} cancelLabel={t("common.cancel")}
52
+ errorLabel={t("note.saveFailed")}
53
+ validate={draft => draft.trim() ? undefined : t("note.titleRequired")}
54
+ onSave={async draft => { await saveTitle(note.id, draft); }} />
55
+ ```
56
+
57
+ ## 축과 기본값
58
+
59
+ | prop | 값 | 기본값 | 설명 |
60
+ | --- | --- | --- | --- |
61
+ | id | string | 필수 | 편집 대상 ID. 바뀌면 내부 상태 전체를 remount |
62
+ | value | string | 필수 | 제품이 확정한 현재 값 |
63
+ | label | string | 필수 | 편집 입력 라벨, Web group 이름 |
64
+ | editLabel / saveLabel / cancelLabel | string | 필수 | 편집 진입·저장·취소 버튼 문구 |
65
+ | errorLabel | string | 필수 | 저장 실패 안내 |
66
+ | validate | (draft) => string \| undefined | 없음 | 오류 문구를 돌려주면 저장 비활성 |
67
+ | onSave | (draft) => void \| Promise | 필수 | 실제 저장 확정 후 resolve, 실패하면 reject |
68
+ | disabled | boolean | false | 편집 진입·저장 차단 |
69
+
70
+ ## 배치
71
+
72
+ | 항목 | 값 | 근거 |
73
+ | --- | --- | --- |
74
+ | 크기 | 기존 TextField·Button 규격 그대로 | 양 renderer |
75
+ | 간격 | 보기 행·버튼 행 sm, 편집 묶음 `actionGap` sm | collectionEditingRecipe |
76
+ | 순서·정렬 | 보기: 값 → 편집(ghost), 세로 중앙. 편집: 입력 → [취소(ghost) · 저장(primary)] → 오류 | 양 renderer |
77
+ | 고정·스크롤 | 바깥 목록·화면 host 소유 | 별도 scroller 없음 |
78
+ | 좁은 폭·큰 글자 | 보기 행과 버튼 행은 `wrap`으로 줄바꿈 | 양 renderer |
79
+
80
+ 오류 문구는 구성 안에서 가로 중앙에 둔다. 앱이 별도 padding이나 버튼 순서를 바꾸지 않는다.
81
+
82
+ ## 꼭 지킬 것
83
+
84
+ - `onSave`는 서버·저장소가 실제로 확정한 뒤 resolve하고 `value`를 확정 값으로 갱신한다. 실패는 reject해야 초안이 보존된다.
85
+ - 다른 항목으로 바뀌면 `id`를 바꾼다. 같은 `id`로 다른 항목을 넣으면 이전 초안이 남는다.
86
+ - 모든 문구는 i18n 키로 전달한다.
87
+
88
+ ## 상태와 상호작용
89
+
90
+ | 상태 | 모습 |
91
+ | --- | --- |
92
+ | 보기 | 값과 편집 버튼. 편집에 들어가면 초안을 현재 `value`로 초기화하고 입력에 포커스 |
93
+ | 편집 | 입력·취소·저장. `validate` 오류가 있으면 입력에 오류 표시, 저장 비활성 |
94
+ | 저장 중 | 저장 버튼 로딩, 취소 비활성, 입력 잠금. 중복 저장은 무시 |
95
+ | 실패 | 초안 유지, 중앙 오류, 같은 저장으로 재시도 |
96
+ | 성공 | 보기로 돌아가 편집 버튼으로 포커스 복귀 |
97
+
98
+ ## 플랫폼 차이
99
+
100
+ | 항목 | Web | Native |
101
+ | --- | --- | --- |
102
+ | 저장·취소 키 | Enter 저장, Escape 취소. IME 조합 중 Enter는 무시 | 키보드 제출(`onSubmitEditing`) 저장 |
103
+ | 저장 중 입력 | `readOnly` | `disabled` |
104
+ | 포커스 복귀 | 포커스가 구성 안에 있을 때만 편집 버튼으로 | 성공 시 입력이 아직 포커스일 때만 복귀 |
105
+ | 실패 알림 | `role="alert"` | `accessibilityRole="alert"`·assertive, iOS는 announce |
@@ -0,0 +1,60 @@
1
+ # KeyboardAvoidanceContext
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 실험
5
+ - 지원: Native
6
+ - 적용: 미게시(1.26.1 이후)
7
+ - 검토일: 2026-10-16
8
+ - 근거: Native `src/keyboard.tsx`; 2026-10-16 키보드 전수 조사에서 바깥 KeyboardAvoiding과 안쪽 ScreenLayout이 같은 키보드 이벤트에 동시에 여백을 주지 않게 하려고 추가
9
+ - 스토리북: 없음
10
+
11
+ ## 언제 쓰나
12
+
13
+ 제품이 직접 만든 키보드 대응 영역 안에서 HJM `ScreenLayout`·`Sheet`가 여백을 두 번 주지 않게 할 때 쓴다.
14
+ `KeyboardAvoiding`, 키보드 대응을 켠 `Sheet`, `Dialog`는 이 값을 이미 `true`로 제공하므로 보통 직접 쓸 일이 없다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 화면을 키보드 위로 올리기 | `ScreenLayout`(기본 `keyboardAvoidance`) 또는 `KeyboardAvoiding` |
21
+ | 모달 안 입력 | `Sheet`(기본 `keyboardAvoidance`)·`Dialog` |
22
+
23
+ ## 공개 이름과 import
24
+
25
+ | 이름 | 역할 | Web | Native |
26
+ | --- | --- | --- | --- |
27
+ | `KeyboardAvoidanceContext` | 키보드 처리 중인 영역 표시 | 없음 | `@hjmds/react-native/keyboard` |
28
+
29
+ ## 최소 사용 예
30
+
31
+ ```tsx
32
+ // Native — 제품 고유 키보드 어댑터가 이미 화면을 올리는 경우
33
+ import { KeyboardAvoidanceContext } from "@hjmds/react-native/keyboard";
34
+
35
+ <ProductKeyboardAdapter>
36
+ <KeyboardAvoidanceContext.Provider value>
37
+ <ChatScreen title={title} composer={composer}>{timeline}</ChatScreen>
38
+ </KeyboardAvoidanceContext.Provider>
39
+ </ProductKeyboardAdapter>
40
+ ```
41
+
42
+ ## 축과 기본값
43
+
44
+ | prop | 값 | 기본값 | 설명 |
45
+ | --- | --- | --- | --- |
46
+ | `value` | `boolean` | `false`(제공자 없음) | `true`면 안쪽 `ScreenLayout`이 자체 키보드 여백과 포커스 스크롤을 건너뛴다 |
47
+
48
+ ## 배치
49
+
50
+ | 항목 | 값 | 근거 |
51
+ | --- | --- | --- |
52
+ | 크기 | 화면에 그리는 것 없음 | React context |
53
+ | 간격 | 없음 | 같음 |
54
+ | 순서·정렬 | 키보드 대응 영역 바로 안쪽에 둔다 | `KeyboardAvoiding` 구현 |
55
+ | 고정·스크롤 | 영향 없음 | 같음 |
56
+ | 좁은 폭·큰 글자 | 영향 없음 | 같음 |
57
+
58
+ ## 꼭 지킬 것
59
+
60
+ 키보드 여백을 실제로 주는 영역에서만 `true`를 제공한다. 여백을 주지 않으면서 `true`를 주면 안쪽 화면이 키보드에 가려진다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-07
7
+ - 검토일: 2026-10-16
8
8
  - 근거: [Native platform](../../native-platform.md#키보드-회피), 판정 `resolveKeyboardInset`(`src/native-platform.ts`)
9
9
  - 스토리북: 없음
10
10
 
@@ -63,6 +63,10 @@ import { BottomCTA } from "@hjmds/react-native/bottom-cta";
63
63
 
64
64
  - 콜백·상태 prop은 없다. 키보드 높이는 컴포넌트 안에서 키보드 이벤트로 잰다.
65
65
 
66
+ 2026-10-16 키보드 전수 조사: `ScreenLayout`(따라서 `ChatScreen`·`CommentThreadScreen`·`EditorScreen` 등)이 `keyboardAvoidance`(기본 켬)로 스스로 키보드와 겹치는 만큼 비키고, 본문 스크롤은 포커스된 입력칸을 보이게 스크롤한다(`useScrollFocusedInputIntoView`). 이 컴포넌트·키보드를 처리하는 `Sheet`·`Dialog` 안에서는 `KeyboardAvoidanceContext`로 중복 처리를 건너뛴다. 따라서 화면을 이 컴포넌트로 따로 감쌀 필요는 없고, 감싸도 여백이 두 번 생기지 않는다.
67
+
68
+ 2026-10-16: 키보드 높이 전체가 아니라 이 영역의 아래 끝과 키보드 윗변이 실제로 겹치는 만큼만 아래 여백으로 준다. 탭 막대·도구 막대 위에 놓인 화면에서 키보드와 작성창 사이에 빈 띠가 생기던 문제를 고쳤다(미게시).
69
+
66
70
  ## 배치
67
71
 
68
72
  | 항목 | 값 | 근거 |
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.17.1
7
- - 검토일: 2026-10-13
7
+ - 검토일: 2026-10-17
8
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
9
  - 스토리북: `배포/화면/콘텐츠/목록과 상세`
10
10
 
@@ -65,7 +65,7 @@ import { ListDetailScreen } from "@hjmds/react-native/screen-flows";
65
65
  | --- | --- | --- | --- |
66
66
  | `list` | `ReactNode` | 필수 | 목록 pane 본문. 상세가 열려도 mount를 유지한다 |
67
67
  | `detail` | `{ title; content; state?; stateAction?; notice? }` | 없음 | 있으면 상세 pane을 보이고 목록 pane을 숨긴다. 닫힌 상태는 prop을 빼서 표현한다(`undefined`를 넘기지 않는다) |
68
- | `back` | `ScreenFlowAction`(`{ label, onAction(), disabled?, pending? }`) | 필수 | 상세 pane `leading` 자리의 ghost 버튼 |
68
+ | `back` | `ScreenFlowAction`(`{ label, onAction(), disabled?, pending? }`) | 필수 | 상세 pane 헤더 시작 쪽 ‹ 아이콘 버튼(`ScreenLayout` `exit` `back`). `label`은 보조기술 이름이고 글자는 보이지 않는다. 2026-10-17부터 글자 버튼 대신 아이콘이며 Native `detail.leading`(제품 뒤로 아이콘 대체)은 제거됐다 |
69
69
  | `refresh` | `ScreenFlowAction` | 없음 | 목록 pane `actions` 자리의 ghost 버튼. 주면 `actions`를 덮는다 |
70
70
  | `loadMore` | `ScreenFlowAction` | 없음 | 목록 pane footer의 ghost 버튼 |
71
71
  | `layoutStyle` | `HjmCompositionStyleProp` | 없음 | Web·Native 모두 목록·상세 pane을 함께 담는 **바깥 틀**에 적용한다(안쪽 목록 ScreenLayout이 아니다). 목록/상세 전환과 상관없이 같은 루트에 남는다(1.13.0) |
@@ -84,7 +84,7 @@ import { ListDetailScreen } from "@hjmds/react-native/screen-flows";
84
84
  ## 꼭 지킬 것
85
85
 
86
86
  - 라벨은 모두 i18n 키로 넣는다.
87
- - `state`(로딩·빈·오류·제한)는 목록 pane에만 적용된다. 상세 최초 조회·오류는 `detail.state`, 재시도는 `detail.stateAction`, 기존 상세를 유지하는 갱신 실패는 `detail.notice`로 전달한다. 미게시(1.18.0 이후)에 추가된 선택 prop이며 이전 설치 버전에서는 상세 content 안에 상태를 구성한다.
87
+ - `state`(로딩·빈·오류·제한)는 목록 pane에만 적용된다. 상세 최초 조회·오류는 `detail.state`, 재시도는 `detail.stateAction`, 기존 상세를 유지하는 갱신 실패는 `detail.notice`로 전달한다. Web·Native 1.26.1 설치 타입에서 제공을 확인한 선택 prop이다. 설치 버전에서 지원하지 않으면 지원 버전으로 갱신하고 공통 상태 틀을 제품에서 복제하지 않는다.
88
88
  - Web은 상세 진입 시 뒤로 버튼에 포커스를 옮기고 복귀 시 목록의 원래 요소로 돌린다. 요소가 삭제되었으면 목록 본문으로 이동한다.
89
89
  처음부터 `detail`이 있는 채로 mount되면(딥링크) 사용자 전환이 아니므로 포커스를 옮기지 않는다. 그때 포커스는 제품 router가 정한다.
90
90
  - 상세 열림 상태는 제품이 가진다. OS 뒤로가기·브라우저 뒤로가기와 `back`을 제품 router에서 함께 연결한다.
@@ -99,5 +99,5 @@ import { ListDetailScreen } from "@hjmds/react-native/screen-flows";
99
99
  ## 함정
100
100
 
101
101
  - `refresh`를 주면 ScreenLayout `actions`는 무시된다. 둘 다 필요하면 `refresh` 대신 `actions`에 직접 조합한다.
102
- - 상세 pane은 `title`·`state`·`stateAction`·`notice`와 `back`(leading)을 받는다. 상세 쪽 `description`·`actions`·footer 자리는 없다.
102
+ - 상세 pane은 `title`·`state`·`stateAction`·`notice`와 `back`(헤더 `exit`)을 받는다. 상세 쪽 `description`·`actions`·footer 자리는 없다.
103
103
  - `loadMore`는 상태 없는 버튼이다. 끝·오류·중복 요청 방지가 필요하면 LoadMore를 쓴다.
@@ -93,7 +93,7 @@ Native `grouped` 프레임은 Provider의 `tokens.radius.lg`를 읽는다. plain
93
93
  | 항목 | 값 | 근거 |
94
94
  | --- | --- | --- |
95
95
  | 크기 | 화면 본문 폭을 채우는 세로 묶음이다. `grouped`는 배경 `--hjm-color-bg`와 모서리 `radius.lg` 16으로 한 덩어리가 된다 | `design-contracts/src/foundations.ts`(`radius`), `react/src/styles.css`(`.hjm-list`) |
96
- | 간격 | 행 사이 간격은 없고 구분선 1px(`--hjm-color-border`)만 들어간다. `indented` 구분선의 시작 들여쓰기: 52(leading 40 + `spacing.sm` 12, `listRecipe.separators.indented`, 두 플랫폼). 끝 쪽은 둘 다 0. 덩어리 바깥 여백·섹션 사이 간격은 감싸는 Stack이 준다(페이지 좌우 여백은 `layout.pagePadding`, 섹션 간격은 `layout.sectionGap` = `spacing.xl` 24) | `design-contracts/src/component-recipes.ts`(`listRecipe`), `design-contracts/src/foundations.ts`(`layout`), `react/src/styles.css`(`.hjm-list`), `react-native/src/data-display.tsx`(List) |
96
+ | 간격 | 행 사이 간격은 없고 구분선 1px(`--hjm-color-border`)만 들어간다. `indented` 구분선의 시작 들여쓰기: 60(행 좌우 여백 `spacing.xs` 8 + leading 40 + `spacing.sm` 12, 글자 시작과 같은 위치, `listRecipe.separators.indented`, 두 플랫폼. 2026-10-17 전에는 행 여백을 빼서 52였다). 끝 쪽은 둘 다 0. 덩어리 바깥 여백·섹션 사이 간격은 감싸는 Stack이 준다(페이지 좌우 여백은 `layout.pagePadding`, 섹션 간격은 `layout.sectionGap` = `spacing.xl` 24) | `design-contracts/src/component-recipes.ts`(`listRecipe`), `design-contracts/src/foundations.ts`(`layout`), `react/src/styles.css`(`.hjm-list`), `react-native/src/data-display.tsx`(List) |
97
97
  | 순서·정렬 | 다음 페이지 요청은 목록 바로 아래에 [LoadMore](load-more.md)를 둔다 | — |
98
98
  | 고정·스크롤 | List 자체는 스크롤하지 않는다. 화면 스크롤 안에 둔다 | `react-native/src/data-display.tsx`(List) |
99
99
  | 좁은 폭·큰 글자 | — | — |
@@ -38,7 +38,6 @@ import { Masonry } from "@hjmds/react/masonry";
38
38
  import { EmptyState } from "@hjmds/react/feedback";
39
39
 
40
40
  <Masonry label={t("gallery.title")} items={photos} keyExtractor={(p) => p.id}
41
- width={containerWidth} columns={3}
42
41
  getItemHeight={(p, itemWidth) => itemWidth * (p.height / p.width)}
43
42
  renderItem={(p) => <PhotoCard photo={p} />}
44
43
  empty={<EmptyState title={t("gallery.empty.title")} />} />
@@ -49,7 +48,7 @@ import { EmptyState } from "@hjmds/react/feedback";
49
48
  import { Masonry } from "@hjmds/react-native/masonry";
50
49
 
51
50
  <Masonry label={t("gallery.title")} items={photos} keyExtractor={(p) => p.id}
52
- width={layoutWidth} getItemHeight={(p, itemWidth) => itemWidth * (p.height / p.width)}
51
+ getItemHeight={(p, itemWidth) => itemWidth * (p.height / p.width)}
53
52
  renderItem={(p) => <PhotoCard photo={p} />} />
54
53
  ```
55
54
 
@@ -61,10 +60,10 @@ import { Masonry } from "@hjmds/react-native/masonry";
61
60
  | --- | --- | --- | --- |
62
61
  | `items` | `readonly T[]` | 필수 | 입력 순서가 읽기·포커스 순서다 |
63
62
  | `keyExtractor` | `(item: T) => string` | 필수 | 유일한 키. 중복이면 `TypeError` |
64
- | `renderItem` | `(item: T, index: number) => ReactNode` | 필수 | 열 폭은 넘기지 않는다. 항목 래퍼가 `getItemHeight` 높이·열 폭으로 고정돼 있어 Web은 일반 레이아웃 div에 `height: "100%", display: "flex"`를 주고 그 안의 Surface에는 `layoutStyle={{ flex: 1 }}`을 준다. Native Surface는 `layoutStyle={{ flex: 1 }}`로 채운다. HJM layoutStyle에 height를 넣지 않는다 |
63
+ | `renderItem` | `(item: T, index: number) => ReactNode` | 필수 | 열 폭은 넘기지 않는다. 항목 틀은 `getItemHeight` 높이·열 폭으로 고정된 세로 flex 상자(두 플랫폼 같음)라, 카드가 틀을 채우려면 Surface 등에 `layoutStyle={{ flex: 1 }}`만 준다. 감싸는 div를 만들거나 layoutStyle에 height를 넣지 않는다 |
65
64
  | `getItemHeight` | `(item: T, itemWidth: number) => number` | 필수 | 그 열 폭에서 항목의 확정 높이(양의 유한수). 루트 높이는 가장 긴 열로 정해진다 |
66
- | `width` | 숫자(> 0) | 필수 | 컨테이너 폭. 측정은 소비 화면이 한다(Web ResizeObserver, Native `onLayout` 등) |
67
- | `columns` | 1~12 정수 | 2 | 열 수 |
65
+ | `width` | 숫자(> 0) | — | 생략하면 Masonry가 자기 자리 폭을 측정한다(Web ResizeObserver, Native `onLayout`). 측정 전에는 아무것도 그리지 않는다. 화면이 이미 폭을 알면 넘긴다 |
66
+ | `columns` | 1~12 정수 | 폭에서 계산 | 생략하면 `resolveMasonryColumns(width, gap)` = 열 폭이 `masonryRecipe.minItemWidth` 160 이상인 최대 열 수(1~12). 375 폰 본문 2열, 데스크톱 1168 슬롯 6열 |
68
67
  | `gap` | 0 이상 숫자 | 12 | 가로·세로 간격 |
69
68
  | `label` | 문자열 | 필수 | 현지화 |
70
69
  | `empty` | 노드 | — | 항목이 없을 때 보일 내용 |
@@ -78,7 +77,7 @@ import { Masonry } from "@hjmds/react-native/masonry";
78
77
  | 간격 | `gap` 기본 12(`spacing.sm`과 같은 값), 가로·세로 같은 값. `width`는 화면 좌우 여백(`layout.pagePadding` compact 16 · regular 20 · spacious 24)을 뺀 본문 폭 | `src/masonry.ts`, `layout.pagePadding` |
79
78
  | 순서·정렬 | 원본 순서대로 가장 짧은 열에 채운다. 읽기·포커스 순서는 열이 아니라 원본 순서. RTL은 오른쪽 열부터 | `masonryRecipe.readingOrder`, `insetInlineStart`(Web)·`right`(Native) |
80
79
  | 고정·스크롤 | 본문 스크롤 영역 안에 놓는다. Masonry는 스크롤을 갖지 않는다 | `src/masonry.tsx`(Web·Native) |
81
- | 좁은 폭·큰 글자 | 열 수는 제품이 폭으로 정한다(좁은 폭 2열, `breakpoint.medium` 600 이상에서 늘림). 큰 글자로 카드 높이가 바뀌면 `getItemHeight`를 다시 계산하고, 폭이 바뀌면 `width`를 다시 넘긴다 | `breakpoint` |
80
+ | 좁은 폭·큰 글자 | 열 수는 기본으로 폭에서 계산한다(최소 열 폭 160, 폰 2열·넓은 슬롯에서 늘어남). 큰 글자로 카드 높이가 바뀌면 `getItemHeight`를 다시 계산한다. `width`를 생략하면 폭 변화도 Masonry가 따라간다 | `masonryRecipe.minItemWidth`, `resolveMasonryColumns` |
82
81
 
83
82
  ## 꼭 지킬 것
84
83
 
@@ -87,7 +86,7 @@ import { Masonry } from "@hjmds/react-native/masonry";
87
86
  - `keyExtractor`는 유일한 키를 돌려준다. 중복 키는 `TypeError`다.
88
87
  - `width`가 0 이하이거나 열 폭이 0 이하, `getItemHeight`가 양의 유한수가 아니면 `TypeError`가 난다. 항목이 비어
89
88
  `empty`만 그릴 때도 기하 검사를 먼저 하므로 같다. 측정 전(폭 0)에는 Masonry를 렌더하지 않는다.
90
- - 화면 폭이 바뀌면 `width`를 다시 넘긴다.
89
+ - `width`를 직접 넘기는 경우 화면 폭이 바뀌면 다시 넘긴다. 생략하면 Masonry가 측정한다.
91
90
  - 빈 목록도 `width`를 유지하며, `layoutStyle`로 할당한 영역 안에서 안내를 가로·세로 중앙에 놓는다. 높이를 지정하지 않은 본문에서는 안내의 내용 높이를 사용한다.
92
91
 
93
92
  ## 플랫폼 차이
@@ -43,7 +43,8 @@ import { MediaSelectionScreen } from "@hjmds/react/screen-flows";
43
43
  title={t("media.title")}
44
44
  items={picked.map((p) => ({ descriptor: { id: p.id, name: p.fileName, state: p.upload },
45
45
  preview: <img src={p.uri} alt="" width={p.width} height={p.height} /> }))}
46
- add={{ label: t("media.add"), onAction: openPicker }}
46
+ exit={{ kind: "close", label: t("media.close"), onClick: close }}
47
+ add={{ label: t("media.add"), icon: <PlusIcon aria-hidden />, onAction: openPicker }}
47
48
  done={{ label: t("media.done"), onAction: submit, disabled: uploading || !canComplete }}
48
49
  labels={{ pending: t("upload.pending"), uploading: t("upload.uploading"),
49
50
  success: t("upload.success"), cancel: t("upload.cancel"), retry: t("upload.retry") }}
@@ -65,7 +66,8 @@ import { Image } from "react-native";
65
66
  title={t("media.title")}
66
67
  items={picked.map((p) => ({ descriptor: { id: p.id, name: p.fileName, state: p.upload },
67
68
  preview: <Image src={p.uri} width={p.width} height={p.height} /> }))}
68
- add={{ label: t("media.add"), onAction: openPicker }}
69
+ exit={{ kind: "close", label: t("media.close"), onPress: close }}
70
+ add={{ label: t("media.add"), icon: <PlusIcon color={colors.text} />, onAction: openPicker }}
69
71
  done={{ label: t("media.done"), onAction: submit, disabled: uploading || !canComplete }}
70
72
  labels={{ pending: t("upload.pending"), uploading: t("upload.uploading"),
71
73
  success: t("upload.success"), cancel: t("upload.cancel"), retry: t("upload.retry") }}
@@ -87,14 +89,14 @@ Web의 `<img alt="">`는 장식용 미리보기다. 사진 이름은 `removeLabe
87
89
  | `items` | `readonly { descriptor: UploadItemDescriptor; preview?: ReactNode }[]` | 필수 | descriptor는 `id`·`name`·선택 `sizeLabel`·`state`(`pending` · `uploading` · `success` · `error`) |
88
90
  | `library` | `ReactNode` | 없음 | 주면 `items` 격자 대신 이 슬롯을 그린다 |
89
91
  | `selectionSummary` | `ReactNode` | 없음 | footer에서 `done` 위 |
90
- | `add` | `ScreenFlowAction`(`{ label, onAction(), disabled?, pending? }`) | 필수 | 상단 `actions` 자리의 ghost 버튼 |
92
+ | `add` | `ScreenFlowIconAction`(`{ label, icon, onAction(), disabled?, pending? }`) | 없음 | 상단 `actions` 끝의 IconButton(이름 `label`). 실제로 사진을 더 고르는 행동일 때만 준다. 화면 나가기(닫기·취소·선택으로 돌아가기)는 `exit` |
91
93
  | `done` | `ScreenFlowAction` | 필수 | footer의 primary 버튼 |
92
94
  | `labels` | `UploadItemLabels`(`{ pending, uploading, success, cancel, retry }`) | 필수 | 각 항목 UploadItem 문구 |
93
95
  | `actionLabels` | `{ remove, moveUp, moveDown }` | 필수 | 항목 아래 버튼의 짧은 표시 문구 |
94
96
  | `removeLabel` · `moveUpLabel` · `moveDownLabel` | `(item: MediaSelectionItem) => string` | 필수 | 사진 이름을 포함한 접근성 이름 |
95
97
  | `onMove` | `(id: string, direction: -1 \| 1) => void` | 필수 | 첫 항목의 위로·마지막 항목의 아래로는 비활성 |
96
98
  | `onRemove` · `onRetry` · `onCancel` | `(id: string) => void` | 필수 | 삭제, UploadItem 재시도·취소 |
97
- | 나머지 | `ScreenLayout`과 같음(`children`·`footer` 제외) | — | `actions`는 `add`가 덮는다 |
99
+ | 나머지 | `ScreenLayout`과 같음(`children`·`footer` 제외) | — | `actions`를 주면 `add` 앞에 같은 행으로 놓인다 |
98
100
 
99
101
  ## 배치
100
102
 
@@ -115,7 +117,7 @@ Web의 `<img alt="">`는 장식용 미리보기다. 사진 이름은 `removeLabe
115
117
 
116
118
  ## 함정
117
119
 
118
- - `actions`를 넘겨도 `add` 버튼이 그 자리를 덮는다. 다른 상단 행동은 `leading`이나 `notice`로 둔다.
120
+ - 나가기를 `add`로 넘기지 않는다. 2026-10-17 화면 검토에서 필수였던 글자 `add`에 "취소"를 넣어 헤더 끝 글자 버튼이 가운데 제목을 밀고 Native에서 "취"로 잘렸다. 그래서 `add`를 선택 항목으로 바꾸고 아이콘을 필수로 했다. 나가기는 헤더 시작의 `exit` 아이콘이다.
119
121
  - `library`를 주면 `items` 격자와 업로드 상태 표시는 그려지지 않는다. 선택 결과는 `selectionSummary`로 보여 준다.
120
122
 
121
123
  ## 삭제·순서 변경 후 Web 키보드 초점