@hjmds/design-contracts 1.26.1 → 1.27.1

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
@@ -0,0 +1,104 @@
1
+ # MediaTransport
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [미디어 재생 제어 계약](../../media-transport.md), `src/media-transport.ts`의 `resolveMediaPlayback`·`mediaTransportPaths`, 양 renderer `src/media-transport.tsx`
9
+ - 스토리북: `배포/구성/직접 조작과 모션/미디어 재생 제어`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 제품의 실제 오디오·영상 엔진에 재생·일시 정지·유한 길이 탐색 제어를 붙일 때 쓴다.
14
+ Lucian 007의 재생/정지 모핑을 실제 관측 상태와 연결하려고 IconButton과 Slider를 합성했다.
15
+ 버튼 모양과 위치는 엔진이 보고한 `state`만 따르며, 클릭으로 playing을 미리 바꾸지 않는다. 구성 배치는 [미디어 재생 제어 지침](../compositions/media-transport.md)에 있다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 모달 안 영상 표시 | [영상 미리보기](../compositions/video-dialog.md) 구성 |
22
+ | 단순 아이콘 행동 | `IconButton` |
23
+ | 일반 수치 입력 | `Slider` |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `MediaTransport` | 재생/일시 정지 모핑·시간·탐색 | `@hjmds/react/media-transport` | `@hjmds/react-native/media-transport` |
30
+
31
+ `MediaPlaybackState`·`resolveMediaPlayback`은 `@hjmds/design-contracts/media-transport`에 있다. Native는 optional peer `react-native-svg`가 필요하다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { MediaTransport } from "@hjmds/react/media-transport";
38
+
39
+ <MediaTransport state={observed /* video 이벤트에서 갱신 */}
40
+ onPlay={() => video.play()} onPause={() => video.pause()}
41
+ onSeek={seconds => seekAndWaitForSeeked(video, seconds)}
42
+ labels={{ play: t("media.play"), pause: t("media.pause"), seek: t("media.seek"), decrement: t("media.back"),
43
+ increment: t("media.forward"), unknownDuration: t("media.live"), error: t("media.failed") }}
44
+ formatTime={formatTime} active={visible} />
45
+ ```
46
+
47
+ ```tsx
48
+ // Native
49
+ import { MediaTransport } from "@hjmds/react-native/media-transport";
50
+
51
+ <MediaTransport state={observed /* player 이벤트에서 갱신 */}
52
+ onPlay={() => player.play()} onPause={() => player.pause()}
53
+ onSeek={seconds => { player.currentTime = seconds; }}
54
+ labels={mediaLabels} formatTime={formatTime} active={isFocused} />
55
+ ```
56
+
57
+ ## 축과 기본값
58
+
59
+ | prop | 값 | 기본값 | 설명 |
60
+ | --- | --- | --- | --- |
61
+ | state | `{ playing, position, duration \| null, buffering? }` | 필수 | 엔진 관측값. 음수·비유한 값은 `RangeError` |
62
+ | onPlay / onPause | () => void \| Promise | 필수 | 실제 엔진 호출. 실패는 reject |
63
+ | onSeek | (seconds) => void \| Promise | 필수 | 실제 seek 확인 후 resolve. [0, duration]으로 제한해 호출 |
64
+ | labels | play·pause·seek·decrement·increment·unknownDuration·error | 필수 | 현지화 문구 |
65
+ | formatTime | (seconds) => string | 필수 | 시간 표시와 Slider 값 문구 |
66
+ | disabled | boolean | false | 모든 제어 비활성 |
67
+ | active | boolean | true | false면 제어·모핑 정지, 탐색 초안 폐기 |
68
+
69
+ ## 배치
70
+
71
+ | 항목 | 값 | 근거 |
72
+ | --- | --- | --- |
73
+ | 크기 | IconButton(ghost) 안 글리프 24×24, Slider step 0.1초 | 양 renderer |
74
+ | 간격 | 바깥 세로 md, 제어·시간 행 sm | 양 renderer |
75
+ | 순서·정렬 | [재생/일시 정지 · 현재/전체 시간(caption)] 세로 중앙 → 탐색 Slider(길이를 알 때만) → 오류(가로 중앙) | 양 renderer |
76
+ | 고정·스크롤 | 영상·오디오 host 아래, 바깥 host 소유 | 별도 scroller 없음 |
77
+ | 좁은 폭·큰 글자 | Slider는 본문 폭, 제어 행은 줄바꿈 없음 | 양 renderer |
78
+
79
+ ## 꼭 지킬 것
80
+
81
+ - `state`는 엔진 이벤트에서만 갱신한다. callback 성공만으로 playing을 바꾸지 않는다.
82
+ - `duration: null`(라이브·미확인)이나 0이면 탐색 Slider가 사라진다. 알려진 양의 길이만 탐색한다.
83
+ - 다른 미디어로 바꿀 때 host `key`를 바꾸고 이전 엔진을 해제한다. 숨길 때는 `active={false}`와 함께 엔진 pause를 제품이 실행한다.
84
+
85
+ ## 상태와 상호작용
86
+
87
+ | 상태 | 모습 |
88
+ | --- | --- |
89
+ | 기본 | 실제 playing에 맞는 play/pause 모양과 이름, `playing`이면 selected |
90
+ | 버퍼링 | 정지 상태에서만 버튼 로딩. 재생 중이면 일시 정지 가능 |
91
+ | 요청 중 | 버튼 로딩, 탐색 잠금, 중복 요청 차단 |
92
+ | 탐색 | 드래그·키 입력 중 값은 로컬 초안, 놓으면 `onSeek` 한 번. 끝나면 관측 위치로 복귀 |
93
+ | 실패 | 관측 상태 유지, 중앙 오류, 같은 제어로 재시도 |
94
+
95
+ 모핑은 `motion.normal` 동안만 실행하고 reduced motion·비활성·백그라운드에서는 목표 모양으로 즉시 정착한다. 프레임마다 React 렌더를 하지 않는다.
96
+
97
+ ## 플랫폼 차이
98
+
99
+ | 항목 | Web | Native |
100
+ | --- | --- | --- |
101
+ | 글리프 | SVG `currentColor`, requestAnimationFrame으로 path만 변경 | react-native-svg `contentBrand`, Animated.Value listener로 path만 변경(`useNativeDriver: false`) |
102
+ | 정지 조건 | `document.hidden` | `AppState`가 active 아님 |
103
+ | 탐색 단계 문구 | `decrement`·`increment`를 Slider에 전달하지 않음 | Slider `decrementLabel`·`incrementLabel` |
104
+ | 실패 알림 | `role="alert"` | `accessibilityRole="alert"`·assertive |
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.17.1
7
- - 검토일: 2026-10-07
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
 
@@ -106,6 +106,10 @@ import { Image } from "react-native";
106
106
  | 크기 | 입력 `composerMinLines` 1 ~ `composerMaxLines` 5줄(넘으면 입력 안에서 스크롤), `shape="large"`; 첨부 썸네일 `attachmentSize` 80(radius `radius.md`); 제거 버튼은 `IconButton size="small"` 36 + 사방 hit 4(실제 타깃 44), 그 안 보이는 원 `attachmentRemoveSize` 24; circle 전송은 `IconButton size="small"` 36 | `screen-patterns.ts`, `icon-button-recipe.ts` `sizes.small`, Web `styles.css`, Native `MessageComposer` |
107
107
  | 간격 | 세로 묶음(context·답장 대상·첨부·입력 줄) 사이와 입력–텍스트 전송 버튼 사이 `screenPatternRecipe.itemGap`(`spacing.sm` 12, 두 플랫폼. 2026-10-06까지 Web은 8, 1.13.0 변경); 답장 대상 줄·첨부 사이 `spacing.sm` 12; circle 전송은 입력 글자와 `spacing.sm` 12, 오른쪽 끝 8 | `screenPatternRecipe.itemGap`, Web `.hjm-message-composer`·`__row`(`--hjm-message-composer-gap`), Native `Stack gap="sm"`·`itemGap` |
108
108
  | 순서·정렬 | `context` → 답장 대상(작성자·발췌 → 취소) → 첨부 썸네일(→ 첨부 버튼) → 입력·전송 한 줄, 전송은 입력 아래쪽(`flex-end`)에 맞춘다. 입력 줄 안은 `leadingAction` → 글자 → (아이콘 모드) 전송/첨부 | 렌더 순서, Web `forms.tsx` `hjm-field__leading` |
109
+ | 작성자 아바타 | 미게시: `leading`은 입력칸 밖, 입력 줄 시작 쪽에 놓는다(세로 가운데). 이모지 빠른 입력 줄은 그 위에서 작성창 전체 폭을 쓴다. 아바타를 작성창 바깥 Stack으로 감싸지 않는다 | Web `.hjm-message-composer__leading`, Native `MessageComposer` |
110
+ | 이모지 빠른 입력 | 미게시: `quickInserts={ label, items: [{ id, emoji, label }] }`를 주면 입력 줄 위에 44 원형 칸(이모지 26)을 고르게 펼친다. 누르면 초안 끝에 이모지를 붙인다(인스타그램 댓글 시트 실측). 댓글처럼 길게 누르기 반응이 없는 화면에서 쓴다 | `composerQuickInsertRecipe` |
111
+ | 높이 고정 | 미게시: 아이콘 전송 모드에서 전송 버튼 자리를 항상 확보(보이지 않고 보조기술에서도 숨김)해 첫 글자를 입력할 때 입력칸 높이가 바뀌지 않는다 | Web `.hjm-message-composer__trailing`, Native `MessageComposer` |
112
+ | 답장 중 | 미게시: `replyTo`가 있으면 답장 대상(제목 굵게 + 원문 한 줄 말줄임 + 원형 × `IconButton size="small"` secondary)·첨부·입력 줄을 `surface` 배경, radius `radius.xl` 24, 안쪽 `spacing.sm` 12 카드 하나로 감싼다. `replyTo.author`에는 "소희에게 답장"처럼 완성된 문구를 넘긴다 | Web·Native `MessageComposer` |
109
113
  | 고정·스크롤 | 자체 고정 없음: 화면 composer 슬롯(footer)에 넣는다; 첨부는 가로 스크롤; 키보드 adapter는 host 하나만 | ChatScreen·CommentThreadScreen `composer` |
110
114
  | 좁은 폭·큰 글자 | 입력이 남은 폭을 채우고(`flex: 1`, 최소 폭 0) 전송 버튼은 줄지 않는다; 큰 글자에서도 5줄 상한 뒤 입력 안 스크롤로 기록을 가리지 않는다 | Web `.hjm-message-composer__row`, Native `layoutStyle={{ flex: 1 }}` |
111
115
 
@@ -116,7 +120,7 @@ import { Image } from "react-native";
116
120
  - 첨부 `id`는 비어 있지 않고 유일해야 하며 `removeLabel`이 필요하다. 첨부가 있으면 `onRemoveAttachment`가 필수다. 어기면 던진다.
117
121
  - 사진 권한·선택기·업로드·개수 제한은 제품 소유다. 출처 선택 UI는 [PhotoSourceSheet](photo-source-sheet.md)를 쓴다.
118
122
  - Enter는 기본 줄바꿈이다. 데스크톱 채팅만 `submitMode="send"`로 선택할 수 있고 Shift+Enter·IME 조합·Safari keyCode 229는 전송하지 않는다. Native는 별도로 선택하지 않으면 줄바꿈을 유지한다.
119
- - 2026-10-07 번뚝 채택에서 기존 blur/IME/검증 연결이 누락되어 이 공개 축을 추가했다. 제품이 내부 TextArea를 따로 조립하는 대안 대신 기존 MessageComposer 엔진을 확장한다.
123
+ - 입력 연결(blur/IME/검증)이 더 필요하면 제품이 내부 TextArea를 따로 조립하지 않고 기존 MessageComposer 엔진의 공개 축을 확장한다.
120
124
  - 로컬 준비 사진은 `attachments`로 표시하며 UploadItem의 서버 업로드 완료로 가장하지 않는다. 준비 중 해당 항목의 `disabled`와 attachmentAction.disabled를 연결해 편집 가능한 본문과 사진 제거를 분리한다. 서버 성공 전 초안을 지우지 않는다.
121
125
  - Web 배치는 `layoutStyle`로 한다. Native는 스타일 통로가 없어 감싸는 레이아웃에서 배치한다.
122
126
 
@@ -132,4 +136,10 @@ import { Image } from "react-native";
132
136
 
133
137
  ### 고정 아이콘과 큰 글자
134
138
 
135
- 2026-10-06 최근 검색 삭제 기호가 큰 글자에서 잘린 재현에 따라 Native 내장 삭제·메뉴 기호는 고정 아이콘 틀의 크기를 유지한다. 주변 제목·라벨은 계속 확대한다. Chip의 체크와 Toast 닫기는 기존 비확대 경로를 유지하며 회귀 검사에 포함한다. 제품이 전달한 아이콘 슬롯은 제품이 같은 조건을 검증한다.
139
+ Native 내장 삭제·메뉴 기호는 고정 아이콘 틀의 크기를 유지한다. 주변 제목·라벨은 계속 확대한다. Chip의 체크와 Toast 닫기는 기존 비확대 경로를 유지하며 회귀 검사에 포함한다. 제품이 전달한 아이콘 슬롯은 제품이 같은 조건을 검증한다.
140
+
141
+ ## 근거
142
+
143
+ - 답장 중 카드 배치: 2026-10-16 카카오톡 실측.
144
+ - `submitMode` 등 입력 공개 축: 2026-10-07 번뚝 채택에서 기존 blur/IME/검증 연결이 누락됐다.
145
+ - Native 내장 기호 고정 크기: 2026-10-06 최근 검색 삭제 기호가 큰 글자에서 잘리는 것을 재현했다.
@@ -37,6 +37,7 @@ import { ModerationScreen } from "@hjmds/react/screen-flows";
37
37
 
38
38
  <ModerationScreen
39
39
  title={t("report.title")}
40
+ exit={{ kind: "close", label: t("common.close"), onClick: closeReport }}
40
41
  reasonLabel={t("report.reason")}
41
42
  reasons={[
42
43
  { value: "spam", label: t("report.reason.spam") },
@@ -63,6 +64,7 @@ import { ModerationScreen } from "@hjmds/react-native/screen-flows";
63
64
 
64
65
  <ModerationScreen
65
66
  title={t("report.title")}
67
+ exit={{ kind: "close", label: t("common.close"), onPress: closeReport }}
66
68
  reasonLabel={t("report.reason")}
67
69
  reasons={[
68
70
  { value: "spam", label: t("report.reason.spam") },
@@ -93,14 +95,14 @@ import { ModerationScreen } from "@hjmds/react-native/screen-flows";
93
95
  | `children` | 상세 설명 입력 등 추가 내용 |
94
96
  | `submit` | 신고 행동 `{ label, onAction, disabled?, pending? }` |
95
97
  | `block` | 차단 버튼 행동과 `AlertDialog` 확인 요청(`mode: "confirm"`) |
96
- | ScreenLayout props | `title`, `description`, `header`, `leading`, `actions`, `notice`, `state`, `stateAction` 등(`footer` 제외) |
98
+ | ScreenLayout props | `title`, `description`, `exit`(push·모달로 여는 신고 화면의 닫기/뒤로, 헤더 시작 끝), `header`, `actions`, `notice`, `state`, `stateAction` 등(`footer` 제외) |
97
99
 
98
100
  ## 배치
99
101
 
100
102
  | 항목 | 값 | 근거 |
101
103
  | --- | --- | --- |
102
104
  | 크기 | ScreenLayout 폭(최대 720); 사유는 세로 `RadioGroup`, 신고·차단은 `Button` 기본 크기 | `ModerationScreen`, `screen-flows.tsx` `Action` |
103
- | 간격 | 화면 padding `spacing.md` 16; 본문 사유 선택–`children` `spacing.lg` 20; footer 신고–차단 `spacing.sm` 12 | Web·Native `ModerationScreen` `Stack gap="lg"`·`gap="sm"` |
105
+ | 간격 | 화면 padding `spacing.md` 16; 본문 사유 선택–`children` `spacing.lg` 20; footer 신고–차단 `spacing.xs` 8(세로로 쌓은 버튼, `actionGroupRecipe.stackedGap`) | Web·Native `ModerationScreen` `Stack gap="lg"`·`gap="xs"` |
104
106
  | 순서·정렬 | 헤더(제목) → 본문(사유 → 추가 설명 `children`) → footer(신고 primary → 차단 ghost) | 렌더 순서 |
105
107
  | 고정·스크롤 | 헤더·footer 고정, 본문 화면 스크롤; 차단 확인은 AlertDialog 오버레이 | `ScreenLayout`, `AlertDialog` |
106
108
  | 좁은 폭·큰 글자 | 사유 문구는 줄바꿈되고 항목이 세로로 늘어난다; footer 버튼은 세로로 쌓여 좁은 폭에서도 나란히 줄지 않는다 | `RadioGroup orientation="vertical"`, `Stack` |
@@ -110,5 +112,6 @@ import { ModerationScreen } from "@hjmds/react-native/screen-flows";
110
112
  - 신고 버튼은 `reason`이 `reasons` 안의 값이 아니거나 `state`가 `ready`가 아니면 자동으로 막힌다. `reasonPicker`를 쓸 때도 `reasons`에 유효한 값을 넣어야 버튼이 열린다.
111
113
  - 차단 버튼의 `block.action.onAction`은 호출되지 않는다. 버튼은 확인 대화상자만 열고, 실제 차단은 `confirmation.onConfirm`에서 한다.
112
114
  - 서버 신고·권한·차단 mutation, 완료 후 이동은 제품 소유다. 사유 목록과 정책 문구도 제품이 정한다.
115
+ - 신고 화면은 push·모달로 열리므로 `exit`로 나갈 길을 준다. 2026-10-17 화면 검토(S5)에서 exit 없는 예제가 나갈 수 없는 화면으로 렌더링됐다.
113
116
  - 확인 대화상자의 성공 후 닫힘·실패 문구는 [AlertDialog](alert-dialog.md) 계약을 따른다.
114
117
  - `block`을 제거하면 열린 확인 세션도 종료한다. 나중에 차단 행동을 다시 공급해도 사용자가 버튼을 새로 눌러야 확인창이 열린다. 2026-10-13 사라진 행동의 확인 상태가 부활하던 경로를 수정했다. 대상이 바뀌는 제품은 대상 id를 화면 key/초안·요청 수명에 함께 연결한다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.17.1
7
- - 검토일: 2026-10-06
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
 
@@ -70,7 +70,7 @@ import { NotificationInboxScreen } from "@hjmds/react-native/screens";
70
70
  | `state` | `ready` · `loading` · `empty` · `error` · `restricted`. ready 외에는 지역화 `title` 필수 |
71
71
  | `stateAction` | 재시도·로그인 같은 실제 행동 |
72
72
  | `children` | 알림 목록(데이터·페이지 합치기·더 보기는 제품) |
73
- | `header`, `leading`, `actions` | 기존 내비게이션 헤더, 뒤로 가기, “모두 읽음” 같은 행동 |
73
+ | `header`, `exit`, `actions` | 기존 내비게이션 헤더, 뒤로 가기 아이콘, “모두 읽음” 같은 행동 |
74
74
 
75
75
  ## 배치
76
76
 
@@ -78,7 +78,7 @@ import { NotificationInboxScreen } from "@hjmds/react-native/screens";
78
78
  | --- | --- | --- |
79
79
  | 크기 | ScreenLayout 폭(최대 720) 안에서 알림 행이 폭을 채운다 | `ScreenLayout`, `NotificationItem`(ListRow) |
80
80
  | 간격 | 화면 padding `spacing.md` 16(notice 영역은 좌우만); `notice`–`filters` `spacing.sm` 12; 행 사이 간격은 목록(제품) 소유 | Web·Native `NotificationInboxScreen` `Stack gap="sm"` |
81
- | 순서·정렬 | 헤더(`leading` → 제목 → `actions`) → `notice` → `filters` → 알림 목록 | 렌더 순서 |
81
+ | 순서·정렬 | 헤더(`exit` → 제목 → `actions`) → `notice` → `filters` → 알림 목록 | 렌더 순서 |
82
82
  | 고정·스크롤 | `notice`·`filters`는 헤더 아래 고정되고 상태 교체 중에도 남는다; 목록은 기본 화면 스크롤(`scroll="screen"`), 가상화 목록이면 `scroll="content"` | `ScreenLayout` `notice`·`scroll` |
83
83
  | 좁은 폭·큰 글자 | 제목 열 최소 폭 120 × 글자 배율, 모자라면 `actions`(모두 읽음 등)가 다음 줄로 내려간다; 필터 줄바꿈·가로 스크롤은 `filters` 소유 | `screenPatternRecipe.headerMinWidth` |
84
84
 
@@ -1,27 +1,83 @@
1
1
  # NotificationRow
2
2
 
3
- - 단계: 구성
4
- - 상태: 후보
3
+ - 단계: 컴포넌트
4
+ - 상태: 실험
5
5
  - 지원: Web · Native
6
- - 근거: [알림 화면](../screens/common-inbox.md)
6
+ - 적용: 1.26.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [알림 화면](../screens/common-inbox.md), `src/screen-patterns.ts`의 `notificationRowRecipe`, 양 renderer `src/screens.tsx`
9
+ - 스토리북: 별도 예제 미등록
7
10
 
8
11
  ## 언제 쓰나
12
+
9
13
  알림 목록에서 아바타, 유형 배지, 미리보기, 시간, 읽지 않음 표시, 선택적 썸네일을 나란히 배치한다. 2026-10-15 소비 앱이 같은 알림 배치를 각각 복제한 원인을 공통 구현에서 해결했다.
10
14
 
15
+ ## 쓰지 않을 때
16
+
17
+ | 상황 | 대신 쓸 것 |
18
+ | --- | --- |
19
+ | 대화 목록 미리보기 | [ConversationRow](conversation-row.md) |
20
+ | 일반 설정·목록 행 | [ListRow](list-row.md) |
21
+ | 알림 화면 전체 틀 | [NotificationInboxScreen](notification-inbox-screen.md) |
22
+
11
23
  ## 공개 이름과 import
12
- Web은 `@hjmds/react/screens`, Native는 `@hjmds/react-native/screens`의 `NotificationRow`를 사용한다.
24
+
25
+ | 이름 | 역할 | Web | Native |
26
+ | --- | --- | --- | --- |
27
+ | `NotificationRow` | 알림 한 행 표시 | `@hjmds/react/screens` | `@hjmds/react-native/screens` |
13
28
 
14
29
  ## 최소 사용 예
30
+
15
31
  ```tsx
16
32
  // Web
17
33
  import { NotificationRow } from '@hjmds/react/screens';
18
- <NotificationRow avatar={avatar} preview={message} metadata={timeLabel} unread={unread} />
34
+ <Link href={destination} aria-label={accessibleSummary}>
35
+ <NotificationRow avatar={avatar} preview={message} metadata={timeLabel} unread={unread} />
36
+ </Link>
19
37
  ```
38
+
20
39
  ```tsx
21
40
  // Native
22
41
  import { NotificationRow } from '@hjmds/react-native/screens';
23
- <NotificationRow avatar={avatar} preview={message} metadata={timeLabel} unread={unread} />
42
+ <Pressable accessibilityRole="link" accessibilityLabel={accessibleSummary} onPress={openNotification}>
43
+ <NotificationRow avatar={avatar} preview={message} metadata={timeLabel} unread={unread} />
44
+ </Pressable>
24
45
  ```
25
46
 
26
- ## 배치와 소유권
27
- HJM이 44px 아바타·썸네일, 20px 유형 배지, 2줄 미리보기와 간격을 소유한다. `avatarBadge`·`thumbnail`은 선택 슬롯이다. 제품은 바깥 링크/Pressable 하나에서 이동, disabled, 전체 접근성 이름을 연결한다. 내부는 장식 표현이므로 별도 클릭 대상을 넣지 않는다. `preview`는 인라인 텍스트 콘텐츠다.
47
+ ## 축과 기본값
48
+
49
+ | prop | 값 | 기본값 | 설명 |
50
+ | --- | --- | --- | --- |
51
+ | avatar | ReactNode | 필수 | 행위자 아바타(원형으로 잘림) |
52
+ | preview | ReactNode | 필수 | 인라인 텍스트 콘텐츠. 상호작용 요소를 넣지 않는다 |
53
+ | metadata | string | 필수 | 현지화된 시간 |
54
+ | avatarBadge | ReactNode | 없음 | 아바타 오른쪽 아래 유형 배지 |
55
+ | thumbnail | ReactNode | 없음 | 대상 썸네일 |
56
+ | unread | boolean | false | 시간 옆 읽지 않음 점 |
57
+
58
+ ## 배치
59
+
60
+ | 항목 | 값 | 근거 |
61
+ | --- | --- | --- |
62
+ | 크기 | 아바타·썸네일 44, 유형 배지 20, 읽지 않음 점 6 | `notificationRowRecipe` |
63
+ | 간격 | 가로 gap 12, 내용 gap 4, 안쪽 여백 세로 12 · 가로 16 | 같은 recipe |
64
+ | 순서·정렬 | 아바타(+배지) → 미리보기 / 시간·읽지 않음 점 → 썸네일, 세로 가운데 | 양 renderer |
65
+ | 고정·스크롤 | 바깥 목록 host 소유 | 별도 scroller 없음 |
66
+ | 좁은 폭·큰 글자 | 미리보기 최대 2줄 말줄임, 아바타·썸네일은 줄지 않는다 | `previewLines: 2`, `flexShrink: 0` |
67
+
68
+ HJM이 44px 아바타·썸네일, 20px 유형 배지, 2줄 미리보기와 간격을 소유한다. `avatarBadge`·`thumbnail`은 선택 슬롯이다. 앱은 별도 행 padding을 넣지 않는다.
69
+
70
+ ## 꼭 지킬 것
71
+
72
+ 제품은 바깥 링크/Pressable 하나에서 이동, disabled, 전체 접근성 이름을 연결한다. 내부는 장식 표현이므로 별도 클릭 대상을 넣지 않는다. `preview`는 인라인 텍스트 콘텐츠다. HJM 내부 색·간격을 제품 CSS로 덮지 않는다.
73
+
74
+ ## 플랫폼 차이
75
+
76
+ | 항목 | Web | Native |
77
+ | --- | --- | --- |
78
+ | 낭독 숨김 | 루트 `aria-hidden="true"` | `accessibilityElementsHidden`, `importantForAccessibility="no-hide-descendants"` |
79
+ | 미리보기 줄 수 | `-webkit-line-clamp: 2` | `numberOfLines={2}` |
80
+
81
+ ## 관련 지침
82
+
83
+ [알림 화면](../screens/common-inbox.md), [ConversationRow](conversation-row.md), [ListRow](list-row.md).
@@ -93,7 +93,7 @@ import { OnboardingScreen } from "@hjmds/react-native/screen-flows";
93
93
  | 항목 | 값 | 근거 |
94
94
  | --- | --- | --- |
95
95
  | 크기 | ScreenLayout의 기본 최대 폭 720; 다음·완료·이전은 `Button` 기본 크기 | `screenPatternRecipe.maxWidth` |
96
- | 간격 | 기본 화면 padding `spacing.md` 16; footer 다음/완료–이전 `spacing.sm` 12; Native 본문 제목·설명·진행·건너뛰기·content 사이 `spacing.md` 16. 단계 content 내부 간격은 제품 소유 | ScreenLayout·양 renderer OnboardingScreen |
96
+ | 간격 | 기본 화면 padding `spacing.md` 16; footer 다음/완료–이전 `spacing.xs` 8(세로로 쌓은 버튼, `actionGroupRecipe.stackedGap`); Native 본문 제목·설명·진행·건너뛰기·content 사이 `spacing.md` 16. 단계 content 내부 간격은 제품 소유 | ScreenLayout·양 renderer OnboardingScreen |
97
97
  | 순서·정렬 | Web: 헤더(제목·설명 → 건너뛰기) → 진행 문구 → content. Native: 본문(제목 → 설명 → 진행 문구 → 건너뛰기 → content). 둘 다 footer는 다음 또는 완료 → 이전, 첫 단계는 이전 없음 | 실제 렌더 순서 |
98
98
  | 고정·스크롤 | Web은 헤더·진행 문구·footer가 본문 스크롤 밖. Native는 제목·설명·진행·건너뛰기·content가 함께 스크롤하고 footer만 고정 | Native의 빈 header·기본 scroll="screen" |
99
99
  | 좁은 폭·큰 글자 | Web 헤더 제목 열은 최소 폭 120 × 글자 배율이며 actions가 줄바꿈한다. Native는 본문 세로 Stack을 사용한다. 둘 다 footer 버튼은 세로로 쌓인다 | ScreenLayout·OnboardingScreen |
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.15.0
7
- - 검토일: 2026-10-08
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [디자인 프로필](../../design-profile.md), 양 renderer `src/design-profile.tsx`; 기존 ScreenLayout·Grid·Collapsible의 상태 엔진을 재사용한다.
9
9
  - 스토리북: `배포/컴포넌트/레이아웃/목록 화면 골격`
10
10
 
@@ -63,13 +63,19 @@ import { OverviewScreen } from "@hjmds/react-native/design-profile";
63
63
  | 항목 | 값 | 근거 |
64
64
  | --- | --- | --- |
65
65
  | 크기 | editorial은 `layout.readingMaxWidth` 720, dashboard·landscape는 `layout.contentMaxWidth` 1200, host가 높이를 제공한다 | `resolveDesignProfileScreen`, ScreenLayout |
66
- | 간격 | 도구–목록 `spacing.xl` 24, rows `spacing.sm` 12, cards/grid `spacing.lg` 20, 카드 내부 rows `spacing.md` 16 / 나머지 `spacing.xl` 24 | OverviewScreen·공유 collection resolver |
66
+ | 본문 폭 | 도구와 목록이 한 열: 최대 `layout.readingMaxWidth` 720, header 제목·BottomCTA 내용과 같은 가운데 축(`overviewContentLayout`). 카드 끝과 입력 끝이 같은 선에 놓인다 | `@hjmds/design-contracts/design-profile-layout` |
67
+ | 간격·항목 | 도구–목록 `spacing.xl` 24. rows는 테두리 없는 항목을 규칙선(Divider, `border`)으로 나누고 선 위아래 `spacing.md` 16, 안쪽 여백 0. cards/grid는 테두리·raised 카드, 간격 `spacing.lg` 20, 안쪽 `spacing.xl` 24 | `resolveDesignProfileCollection`·`resolveDesignProfileCollectionItem` |
67
68
  | 순서·정렬 | 헤더 → notice → 도구 → 목록 → footer. leading은 뒤로, actions는 화면 도구, footer는 주 행동 | ScreenLayout 슬롯 |
68
69
  | 고정·스크롤 | 헤더·notice·footer 고정, 본문 스크롤. 가상 목록은 별도 ScreenLayout + `scroll="content"` | ScreenLayout 상태·스크롤 엔진 |
69
- | 좁은 폭·큰 글자 | rows 1/1/1, cards 1/2/2, grid 1/2/3(compact/medium/expanded). 최소 열 폭 240이며 실제 컨테이너·글자 배율로 열 수를 줄인다 | `resolveDesignProfileCollection`·Grid |
70
+ | 좁은 폭·큰 글자 | rows 1/1/1, cards 1/2/2, grid 1/2/3(compact/medium/expanded). 최소 열 폭 226(720 열에 grid 3열과 간격 2개가 들어가는 값)이며 실제 컨테이너·글자 배율로 열 수를 줄인다 | `resolveDesignProfileCollection`·Grid |
70
71
 
71
72
  `collection`, `toolbarPresentation`, `presentation` 명시값이 프로필을 이긴다. 작은 화면의 열 수는 늘리지 않는다. virtual list는 이 정적 items 목록 대신 기존 ScreenLayout의 `scroll="content"`와 제품 list를 쓴다.
72
73
 
73
74
  ## 꼭 지킬 것
74
75
 
75
76
  ScreenLayout의 ready/loading/empty/error와 stateAction을 그대로 사용한다. 초기 조회 오류만 본문을 교체하고 저장 실패는 notice로 알린다. stable id가 중복되거나 비어 있으면 거부한다. 도구를 닫으면 입력 상태는 유지하되 접근성·포커스 대상에서 제외한다. 제품이 서버 확정·재시도·동시 저장 방지를 소유한다. 프로필은 같은 아이템의 입력 subtree를 갈아 끼우지 않는다.
77
+
78
+ ## 근거
79
+
80
+ - 2026-10-17 구성 리뷰: dashboard 표현에서 도구만 왼쪽 반폭에 붙어 가운데 header·footer와 정렬 축이 섞였다.
81
+ - 2026-10-17 사용자 검토: 도구만 읽기 폭이고 목록은 화면 폭이라 카드 행이 입력보다 넓어 좌우 끝이 어긋났다(1024px에서 카드 48–976, 입력 152–872). 도구와 목록을 한 열로 묶고, 그 열에서 grid 3열이 실제로 나오도록 최소 열 폭을 열 폭에서 유도했다. rows는 작은 테두리 상자를 쌓아 cards와 구분되지 않았고 에디토리얼의 규칙선을 표현할 수 없어 규칙선 목록으로 바꿨다. 항목 host(Surface)는 모든 구성에서 같아 테마를 바꿔도 항목 내용이 다시 마운트되지 않는다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-15
7
+ - 검토일: 2026-10-16
8
8
  - 근거: [Pagination](../../pagination.md), `src/pagination.ts`(`paginationRecipe`)
9
9
  - 스토리북: `배포/컴포넌트/탐색/페이지 이동`
10
10
 
@@ -46,9 +46,24 @@ import { Pagination } from "@hjmds/react/pagination";
46
46
  />
47
47
  ```
48
48
 
49
- Native는 같은 props로 `@hjmds/react-native/pagination`에서 가져온다. 조회 중에는 `disabled`로 이전·번호·다음 행동을 함께 잠근다. `testID`는 탐색 묶음에 붙는다.
49
+ ```tsx
50
+ // Native
51
+ import { Pagination } from "@hjmds/react-native/pagination";
52
+
53
+ <Pagination
54
+ label={t("records.pagination")}
55
+ descriptor={{ currentPage: page, totalCount: total, pageSize: 20 }}
56
+ labels={{ previous: t("pagination.previous"), next: t("pagination.next") }}
57
+ composeAccessibleName={({ page, totalPages, current }) =>
58
+ t(current ? "pagination.current" : "pagination.goTo", { page, totalPages })}
59
+ onPageChange={(next) => setPage(next)}
60
+ disabled={isFetching}
61
+ />
62
+ ```
63
+
64
+ Native는 같은 props를 받는다. 조회 중에는 `disabled`로 이전·번호·다음 행동을 함께 잠근다. `testID`는 탐색 묶음에 붙는다.
50
65
 
51
- 2026-10-15 다에리의 유한 편지 목록에서 페이지 번호가 이미 제품 기능이었으나 renderer 부재로 공통 배치를 복제한 원인을 수정했다. Native renderer는 다음 게시본부터 제공하며 현재 후보 상태다.
66
+ 2026-10-15 다에리의 유한 편지 목록에서 페이지 번호가 이미 제품 기능이었으나 renderer 부재로 공통 배치를 복제한 원인을 수정했다. Native renderer는 1.26.0부터 게시본에 포함된다.
52
67
 
53
68
  ## 축과 기본값
54
69
 
@@ -0,0 +1,90 @@
1
+ # PasswordRulesField
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [작업 선택 계약](../../task-controls.md), `src/task-controls.ts`의 `resolvePasswordRules`, 양 renderer `src/task-controls.tsx`
9
+ - 스토리북: `배포/구성/입력과 작성/비밀번호 규칙 확인`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 새 비밀번호 입력 아래에 제품이 정한 규칙의 충족 개수와 규칙별 상태를 함께 보여 줄 때 쓴다.
14
+ [PasswordField](password-field.md)에 진행 막대와 규칙 목록을 붙인 구성이며, 충족 개수만 센다.
15
+ 비밀번호 보안 강도(entropy)는 추정하지 않는다. 묶음 사용 기준은 [선택과 실제 실행](../compositions/task-controls.md)이다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 로그인처럼 규칙 표시가 없는 비밀번호 입력 | [PasswordField](password-field.md) |
22
+ | 일반 텍스트 입력 | `TextField`([Field](field.md)) |
23
+ | 보안 강도 점수 표시 | 제품 전용 구성(HJM은 강도를 계산하지 않는다) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `PasswordRulesField` | 비밀번호 입력 + 규칙 충족 막대·목록 | `@hjmds/react/task-controls` | `@hjmds/react-native/task-controls` |
30
+
31
+ 루트 barrel에는 없다. 규칙 타입 `PasswordRule`과 `resolvePasswordRules`는 `@hjmds/design-contracts/task-controls`에 있다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { PasswordRulesField } from "@hjmds/react/task-controls";
38
+
39
+ <PasswordRulesField label={t("password.new")} value={password} onValueChange={setPassword}
40
+ autofillHint="new" revealLabel={t("password.reveal")} concealLabel={t("password.conceal")}
41
+ progressLabel={t("password.rules")}
42
+ summaryLabel={(satisfied, total) => t("password.summary", { satisfied, total })}
43
+ ruleLabel={rule => t(rule.satisfied ? "password.met" : "password.unmet", { rule: rule.label })}
44
+ rules={[{ id: "length", label: t("password.length"), test: v => v.length >= 8 }]} />
45
+ ```
46
+
47
+ ```tsx
48
+ // Native
49
+ import { PasswordRulesField } from "@hjmds/react-native/task-controls";
50
+
51
+ <PasswordRulesField label={t("password.new")} value={password} onValueChange={setPassword}
52
+ autofillHint="new" revealLabel={t("password.reveal")} concealLabel={t("password.conceal")}
53
+ progressLabel={t("password.rules")}
54
+ summaryLabel={(satisfied, total) => t("password.summary", { satisfied, total })}
55
+ ruleLabel={rule => t(rule.satisfied ? "password.met" : "password.unmet", { rule: rule.label })}
56
+ rules={rules} />
57
+ ```
58
+
59
+ ## 축과 기본값
60
+
61
+ | prop | 값 | 기본값 | 설명 |
62
+ | --- | --- | --- | --- |
63
+ | `label` | `string` | 필수 | 입력 이름 |
64
+ | `value` / `onValueChange` | `string` / `(value) => void` | 필수 | controlled 전용. `defaultValue`는 받지 않는다 |
65
+ | `rules` | `{ id, label, test(value) }[]` | 필수 | 비어 있거나 id가 겹치면 `TypeError`(`resolvePasswordRules`) |
66
+ | `progressLabel` | `string` | 필수 | 진행 막대 이름 |
67
+ | `summaryLabel` | `(satisfied, total) => string` | 필수 | 막대의 값 문구(`valueText`) |
68
+ | `ruleLabel` | `(rule: { id, label, satisfied }) => string` | 필수 | 규칙 한 줄 문구. 충족 여부를 글자로 포함한다 |
69
+ | 나머지 | `PasswordFieldProps` | — | `autofillHint`·`revealLabel`·`concealLabel`·`description`·`error` 등은 [PasswordField](password-field.md)로 그대로 전달 |
70
+
71
+ ## 배치
72
+
73
+ | 항목 | 값 | 근거 |
74
+ | --- | --- | --- |
75
+ | 크기 | 입력은 PasswordField 기본, 막대는 Progress 기본(`max`=규칙 수) | 양 renderer `PasswordRulesField` |
76
+ | 간격 | 입력·막대·규칙 줄 사이 `Stack gap="sm"`(12) | 같은 구현, `foundations.ts` space.sm |
77
+ | 순서·정렬 | 비밀번호 입력 → 충족 막대 → 규칙별 caption(`rules` 순서) | 렌더 순서 |
78
+ | 고정·스크롤 | 자체 고정·스크롤 없음, 바깥 폼 host 소유 | 별도 scroller 없음 |
79
+ | 좁은 폭·큰 글자 | 규칙 문구는 caption Text로 줄바꿈된다 | 양 renderer `Text variant="caption"` |
80
+
81
+ ## 꼭 지킬 것
82
+
83
+ - 충족 개수를 "보안 강도"라고 부르지 않는다. 규칙 `test`는 순수 함수이고 예외를 던지지 않게 만든다.
84
+ - 충족 여부는 색(`primary`/`muted`)만으로 구분하지 않는다. `ruleLabel`이 "충족/미충족"을 글자로 말해야 한다.
85
+ - 제출 가능 여부·서버 검증은 제품이 결정한다. 이 구성은 제출을 막지 않는다.
86
+
87
+ ## 상태와 상호작용
88
+
89
+ 입력이 바뀔 때마다 `resolvePasswordRules(value, rules)`로 다시 센다. 충족한 규칙은 `tone="primary"`, 미충족은 `tone="muted"`로 표시한다.
90
+ 보이기/숨기기와 자동 채움은 PasswordField 계약을 따른다.
@@ -94,7 +94,7 @@ import { Linking } from "react-native";
94
94
  | 항목 | 값 | 근거 |
95
95
  | --- | --- | --- |
96
96
  | 크기 | ScreenLayout 폭(최대 720); 그림 크기는 `illustration`(제품) 소유; 행동은 `Button` 기본 크기 | `PermissionScreen` |
97
- | 간격 | 화면 padding `spacing.md` 16; 본문 그림–설명 `spacing.xl` 24(가운데 정렬); footer 주 행동–나중에 `spacing.sm` 12 | Web·Native `PermissionScreen` `Stack gap="xl" align="center"`·`gap="sm"` |
97
+ | 간격 | 화면 padding `spacing.md` 16; 본문 그림–설명 `spacing.xl` 24(가운데 정렬); footer 주 행동–나중에 `spacing.xs` 8(세로로 쌓은 버튼, `actionGroupRecipe.stackedGap`) | Web·Native `PermissionScreen` `Stack gap="xl" align="center"`·`gap="xs"` |
98
98
  | 순서·정렬 | 헤더(제목) → 본문(그림 → 설명, 가운데) → footer(상태별 주 행동 primary → 나중에 ghost) | 렌더 순서, `resolvePermissionAction` |
99
99
  | 고정·스크롤 | 헤더·footer 고정, 본문 화면 스크롤; OS 권한 창은 제품이 `request.onAction`에서 호출 | `ScreenLayout` |
100
100
  | 좁은 폭·큰 글자 | 설명은 줄바꿈되고 본문이 스크롤된다; footer 버튼은 세로로 쌓인다; `unavailable`이면 주 행동 없이 `skip`만 남는다 | `PermissionScreen` |
@@ -94,7 +94,7 @@ import { PhotoSourceSheet } from "@hjmds/react-native/screen-flows";
94
94
  | 항목 | 값 | 근거 |
95
95
  | --- | --- | --- |
96
96
  | 크기 | Sheet 기본 `placement="bottom"`·`size="auto"`(내용 높이, 화면 높이 0.9 상한), Web 최대 폭 640; 선택 버튼은 `tone="secondary"` `Button` 두 개 | `sheetRecipe`, `screen-flows.tsx` |
97
- | 간격 | 시트 안 여백은 Sheet 소유(두 플랫폼 좌우 `spacing.lg` 20 · 위아래 `spacing.sm` 12 + 하단 safe area); 두 버튼 사이 `spacing.sm` 12; 바깥에 padding을 더하지 않는다 | Web `.hjm-sheet__body`, Native `sheetRecipe.content`, `Stack gap="sm"` |
97
+ | 간격 | 시트 안 여백은 Sheet 소유(두 플랫폼 좌우 `spacing.lg` 20 · 위아래 `spacing.sm` 12 + 하단 safe area); 두 버튼 사이 `spacing.xs` 8(세로로 쌓은 버튼, `actionGroupRecipe.stackedGap`); 바깥에 padding을 더하지 않는다 | Web `.hjm-sheet__body`, Native `sheetRecipe.content`, `Stack gap="xs"` |
98
98
  | 순서·정렬 | Sheet 제목·닫기 → 앨범 → 촬영(`cameraAvailable`일 때만) | Web `screen-flows.tsx` 59–60행, Native 50–51행 |
99
99
  | 고정·스크롤 | 화면 하단에 뜨는 오버레이(배경 막) — 선택 뒤 picker 호출 시점은 플랫폼 차이 참고 | `Sheet` |
100
100
  | 좁은 폭·큰 글자 | 버튼 문구는 줄바꿈되고 Sheet 높이가 늘어난다(0.9 상한을 넘으면 본문 스크롤); 카메라가 없으면 버튼 하나만 남는다 | `sheetRecipe.content.maxHeightRatio` |
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-07
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [Popover](../../popover.md), [ConfirmPopover 조합](../../confirm-popover.md), `src/popover.ts`(`popoverRecipe`)
9
9
  - 스토리북: `배포/컴포넌트/오버레이/팝오버`
10
10
 
@@ -33,13 +33,15 @@
33
33
 
34
34
  ```tsx
35
35
  // Web
36
- import { Button } from "@hjmds/react/actions";
36
+ import { IconButton } from "@hjmds/react/actions";
37
+ import { Icon } from "@hjmds/react/display";
37
38
  import { Popover } from "@hjmds/react/popover";
38
39
 
39
40
  <Popover
40
- trigger={<Button tone="secondary">{t("filter.open")}</Button>}
41
+ trigger={<IconButton label={t("filter.open")} tone="secondary"><Icon name="filter" /></IconButton>}
41
42
  title={t("filter.title")}
42
43
  closeLabel={t("common.close")}
44
+ closeIcon={<Icon name="close" />}
43
45
  descriptor={{ placement: "bottom", align: "start" }}
44
46
  >
45
47
  {({ close }) => <FilterForm onApply={() => { apply(); close(); }} />}
@@ -54,6 +56,7 @@ Native 예는 없다(renderer 없음).
54
56
  | --- | --- | --- | --- |
55
57
  | `trigger` | 요소 하나 | 필수 | 여는 버튼. Popover가 ref·aria 속성을 붙인다 |
56
58
  | `title` · `closeLabel` | 문자열 | 필수 | 비면 던진다 |
59
+ | `closeIcon` | `ReactNode` | — | 주면 머리 닫기를 ghost `IconButton`(이름 `closeLabel`)으로 그린다. 도구성 행동은 아이콘으로 둔다([IconButton](icon-button.md)). 없으면 기존 글자 버튼 |
57
60
  | `description` | 문자열 | — | 제목 아래 설명 |
58
61
  | `children` | 노드 또는 `(actions: { close(): void }) => ReactNode` | — | 콘텐츠 안에서 닫을 때 함수형을 쓴다 |
59
62
  | `openOn` | `press` · `hover` | `press` | `hover`는 지연된 포인터 진입(열기 300ms·닫기 150ms)을 **더할** 뿐이고 click/Enter/Space 경로는 그대로다 |
@@ -0,0 +1,85 @@
1
+ # PricingChoice
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [작업 선택 계약](../../task-controls.md), `src/task-controls.ts`의 `resolvePriceOption`, 양 renderer `src/task-controls.tsx`
9
+ - 스토리북: `배포/구성/선택과 필터/요금 주기 선택`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 요금제·결제 주기 중 하나를 고르고, 선택한 실제 가격과 혜택을 확인한 뒤 [계속]으로 제품 결제 흐름을 시작할 때 쓴다.
14
+ 선택은 controlled callback만 호출하고, 실제 요청은 계속 버튼에서만 시작한다. 실패하면 선택을 유지하고 같은 버튼으로 다시 시도한다.
15
+ 묶음 사용 기준은 [선택과 실제 실행](../compositions/task-controls.md)이다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 비동기 실행 없는 단순 선택 | [RadioGroup](radio-group.md) `presentation="card"` |
22
+ | 결제·약관·상품 검증 자체 | 제품 결제 흐름(onContinue 뒤) |
23
+ | 실행 방식 선택 + 즉시 실행 | `SplitAction`([선택과 실제 실행](../compositions/task-controls.md)) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `PricingChoice` | 요금 선택 카드·가격·혜택·계속 | `@hjmds/react/task-controls` | `@hjmds/react-native/task-controls` |
30
+
31
+ 루트 barrel에는 없다. `PriceOption` 타입과 `resolvePriceOption`은 `@hjmds/design-contracts/task-controls`에 있다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { PricingChoice } from "@hjmds/react/task-controls";
38
+
39
+ <PricingChoice label={t("plan.title")} options={prices} selectedId={selectedId}
40
+ onSelectionChange={setSelectedId} formatPrice={formatConfirmedPrice}
41
+ continueLabel={t("plan.continue")} errorLabel={t("plan.failed")}
42
+ onContinue={option => startCheckout(option.id)} />
43
+ ```
44
+
45
+ ```tsx
46
+ // Native
47
+ import { PricingChoice } from "@hjmds/react-native/task-controls";
48
+
49
+ <PricingChoice label={t("plan.title")} options={prices} selectedId={selectedId}
50
+ onSelectionChange={setSelectedId} formatPrice={formatConfirmedPrice}
51
+ continueLabel={t("plan.continue")} errorLabel={t("plan.failed")}
52
+ onContinue={option => startCheckout(option.id)} />
53
+ ```
54
+
55
+ ## 축과 기본값
56
+
57
+ | prop | 값 | 기본값 | 설명 |
58
+ | --- | --- | --- | --- |
59
+ | `label` | `string` | 필수 | Card 제목이자 RadioGroup 접근성 이름 |
60
+ | `options` | `PriceOption[]` | 필수 | `{ id, label, description?, disabled?, amountMinor, currency, interval: "month" \| "year", intervalCount, benefits }`. `amountMinor`는 0 이상 안전 정수, `currency`는 대문자 3자, `intervalCount`는 1 이상 정수가 아니면 `RangeError` |
61
+ | `selectedId` / `onSelectionChange` | `string` / `(id) => void` | 필수 | controlled. 목록에 없는 id·중복 id는 `TypeError` |
62
+ | `formatPrice` | `(option) => string` | 필수 | 라디오 설명과 선택 가격 줄에 같이 쓴다 |
63
+ | `continueLabel` / `errorLabel` | `string` | 필수 | 계속 버튼·실패 문구 |
64
+ | `onContinue` | `(option) => void \| Promise<void>` | 필수 | 선택 옵션의 고정된(frozen) 사본을 받는다. reject하면 실패 표시 |
65
+ | `disabled` | `boolean` | `false` | 선택과 계속을 모두 잠근다 |
66
+
67
+ ## 배치
68
+
69
+ | 항목 | 값 | 근거 |
70
+ | --- | --- | --- |
71
+ | 크기 | Card 하나 안에 RadioGroup `presentation="card"`, 계속 Button 기본 크기 | 양 renderer `PricingChoice` |
72
+ | 간격 | Card 안 `Stack gap="md"`(16) | 같은 구현, `foundations.ts` space.md |
73
+ | 순서·정렬 | Card 제목 → 주기별 라디오 카드 → 선택한 가격(strong) → 혜택 줄 → [계속] → 실패 문구(가운데 정렬) | 렌더 순서 |
74
+ | 고정·스크롤 | 자체 고정·스크롤 없음 | 별도 scroller 없음 |
75
+ | 좁은 폭·큰 글자 | 라디오 카드·혜택 Text는 줄바꿈되고 버튼은 Card 폭 안에 머문다 | 양 renderer |
76
+
77
+ ## 꼭 지킬 것
78
+
79
+ - `formatPrice`는 실제 `amountMinor`·`currency`·`interval`·`intervalCount`와 일치해야 한다. 할인율·월 환산액을 추정해 넣지 않는다.
80
+ - 옵션 선택으로 결제를 시작하지 않는다. 결제·약관·상품 검증은 `onContinue` 뒤 제품 흐름 책임이다.
81
+ - 선택 옵션이 `disabled`이면 계속 버튼도 비활성이다. 비활성 옵션은 선택 callback을 호출하지 않는다.
82
+
83
+ ## 상태와 상호작용
84
+
85
+ 진행 중에는 계속 버튼이 로딩이 되고 라디오 선택이 잠긴다(중복 요청 차단). 실패하면 선택을 유지하고 Card 안에 오류 문구를 가운데 정렬로 표시한다(Web `role="alert"`).