@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
@@ -73,7 +73,8 @@ import { FolderPreview } from "@hjmds/react-native/folder-preview";
73
73
  | `open` + `onOpenChange` | `boolean` + `(open: boolean) => void` | — | 제어. `open`만 주고 `onOpenChange`를 빼면 `TypeError`를 던진다 |
74
74
  | `defaultOpen` · `onOpenChange` | `boolean` · `(open: boolean) => void` | `defaultOpen` `false` | 비제어. `open`과 `defaultOpen`을 함께 주면 `TypeError`를 던진다 |
75
75
  | `trigger` | `ReactNode` | 필수 | 버튼 문구. Native는 문자열이면 HJM `Text`로 감싼다 |
76
- | `disabled` | `boolean` | `false` | trigger 옆의 ▸/▾ 표시는 HJM이 그리며 장식으로 숨겨진다 |
76
+ | `disabled` | `boolean` | `false` | trigger 옆의 펼침 표시(닫힘 `chevronEnd`·열림 `chevronDown`, glyph.sm 20)는 HJM이 그리며 장식으로 숨겨진다. 닫힘 표시는 RTL에서 읽기 끝 쪽으로 뒤집힌다 |
77
+ | `marker` | `end` · `none` | `end` | `none`이면 표시를 그리지 않고, 한 줄이 아닌 trigger가 `CollapsibleMarker`(`open`)를 직접 둔다(FolderPreview가 사용) |
77
78
  | `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 배치 |
78
79
  | `FolderPreview` `open` · `onOpenChange` | `boolean` · `(open: boolean) => void` | 필수(항상 controlled) | — |
79
80
  | `FolderPreview` `previews` · `label` | `readonly ReactNode[]` · `string` | 필수 | `previews`는 앞의 세 개만 그리고, `label`이 비면 `TypeError`를 던진다 |
@@ -87,7 +88,7 @@ FolderPreview의 Native preview `md`와 앞면 `lg`는 Provider radius를 읽는
87
88
  | --- | --- | --- |
88
89
  | 크기 | 트리거는 폭을 꽉 채운다. 트리거 최소 높이 44(`control.minTouchTarget`, 두 플랫폼. Native는 1.13.0 변경), Web 위아래 `spacing.xs` 8. `FolderPreview` 표지는 최대 260×160 고정 그림 | `.hjm-collapsible__trigger`, `react-native/src/collapsible.tsx`, `react/src/folder-preview.tsx` |
89
90
  | 간격 | 트리거↔내용, 트리거 안 문구↔표시 `spacing.xs` 8(Web 트리거 안은 `spacing.sm` 12) | `collapsibleRecipe.gap`, `.hjm-collapsible__trigger` |
90
- | 순서·정렬 | 위→아래 [트리거: 문구 시작 쪽 · ▸/▾ 끝 쪽] → [내용(열렸을 때만)]. `FolderPreview`는 트리거 안에 [표지 그림(가운데)] → [label]이 쌓인다 | `react/src/collapsible.tsx`, `react/src/folder-preview.tsx` |
91
+ | 순서·정렬 | 위→아래 [트리거: 문구 시작 쪽 · 펼침 표시 끝 쪽] → [내용(열렸을 때만)]. `FolderPreview`는 트리거 안에 [표지 그림] → [label · 펼침 표시 한 줄]이 가운데 정렬로 쌓이고, 그림과 label 사이는 `spacing.sm` 12다(2026-10-17, label이 시작 쪽·표시가 끝 쪽에 떨어져 있던 것을 고침) | `react/src/collapsible.tsx`, `react/src/folder-preview.tsx` |
91
92
  | 고정·스크롤 | 고정 영역이 없다. 닫히면 내용이 트리에서 빠져 아래 내용이 올라온다 | `react-native/src/collapsible.tsx` 주석 |
92
93
  | 좁은 폭·큰 글자 | 트리거 문구·내용이 줄바꿈된다(`overflow-wrap: anywhere`). `FolderPreview` 표지는 폭이 260보다 좁으면 줄어든다(`maxWidth: 260`) | `.hjm-collapsible__content`, `folder-preview.tsx` |
93
94
 
@@ -120,3 +121,7 @@ inline은 트리거를 숨기고 항상 내용을 펼친다. 기본 disclosure
120
121
  keepMounted가 있어도 닫힌 내용은 포커스·스크린리더·터치 대상에서 제외한다. 단순 read-only 내용은 기존 기본값을 쓴다.
121
122
 
122
123
  Web에서 외부 상태 변경으로 닫힐 때 내부에 있던 초점은 trigger로 돌아간다. 동시에 disabled면 disclosure host로 복구하고, 이미 외부로 이동한 초점은 유지한다. 열린 도구를 trigger로 직접 닫으면 해당 버튼에 초점을 유지하므로 Enter로 다시 열 수 있다.
124
+
125
+ ## 근거
126
+
127
+ - 2026-10-17 구성 리뷰: 텍스트 표시 ▸가 약 8px로 그려져 트리거가 정적 문구처럼 보였다. 그래서 두 플랫폼 모두 glyph.sm 20 chevron 아이콘으로 바꿨다.
@@ -0,0 +1,102 @@
1
+ # CommentThreadRow
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 실험
5
+ - 지원: Web · Native
6
+ - 적용: 1.23.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: `src/screen-patterns.ts`의 `commentThreadRecipe`, 양 renderer `src/screen-flows.tsx`의 `CommentThreadRow`·`CommentThreadScreen`
9
+ - 스토리북: 별도 예제 미등록
10
+
11
+ ## 언제 쓰나
12
+
13
+ 가상화 목록(FlatList·가상 스크롤러)처럼 제품이 댓글 목록을 직접 그려야 할 때 댓글 한 행을 그린다.
14
+ `CommentThreadScreen`이 쓰는 것과 같은 행이라 아바타·작성자·본문·답글·좋아요 배치가 화면과 똑같다.
15
+ 1.23.0에서 가상화 댓글 목록용으로 공개했다. 행만 단독 예제는 없고 `배포/화면/소통/댓글`의
16
+ `CommentThreadScreen` 안에서 같은 행이 렌더된다.
17
+
18
+ ## 쓰지 않을 때
19
+
20
+ | 상황 | 대신 쓸 것 |
21
+ | --- | --- |
22
+ | 댓글 화면 전체(헤더·답글 펼침·작성창) | [CommentThreadScreen](comment-thread-screen.md) |
23
+ | 1:1·그룹 메시지 | [ChatMessage](chat-message.md) |
24
+ | 대화 목록 미리보기 | [ConversationRow](conversation-row.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `CommentThreadRow` | 댓글·답글 한 행 | `@hjmds/react/screen-flows` | `@hjmds/react-native/screen-flows` |
31
+
32
+ 행 데이터 타입 `CommentThreadItem`·`CommentReactions`도 같은 entry에서 import한다.
33
+
34
+ ## 최소 사용 예
35
+
36
+ ```tsx
37
+ // Web
38
+ import { CommentThreadRow } from "@hjmds/react/screen-flows";
39
+
40
+ <VirtualList items={comments} renderItem={item =>
41
+ <CommentThreadRow item={item} onLike={toggleLike} onReply={startReply} replyLabel={t("comment.reply")} />} />
42
+ ```
43
+
44
+ ```tsx
45
+ // Native
46
+ import { CommentThreadRow } from "@hjmds/react-native/screen-flows";
47
+
48
+ <FlatList data={comments} keyExtractor={item => item.id}
49
+ renderItem={({ item }) =>
50
+ <CommentThreadRow item={item} onLike={toggleLike} onReply={startReply} replyLabel={t("comment.reply")} />} />
51
+ ```
52
+
53
+ ## 축과 기본값
54
+
55
+ | prop | 값 | 기본값 | 설명 |
56
+ | --- | --- | --- | --- |
57
+ | item | `CommentThreadItem` | 필수 | 댓글 데이터와 슬롯(아래 표) |
58
+ | onLike | `(id) => void` | 필수 | 기본 좋아요 `IconButton` 누름 |
59
+ | onReply | `(id) => void` | 필수 | 답글 문구 행동·길게 누르기 메뉴의 답글 항목 |
60
+ | replyLabel | string | 필수 | 답글 행동 문구 |
61
+ | layoutStyle | ScreenLayout `layoutStyle` | 없음 | 바깥 Stack 배치 보정 |
62
+
63
+ | `item` 필드 | 기본값 | 설명 |
64
+ | --- | --- | --- |
65
+ | `id`·`parentId`·`author`·`body`·`timeLabel`·`likeCountLabel`·`likeIcon`·`likeLabel` | 필수 | `parentId`가 `null`이면 최상위 댓글 |
66
+ | `bodyText` | 없음 | 주면 `author`와 본문을 한 문장으로, 시각은 아래 행동 줄로 옮긴다 |
67
+ | `avatar` | 없음 | 제품 아바타. 최상위 32·답글 24 폭 칸에 둔다 |
68
+ | `canReply` | `parentId === null` | 답글 행동 표시 여부 |
69
+ | `replyDisabled` | false | 답글 행동 비활성 |
70
+ | `likeAction`·`actions` | 없음 | 기본 좋아요 버튼 대체·옆에 추가 |
71
+ | `reactions`·`reactionSummary`·`media` | 없음 | 길게 누르기 메뉴·선택 반응 표시·본문 아래 사진. 제공 버전은 [CommentThreadScreen](comment-thread-screen.md) 지침을 따른다 |
72
+ | `timeDateTime` | 없음 | Web `<time dateTime>` 값. Native는 쓰지 않는다 |
73
+
74
+ ## 배치
75
+
76
+ | 항목 | 값 | 근거 |
77
+ | --- | --- | --- |
78
+ | 크기 | 아바타 칸 최상위 32·답글 24, 좋아요 수 칸 폭 44, `media` 최대 폭 160·radius `radius.md` | `commentThreadRecipe` |
79
+ | 간격 | 아바타·본문·좋아요 열 사이 `spacing.xs` 8, 본문 줄 사이 2, 행동 줄 항목 사이 `spacing.sm` 12, 작성자–시각 `spacing.xxs` 4, `media` 위 4 | `commentThreadRecipe.lineGap`, `Stack gap` |
80
+ | 순서·정렬 | 아바타 → (작성자·시각 → 본문 → media → 시각/반응 요약/답글 행동) → 좋아요 버튼·`actions` 아래 좋아요 수. 위 정렬 | 양 renderer |
81
+ | 고정·스크롤 | 행은 스크롤하지 않는다. 목록 스크롤·행 간격(`rowGap` 12)·답글 들여쓰기(`replyIndent` 40)는 host가 준다 | `CommentThreadScreen` |
82
+ | 좁은 폭·큰 글자 | 본문 열이 `min-width: 0`으로 줄며 줄바꿈되고 작성자·행동 줄은 `flexWrap`. 좋아요 열은 줄지 않는다 | `.hjm-comment-row__text`, `flexShrink: 0` |
83
+
84
+ ## 꼭 지킬 것
85
+
86
+ - 가상화 host에서 행 사이 간격(`commentThreadRecipe.rowGap`)과 답글 들여쓰기(`replyIndent`)를 직접 줄 때도 recipe 값을 쓴다. 제품 숫자로 다시 정하지 않는다.
87
+ - 답글 펼치기·정렬·권한·좋아요 저장은 제품이 소유한다. 행은 콜백만 호출한다.
88
+ - 행 안에 두 번째 레이아웃을 만들지 말고 `actions`·`likeAction`·`media` 슬롯으로 추가한다.
89
+
90
+ ## 상태와 상호작용
91
+
92
+ 답글 행동은 채운 버튼이 아니라 muted caption 문구 버튼이며 `replyDisabled`면 불투명도 0.5로 비활성이다.
93
+ `reactions`를 주면 작성자·본문만 길게 누르기 대상이 되고, 답글 가능하면 메뉴에 답글 항목이 자동으로 붙는다.
94
+ 답글 행동 버튼은 길게 누르기 대상 밖에 남아 자기 버튼 의미를 유지한다.
95
+
96
+ ## 플랫폼 차이
97
+
98
+ | 항목 | Web | Native |
99
+ | --- | --- | --- |
100
+ | 답글 행동 | `<button class="hjm-comment-row__action">` | `Pressable`, `hitSlop` 8 |
101
+ | 시각 | `<time dateTime>` 안 caption | caption `Text` |
102
+ | 길게 누르기 반응 | `MessageReactions` 트리거 class `hjm-comment-row__reactions` | `MessageReactions previewSurface` |
@@ -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
 
@@ -30,6 +30,7 @@
30
30
  | `CommentThreadScreen` | 화면 조합 | `/screen-flows` | `/screen-flows` |
31
31
  | `CommentThreadRow` | 가상화 목록에서도 같은 댓글 행 재사용(1.23.0) | `/screen-flows` | `/screen-flows` |
32
32
  | `CommentThreadItem`(타입) | 댓글 한 개의 데이터 | `/screen-flows` | `/screen-flows` |
33
+ | `CommentMenu`(타입) | 댓글별 세로 점 더보기 메뉴 설정(미게시) | `/screen-flows` | `/screen-flows` |
33
34
 
34
35
  루트 barrel에는 없다. optional native peer를 요구하지 않는다.
35
36
 
@@ -86,13 +87,18 @@ import { MessageComposer } from "@hjmds/react-native/screens";
86
87
  | `replyLabel`·`repliesLabel(count, expanded)` | 지역화 문구. 복수형·펼침 상태는 제품 i18n이 만든다. 펼침 버튼 이름이 이 문구이므로 `expanded`에 따라 "답글 3개 보기"/"답글 숨기기"처럼 상태가 드러나게 만든다 |
87
88
  | `composer` | 작성창(보통 `MessageComposer`의 `replyTo`·`sendIcon`). `ready`·`empty`일 때만 보인다 |
88
89
  | `threadFooter` | 더 보기·커서 페이지네이션 |
90
+ | `commentMenu`(미게시) | 댓글별 더보기 메뉴. `label(item)` 버튼·메뉴의 지역화 접근성 이름(예: "seoyeon 댓글 더 보기"), `items(item)` 그 댓글에 허용된 항목(`{ id, label, tone?: "danger", disabled? }`, Menu·ContextMenu와 같은 항목 어휘), `onAction(item, id)` 메뉴가 닫힌 뒤 호출. Native는 메뉴 시트의 닫기 문구 `dismissLabel`도 받는다. 항목이 비면 그 행에는 버튼을 그리지 않는다. 같은 prop을 `CommentThreadRow`도 받는다 |
89
91
 
90
92
  `CommentThreadItem`: `id`, `parentId`(`null`이면 최상위), `author`, `body`(노드), `timeLabel`, `likeCountLabel`,
91
93
  `likeIcon`, `likeLabel`은 필수. 선택은 `bodyText`(작성자와 본문을 한 줄 흐름으로), `avatar`, `likeAction`(기본
92
94
  좋아요 버튼 교체, `null`이면 생략), `actions`(하트 바로 옆의 더보기 메뉴 등), `canReply`(기본: 최상위만 true), `replyDisabled`.
95
+ `reactions`(미게시)를 주면 작성자·본문을 길게 눌러 ChatMessage와 같은 메뉴(반응 줄 → 미리보기 → 시각 → 답글 달기 → `menuActions` → `reportAction` 신고)가 열린다. 답글 달기는 `canReply`일 때 `onReply`로 자동 연결되고, 아이콘은 `reactions.replyIcon`으로 넘긴다. `reactionSummary`는 답글 달기 앞에 놓는 선택된 반응 표시 슬롯이다. 댓글의 기본은 반응 옵션을 비운(`options: []`) 동작 목록 전용 메뉴다(답글 달기 → `menuActions` → 신고, 인스타그램 댓글 길게 누르기 실측). 댓글 이모지는 길게 누르기가 아니라 작성창의 `quickInserts` 이모지 줄로 입력한다. 반응 줄은 제품이 명시적으로 댓글 반응을 운영할 때만 옵션을 넣는다. 댓글 메뉴에는 시각 제목을 두지 않는다(행에 이미 보인다).
96
+ `media`(미게시, 선택)는 본문 아래 사진·스티커이며 최대 폭 `commentThreadRecipe.mediaMaxWidth` 160, radius 12로 감싼다. `likeCountLabel`은 하트 아래 숫자만(예: "15") 넣고 0이면 빈 문자열로 숨긴다. 개수를 포함한 전체 문장은 `likeLabel`에 둔다.
93
97
 
94
98
  ## 배치
95
99
 
100
+ 좋아요 수는 하트 아래, 답글 달기는 본문 아래 흐린 글자, 답글은 부모 글 열에 맞춘다. 미게시 변경이다.
101
+
96
102
 
97
103
  `threadHeader?: ReactNode`는 장소 소개·원문·필터처럼 댓글과 함께 스크롤할 콘텐츠다.
98
104
  공통 본문 안에서 첫 댓글 앞에 놓이고, `threadFooter`는 마지막 댓글 뒤에 놓인다.
@@ -100,16 +106,14 @@ import { MessageComposer } from "@hjmds/react-native/screens";
100
106
  넣지 않는다. `composer`는 기존 고정 작성 영역을 유지한다. 화면 상태가 본문을 교체하면
101
107
  threadHeader도 댓글과 함께 숨겨지므로 항상 보여야 하는 안내는 `notice`를 사용한다.
102
108
 
103
- 2026-10-08 Spint 실제 화면에서 긴 소개를 고정 header에 넣고 높이를 제한하자 스팟 선택이
104
- 잘리고 스크롤이 둘로 나뉘었다. 임의의 댓글 항목에 소개를 끼워 넣으면 댓글 ID·펼침 계약이
105
- 오염되므로 두 renderer에 동일한 optional 슬롯을 추가했다. 생략한 기존 호출은 바뀌지 않는다.
109
+ 임의의 댓글 항목에 소개를 끼워 넣지 않는다. 두 renderer가 같은 optional 슬롯을 제공하며 생략한 기존 호출은 바뀌지 않는다.
106
110
 
107
111
 
108
112
  | 항목 | 값 | 근거 |
109
113
  | --- | --- | --- |
110
- | 크기 | ScreenLayout 폭(최대 720); 본문 열이 남은 폭을 채우고(`flex: 1`, 최소 폭 0) 오른쪽 끝에 좋아요와 `actions`를 같은 가로 행으로; 답글·펼침 버튼은 `Button size="small"` ghost | Web·Native `CommentThreadScreen` |
111
- | 간격 | 화면 padding `spacing.md` 16; 최상위 댓글 묶음 사이 `spacing.lg` 20; 댓글 행–답글 묶음 `spacing.xs` 8; 답글 묶음 들여쓰기 `sectionGap`(`spacing.xl`) 24, 펼침 버튼·답글 사이 `spacing.md` 16; 행 안 아바타–본문–좋아요 `spacing.sm` 12, 본문 줄 사이 `spacing.xxs` 4, 좋아요–actions 사이 `spacing.xxs` 4, 시각·좋아요 수·답글 버튼 사이 `spacing.sm` 12 | `screen-flows.tsx` `Stack gap`, `screenPatternRecipe.sectionGap` |
112
- | 순서·정렬 | 최상위 댓글(아바타 → 작성자·본문 → 시각·좋아요 수·답글) + 행 오른쪽 좋아요 → `actions` → 답글 펼침 버튼 → 펼친 답글 → … → `threadFooter` → composer(footer) | 렌더 순서 |
114
+ | 크기 | ScreenLayout 폭(최대 720); 아바타 `commentThreadRecipe.avatarSize` 32, 답글 아바타 `replyAvatarSize` 24(제품은 이 크기로 넘긴다); 본문 열이 남은 폭을 채우고(`flex: 1`, 최소 폭 0) 오른쪽 끝에 좋아요(44 터치 영역)·`actions`를 한 행으로, 좋아요 수는 하트 아래 가운데; 답글·펼침은 흐린 글자 버튼(caption, 굵게) | `commentThreadRecipe`, Web·Native `CommentThreadRow` |
115
+ | 간격 | 화면 padding `spacing.md` 16; 댓글 행 사이 `rowGap` 12(최상위·답글 동일); 답글 묶음은 부모 글 열에 맞춰 `replyIndent` 40 들여쓰기; 아바타–본문 `avatarGap` 8; 작성자·본문·답글 줄 사이 `lineGap` 2; 작성자–시각 `spacing.xxs` 4; 좋아요–actions `spacing.xxs` 4 | `commentThreadRecipe` |
116
+ | 순서·정렬 | 최상위 댓글(아바타 → 작성자·시각 → 본문 → 답글 달기) + 행 오른쪽 좋아요 → `actions` → `commentMenu` 세로 점 버튼(끝), 그 아래 좋아요 수 → 답글 펼침 → 펼친 답글 → … → `threadFooter` → composer(footer) | 렌더 순서 |
113
117
  | 고정·스크롤 | 헤더·composer 고정, 댓글은 본문 화면 스크롤(`scroll` 기본 `"screen"`); composer는 `ready`·`empty`에서만 | `ScreenLayout`, `screen-flows.tsx` |
114
118
  | 좁은 폭·큰 글자 | 시각·좋아요 수·답글 버튼 줄과 작성자 줄은 줄바꿈(`flexWrap: "wrap"`); 답글은 한 단계만 들여써 좁은 폭에서도 본문 폭을 지킨다 | `screen-flows.tsx` |
115
119
 
@@ -131,20 +135,30 @@ threadHeader도 댓글과 함께 숨겨지므로 항상 보여야 하는 안내
131
135
 
132
136
  ## 함정
133
137
 
134
- - 펼침 상태는 두 플랫폼 모두 버튼의 expanded 상태로 전한다(2026-10-06 Web `aria-expanded` 추가). 그래도 `repliesLabel(count, expanded)`가
138
+ - 펼침 상태는 두 플랫폼 모두 버튼의 expanded 상태로 전한다. 그래도 `repliesLabel(count, expanded)`가
135
139
  버튼 이름이므로 문구에서도 펼침 여부가 드러나게 만든다.
136
140
 
137
141
  - ChatScreen과 CommentThreadScreen 모두 `ready`·`empty`에서 작성창을 보인다.
138
142
  `loading`·`error`·`restricted`에서는 둘 다 숨는다.
139
143
 
140
- 2026-10-07 사용자 요청으로 댓글의 추가 행동을 본문 아래에서 하트 바로 옆으로 옮겼다.
141
- 기존 위치는 답글 행동과 섞였고 세로로 쌓으면 하트와 더보기의 관계가 떨어져 보였다.
142
- 두 버튼은 같은 가로 행이며 더보기 glyph는 세로 점3개(`MoreVertical`)를 제품이 Menu
143
- trigger로 공급한다. `actions`는 자유 노드 슬롯을 유지하고 메뉴 권한/항목/전송은 제품 소유다.
144
+ 댓글의 추가 행동은 하트 바로 옆에 둔다. 두 버튼은 같은 가로 행이다. 더보기 메뉴는 `commentMenu`로 HJM이 그린다(미게시):
145
+ 행 끝의 세로 점 3개 ghost IconButton(44 터치 영역, glyph `commentThreadRecipe.menuGlyphSize` 20, Web SVG·Native 그린 점으로 같은 모양),
146
+ 지역화 접근성 이름, Web은 버튼에 붙는 Menu 팝오버(`align="end"`), Native는 Menu 시트. `tone: "danger"` 항목(삭제·차단)은
147
+ 계약 `resolveCommentMenuItems`가 항상 끝으로 옮기고, 메뉴는 위험 표시를 색과 함께 글자 단서로도 붙인다. 제품은 항목·권한·콜백만 공급하고
148
+ 버튼·메뉴를 `actions`로 다시 조립하지 않는다. `actions`는 기존 호출 호환을 위해 자유 노드 슬롯으로 남고 하트와 메뉴 버튼 사이에 놓인다.
149
+ 삭제 확인 대화상자는 제품이 `onAction`에서 띄운다(메뉴가 닫힌 뒤 호출되므로 포커스·모달이 겹치지 않는다).
144
150
  순서는 좋아요 → 더보기이며 RTL에서는 논리 방향을 따른다. `likeAction=null`도 그대로 지원한다.
145
151
  이 배치 수정은 1.16.1 변경이다. 실제 설치 버전과 제품 화면 검증은 별도로 확인한다.
146
152
 
147
- 2026-10-12 Spint의 작성자 없는 개발 댓글에서 빈 머리글이 본문을 아래로 밀었다.
148
153
  `author`와 `timeLabel`이 모두 비면 머리글 행을 생략해 avatar와 본문 시작 높이를 맞춘다. Web·Native 동일하다.
149
154
 
150
155
  `CommentThreadRow`는 `item`, `onLike`, `onReply`, `replyLabel`을 받으며 화면 내부도 같은 구현을 사용한다. 가상화 host는 항목 ID·페이지·스크롤을 연결하고 댓글 내부 간격·작성자·본문·시각·반응·메뉴 배치를 복제하지 않는다. `bodyText`가 있으면 작성자와 본문을 이어 쓰고 시각을 하단에, 없으면 작성자와 시각을 머리글에 표시한다.
156
+
157
+ ## 근거
158
+
159
+ - 댓글 행 배치(좋아요 수·답글 달기·답글 들여쓰기): 2026-10-16 사용자 결정 — 공통 기준을 인스타그램 iOS 댓글 시트 실측(@3x 캡처)으로 바꿨다.
160
+ - `threadHeader`·`threadFooter` 슬롯: 2026-10-08 Spint 실제 화면에서 긴 소개를 고정 header에 넣고 높이를 제한하자 스팟 선택이 잘리고 스크롤이 둘로 나뉘었다. 댓글 항목에 소개를 끼우면 댓글 ID·펼침 계약이 오염된다.
161
+ - Web 답글 펼침 `aria-expanded`: 2026-10-06 추가.
162
+ - 추가 행동을 하트 옆에: 2026-10-07 사용자 요청 — 본문 아래 위치는 답글 행동과 섞였고 세로로 쌓으면 하트와 더보기의 관계가 떨어져 보였다.
163
+ - 작성자·시각이 모두 비면 머리글 생략: 2026-10-12 Spint의 작성자 없는 개발 댓글에서 빈 머리글이 본문을 아래로 밀었다.
164
+ - `commentMenu`(HJM 세로 점 더보기): 2026-10-17 사용자 결정. 2026-10-15 규칙(메시지·댓글의 공통 구조는 HJM이 맡고 앱은 데이터·문구·권한만 연결)에 따라, 화면 검토 S4#7에서 제품·예제마다 `actions`로 더보기 버튼·아이콘·메뉴를 직접 조립해 위치와 이름이 갈라진 것을 HJM 공개 prop으로 옮겼다. 위험 항목을 끝에 두는 규칙은 대화 목록 ContextMenu와 같다.
@@ -73,6 +73,7 @@ footer
73
73
  | 항목 | 값 | 근거 |
74
74
  | --- | --- | --- |
75
75
  | 크기 | 부모의 가용 폭·높이 | 별도 고정 높이나 스크롤 없음 |
76
+ | 제목·상태 문구 | 제목은 두 플랫폼 모두 `Heading level3`(문서 단계는 `headingLevel`). 상태 블록은 제목(`Text title`)·설명이 블록 줄로 가운데 정렬된다 | renderer. 2026-10-17 화면 검토: Native 제목이 작은 `Text title`이었고 Web 상태 문구가 한 줄로 이어져 왼쪽 정렬됐다 |
76
77
  | 간격 | 영역 spacing.md 16, 제목 행 spacing.sm 12, 배지 spacing.xs 8, 제목/설명 spacing.xxs 4 | Stack props |
77
78
  | 순서·정렬 | 머리글 → notice → 본문 → footer | renderer |
78
79
  | 고정·스크롤 | 부모 HJM 화면이 소유, 자체 ScrollView 없음 | renderer |
@@ -4,15 +4,17 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-07
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [ContextMenu](../../context-menu.md), [선택 어댑터](../../optional-adapters.md), recipe `menuRecipe`
9
9
  - 스토리북: `배포/컴포넌트/탐색/상황별 메뉴`
10
10
 
11
11
  ## 언제 쓰나
12
12
 
13
13
  Web에서 제품이 소유한 영역(카드·목록 행·캔버스)의 우클릭·길게 누르기·Shift+F10에 명령 목록을
14
- 띄울 때 쓴다. 같은 명령은 화면의 다른 경로(버튼·Menu)에도 있어야 한다. Native canonical 구현은 없고,
15
- OS 길게 누르기 메뉴가 필요하면 선택 어댑터 `NativeContextMenu`를 쓴다.
14
+ 띄울 때 쓴다. 같은 명령은 화면의 다른 경로(버튼·Menu)에도 있어야 한다. Native는 추가 의존성 없는
15
+ canonical `ContextMenu`(`@hjmds/react-native/context-menu`, 미게시, 2026-10-16)가 채팅 길게 누르기 메뉴와 같은
16
+ 모양(배경 어둡게 + 누른 위치 근처 카드, 행 높이 48, 아이콘 24 칸, 위험 항목 빨간 글자)으로 연다. 대화 목록·댓글처럼
17
+ 행을 길게 눌러 여는 동작 목록은 이것을 쓴다. OS 고유 메뉴가 꼭 필요할 때만 선택 어댑터 `NativeContextMenu`를 쓴다.
16
18
 
17
19
  ## 쓰지 않을 때
18
20
 
@@ -22,7 +24,7 @@ OS 길게 누르기 메뉴가 필요하면 선택 어댑터 `NativeContextMenu`
22
24
  | 앱 상단 메뉴 막대(Web) | [Menubar](menubar.md) |
23
25
  | 목록 행을 밀어 나오는 행동 | [SwipeActions](swipe-actions.md) |
24
26
  | 텍스트 선택·링크·이미지 위의 브라우저 기본 메뉴 대체 | 쓰지 않는다([계약](../../context-menu.md)) |
25
- | Native에서 추가 의존성 없이 행동 목록 | [Menu](menu.md), [Sheet](sheet.md) |
27
+ | 버튼으로 여는 Native 행동 목록 | [Menu](menu.md), [Sheet](sheet.md) |
26
28
 
27
29
  ## 공개 이름과 import
28
30
 
@@ -42,8 +44,8 @@ import { ContextMenu } from "@hjmds/react/context-menu";
42
44
  <ContextMenu
43
45
  accessibilityLabel={t("post.actions")}
44
46
  items={[
45
- { id: "share", label: t("post.share"), textValue: t("post.share") },
46
- { id: "delete", label: t("post.delete"), textValue: t("post.delete"), tone: "danger" },
47
+ { id: "share", label: t("post.share"), textValue: t("post.share"), icon: <Share size={20} /> },
48
+ { id: "delete", label: t("post.delete"), textValue: t("post.delete"), tone: "danger", icon: <Trash2 size={20} /> },
47
49
  ]}
48
50
  onAction={(id) => runPostAction(post, id)}
49
51
  >
@@ -51,6 +53,19 @@ import { ContextMenu } from "@hjmds/react/context-menu";
51
53
  </ContextMenu>
52
54
  ```
53
55
 
56
+ ```tsx
57
+ // Native canonical — 호스트 Pressable의 탭·이름은 그대로, 길게 누르기와 접근성 동작만 더한다
58
+ import { ContextMenu } from "@hjmds/react-native/context-menu";
59
+
60
+ <ContextMenu accessibilityLabel={t("chat.rowMenu")} onAction={(id) => runRowAction(room, id)}
61
+ items={[
62
+ { id: "unread", label: t("chat.markUnread"), icon: <MailOpen /> },
63
+ { id: "block", label: t("chat.block"), tone: "danger", icon: <Ban /> },
64
+ ]}>
65
+ <Pressable accessibilityRole="link" accessibilityLabel={summary} onPress={openRoom}><ConversationRow {...row} /></Pressable>
66
+ </ContextMenu>
67
+ ```
68
+
54
69
  ```tsx
55
70
  // Native (선택 어댑터)
56
71
  import { NativeContextMenu } from "@hjmds/react-native/context-menu-native";
@@ -73,8 +88,8 @@ import { Pressable } from "react-native";
73
88
 
74
89
  | prop | 값 | 기본값 | 설명 |
75
90
  | --- | --- | --- | --- |
76
- | Web `items` | `{ id, label, textValue, shortcut?, disabled?, tone?: "default" \| "danger" }[]` | — (필수) | `MenuItemDescriptor`. `description`은 그리지 않는다 |
77
- | Native `items` | `{ id: string, label: string, disabled?, tone?: "default" \| "danger" }[]` | — (필수) | 비거나 id 중복·빈 라벨이면 `TypeError` |
91
+ | Web `items` | `{ id, label, textValue, shortcut?, disabled?, tone?: "default" \| "danger", icon? }[]` | — (필수) | `ContextMenuItem` = `MenuItemDescriptor` + `icon`. `description`은 그리지 않는다. `icon`은 장식(`aria-hidden`)이며 `currentColor`로 항목 색(위험 포함)을 따른다 |
92
+ | Native `items` | `{ id: string, label: string, disabled?, tone?: "default" \| "danger", icon? }[]` | — (필수) | 비거나 id 중복·빈 라벨이면 `TypeError` |
78
93
  | `onAction` | Web `(id: Key) => void` · Native `(id: string) => void` | — (필수) | 고른 항목 id. 메뉴는 닫힌다 |
79
94
  | Web `accessibilityLabel` | `string` | — (필수) | 메뉴 이름 |
80
95
  | Native `onOpenChange` | `(open: boolean) => void` | — | OS 메뉴가 열리고 닫힐 때 |
@@ -86,8 +101,8 @@ import { Pressable } from "react-native";
86
101
  | 항목 | 값 | 근거 |
87
102
  | --- | --- | --- |
88
103
  | 크기 | 메뉴(Web): 최대 폭 `min(24rem, 90vw)`, 최대 높이 뷰포트 높이 − 2×`spacing.md`(16). 항목 최소 높이 `control.minTouchTarget` 44. 감싼 영역 래퍼(`.hjm-context-menu-host`)는 `min-inline-size: 0`만 가지며 크기·여백을 더하지 않는다 | `packages/react/src/styles.css` `.hjm-context-menu*` |
89
- | 간격 | 메뉴 안쪽 여백 `spacing.xs` 8(`menuRecipe.surface.padding`, Menu와 같다. 2026-10-06까지 4, 1.13.0 변경), 항목 여백 `spacing.xs` 8 · `spacing.sm` 12, 라벨–단축키 간격 `spacing.sm` 12 | `packages/react/src/styles.css` `.hjm-context-menu__item` |
90
- | 순서·정렬 | 항목은 `items` 배열 순서대로 위에서 아래로 그린다. 단축키는 끝에 붙고 줄바꿈하지 않는다. 우클릭·길게 누르기는 누른 좌표, Shift+F10·메뉴 키는 영역의 시작 쪽 아래 모서리에 메뉴를 연다 | `packages/react/src/context-menu.tsx`, `src/context-menu.ts` |
104
+ | 간격 | 메뉴 안쪽 여백 `spacing.xs` 8(`menuRecipe.surface.padding`, Menu와 같다. 2026-10-06까지 4, 1.13.0 변경), 항목 여백 `spacing.xs` 8 · `spacing.sm` 12, 아이콘 칸 24 · 아이콘–라벨, 라벨–단축키 간격 `spacing.sm` 12(Native canonical과 같다) | `packages/react/src/styles.css` `.hjm-context-menu__item` |
105
+ | 순서·정렬 | 항목은 `items` 배열 순서대로 위에서 아래로 그린다. 아이콘은 라벨 앞, 라벨이 남은 폭을 차지하고 단축키는 끝에 붙고 줄바꿈하지 않는다. 우클릭·길게 누르기는 누른 좌표, Shift+F10·메뉴 키는 영역의 시작 쪽 아래 모서리에 메뉴를 연다 | `packages/react/src/context-menu.tsx`, `src/context-menu.ts` |
91
106
  | 고정·스크롤 | `position: fixed`로 뜨고 뷰포트 밖으로 넘치면 안쪽으로 밀어 넣는다. 최대 높이를 넘으면 메뉴 안에서 스크롤한다 | `packages/react/src/context-menu.tsx` |
92
107
  | 좁은 폭·큰 글자 | 좁은 폭에서는 최대 폭이 `90vw`로 줄고 항목 라벨은 줄바꿈한다. Native `NativeContextMenu`의 위치·크기·미리보기는 OS가 정한다 | `packages/react/src/styles.css` |
93
108
 
@@ -135,8 +150,10 @@ import { Pressable } from "react-native";
135
150
  | 열림 알림 | 없음 | `onOpenChange(open)` |
136
151
  | 성숙도 | stable | 실험적 어댑터(기기 증거 전까지 canonical unsupported) |
137
152
 
138
- 2026-10-07 Web 키보드 회귀에서 팝업이 정지한 커서 아래에 배치된 뒤 mouseenter가 활성 항목을
139
- 첫 항목으로 되돌리는 문제를 재현했다. 1.15.0 변경 수정은 실제 마우스 이동으로 항목을
140
- 바꾸며, 늦은 경계 이벤트만으로 키보드 선택을 덮지 않는다. 클릭·키보드 실행·포커스 복귀와
141
- Native OS 어댑터는 기존 계약을 유지한다. 고정 대기 시간을 추가하지 않고 입력 의도를 구분했다.
142
- [검증 기록](../../../../../docs/qa/2026-10-07-command-records.md).
153
+ Web 활성 항목은 실제 마우스 이동으로만 바꾸며(1.15.0 변경), 늦은 경계 이벤트만으로 키보드 선택을 덮지 않는다. 클릭·키보드 실행·포커스 복귀와
154
+ Native OS 어댑터는 기존 계약을 유지한다. 고정 대기 시간이 아니라 입력 의도로 구분한다.
155
+
156
+ ## 근거
157
+
158
+ - 마우스 이동으로만 활성 항목 변경: 2026-10-07 Web 키보드 회귀에서 팝업이 정지한 커서 아래에 배치된 뒤 mouseenter가 활성 항목을 첫 항목으로 되돌리는 문제를 재현했다([검증 기록](../../../../../docs/qa/2026-10-07-command-records.md)).
159
+ - Web 항목 `icon`: 2026-10-17 화면 검토(S5)에서 같은 대화 목록 메뉴가 Native에서는 아이콘 24 칸과 함께, Web에서는 글자만으로 그려지는 차이를 확인해 Web ContextMenu에 같은 칸을 추가했다.
@@ -4,14 +4,14 @@
4
4
  - 상태: 실험
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.24.0
7
- - 검토일: 2026-10-15
7
+ - 검토일: 2026-10-17
8
8
  - 근거: `src/conversation-row.ts`의 `conversationRowRecipe`
9
- - 스토리북: 별도 예제 미등록
9
+ - 스토리북: `실험/화면/소통/대화 목록`
10
10
 
11
11
  ## 언제 쓰나
12
12
 
13
- 대화 목록에서 상대, 마지막 메시지, 상대 시각, 안 읽음 점을 표시한다. 2026-10-15 전사 적용 중
14
- 제품마다 복제한 DM 행의 배치가 달라져 공개 구성으로 분리했다. 일반 설정 행은 `ListRow`를 쓴다.
13
+ 대화 목록에서 상대, 마지막 메시지, 상대 시각, 안 읽음 점을 표시한다. 제품에서 DM 행을 복제하지 않고
14
+ 이 공개 구성을 쓴다. 일반 설정 행은 `ListRow`를 쓴다.
15
15
  이 구성은 큰 아바타와 한 줄 미리보기·시각, 읽지 않은 대화 강조를 보존한다.
16
16
 
17
17
  ## 쓰지 않을 때
@@ -71,6 +71,8 @@ import { ConversationRow } from "@hjmds/react-native/conversation-row";
71
71
 
72
72
  메시지가 길어도 시각과 읽지 않음 점은 유지하고 미리보기만 말줄임한다. 앱은 별도 행 padding을 넣지 않는다.
73
73
 
74
+ 길게 눌러 여는 행 메뉴(읽지 않음 표시·알림 해제·차단·삭제 등)는 행 호스트를 [ContextMenu](context-menu.md)로 감싼다. 위험 항목은 `tone: "danger"`이며 목록 끝에 둔다. 예제: `실험/화면/소통/대화 목록`.
75
+
74
76
  ## 꼭 지킬 것
75
77
 
76
78
  전체 행의 현지화 접근성 이름과 라우팅 host를 함께 제공한다. HJM 내부 색·간격을 제품 CSS로 덮지 않는다.
@@ -91,3 +93,8 @@ host는 상대·미리보기·시각·읽지 않은 수가 포함된 현지화
91
93
  ## 관련 지침
92
94
 
93
95
  [선택 기준](../SELECTION.md), [ListRow](list-row.md), [ChatScreen](chat-screen.md).
96
+
97
+ ## 근거
98
+
99
+ - 공개 구성으로 분리: 2026-10-15 전사 적용 중 제품마다 복제한 DM 행의 배치가 달라졌다.
100
+ - 행 메뉴의 위험 항목 위치: 2026-10-16 인스타그램 DM 실측.
@@ -0,0 +1,85 @@
1
+ # ConversionFunnel
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: `src/data-workflows.ts`의 `resolveFunnel`, 양 renderer `src/data-workflows.tsx`, [데이터 해석과 가져오기 구성](../compositions/data-workflows.md)
9
+ - 스토리북: `배포/구성/정보 표시/전환 퍼널`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 순차 단계(열기 → 작성 → 저장 등)의 인원을 가로 막대로 보여 주고, 단계를 골라 전체 대비·직전 대비 전환율과
14
+ 이탈 수를 문구로 읽을 때 쓴다. 단계 수는 0 이상 정수이고 뒤 단계가 앞 단계보다 크면 오류다.
15
+ 비율 계산은 `resolveFunnel`이 하고 문구 형식은 제품이 정한다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 순서가 없는 범주 비교 | [Chart](chart.md)의 `BarChart` |
22
+ | 가입 시점별 기간 잔존 | [CohortRetentionTable](cohort-retention-table.md) |
23
+ | 목표 대비 하나의 수치 | `TargetSummary`([데이터 해석과 가져오기](../compositions/data-workflows.md)) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `ConversionFunnel` | 순차 퍼널 막대·단계 선택·상세 문구 | `@hjmds/react/data-workflows` | `@hjmds/react-native/data-workflows` |
30
+
31
+ 단계 타입 `FunnelStage`와 계산 helper `resolveFunnel`은 `@hjmds/design-contracts/data-workflows`에서 import한다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { ConversionFunnel } from "@hjmds/react/data-workflows";
38
+
39
+ <ConversionFunnel label={t("funnel.title")} stages={stages} formatCount={formatNumber}
40
+ missingLabel={t("chart.missing")}
41
+ detailLabel={stage => t("funnel.detail", { label: stage.label, count: stage.count, rate: stage.conversion })} />
42
+ ```
43
+
44
+ ```tsx
45
+ // Native
46
+ import { ConversionFunnel } from "@hjmds/react-native/data-workflows";
47
+
48
+ <ConversionFunnel label={t("funnel.title")} stages={stages} formatCount={formatNumber}
49
+ missingLabel={t("chart.missing")}
50
+ detailLabel={stage => t("funnel.detail", { label: stage.label, count: stage.count, rate: stage.conversion })} />
51
+ ```
52
+
53
+ ## 축과 기본값
54
+
55
+ | prop | 값 | 기본값 | 설명 |
56
+ | --- | --- | --- | --- |
57
+ | label | string | 필수 | 제목 문구, 차트 접근성 이름, 막대 계열 이름 |
58
+ | stages | `readonly FunnelStage[]`(`id`·`label`·`count`·`reason?`) | 필수 | 순서대로. id 중복·증가하는 수는 `RangeError`/`TypeError` |
59
+ | formatCount | `(count) => string` | 필수 | 막대 값 문구 |
60
+ | missingLabel | string | 필수 | 결측 값 문구, 단계가 없을 때 빈 상태 문구 |
61
+ | detailLabel | `(stage) => string` | 필수 | 선택 단계 상세. `conversion`·`previousConversion`·`lost`는 분모가 0이거나 첫 단계면 `null` |
62
+
63
+ 선택 단계는 내부 상태이며 처음에는 첫 단계다. 외부 controlled prop은 없다.
64
+
65
+ ## 배치
66
+
67
+ | 항목 | 값 | 근거 |
68
+ | --- | --- | --- |
69
+ | 크기 | 가로 막대 차트(`orientation="horizontal"`), 높이는 BarChart 기본 `chartRecipe.heights.regular` 180 | 양 renderer, [Chart](chart.md) |
70
+ | 간격 | 제목·차트·단계 버튼 행·상세 문구 사이 `spacing.md` 16, 단계 버튼 사이 `spacing.sm` 12 | `Stack gap="md"`, `Stack axis="inline" gap="sm"` |
71
+ | 순서·정렬 | 제목(strong) → 막대 차트 → 단계 버튼(ghost, 선택 표시) → 상세 문구 | 양 renderer |
72
+ | 고정·스크롤 | 자체 스크롤 없음 | — |
73
+ | 좁은 폭·큰 글자 | 단계 버튼 행은 줄바꿈(`wrap`), 차트는 가로만 줄어든다 | `Stack wrap`, [Chart](chart.md) |
74
+
75
+ ## 꼭 지킬 것
76
+
77
+ - 단계 수는 실제 관측값을 넣는다. 감소하지 않는 데이터를 억지로 정렬·보정하지 않는다(계산 helper가 오류를 낸다).
78
+ - 비율·인원 문구는 `detailLabel`·`formatCount`에서 제품 i18n으로 만든다. 0 분모를 0%나 100%로 만들지 않는다.
79
+ - 이탈 사유(`reason`)는 수집된 응답일 때만 넣는다.
80
+
81
+ ## 상태와 상호작용
82
+
83
+ 막대를 누르거나 끌어 포커스하면(`onFocusChange`) 또는 단계 버튼을 누르면 선택 단계가 바뀐다. 상세 문구는
84
+ Web `role="status"`, Native `accessibilityLiveRegion="polite"`로 알린다. 단계가 없으면 차트 대신 `missingLabel`을
85
+ 가운데 정렬로 표시한다.
@@ -0,0 +1,80 @@
1
+ # CorrelationMatrix
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: `src/data-workflows.ts`의 `validateCorrelation`, 양 renderer `src/data-workflows.tsx`, [데이터 해석과 가져오기 구성](../compositions/data-workflows.md)
9
+ - 스토리북: `배포/구성/정보 표시/상관 행렬`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 여러 지표 사이의 상관계수(-1~1)를 정사각 대칭 표와 양·음 두 방향 heat 색으로 보여 줄 때 쓴다.
14
+ 계산은 제품이 하고 HJM은 정사각·대칭·대각선 1·범위를 검증해 표시한다. 관측하지 못한 쌍은 `null`이다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 기간별 잔존 비율 | [CohortRetentionTable](cohort-retention-table.md) |
21
+ | 두 변수 산포 | [Chart](chart.md)의 산점도 |
22
+ | 대각선을 비우는 대전표 | `MatrixTable`([DataTable](data-table.md)) |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `CorrelationMatrix` | 대칭 상관 행렬 heat 표 | `@hjmds/react/data-workflows` | `@hjmds/react-native/data-workflows` |
29
+
30
+ 데이터 타입 `CorrelationData`와 검증 helper `validateCorrelation`은 `@hjmds/design-contracts/data-workflows`에서 import한다.
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { CorrelationMatrix } from "@hjmds/react/data-workflows";
37
+
38
+ <CorrelationMatrix label={t("correlation.title")} variableColumnLabel={t("correlation.variable")}
39
+ data={{ variables, values }} formatValue={v => v.toFixed(2)} missingLabel={t("correlation.unobserved")} />
40
+ ```
41
+
42
+ ```tsx
43
+ // Native
44
+ import { CorrelationMatrix } from "@hjmds/react-native/data-workflows";
45
+
46
+ <CorrelationMatrix label={t("correlation.title")} variableColumnLabel={t("correlation.variable")}
47
+ data={{ variables, values }} formatValue={v => v.toFixed(2)} missingLabel={t("correlation.unobserved")} />
48
+ ```
49
+
50
+ ## 축과 기본값
51
+
52
+ | prop | 값 | 기본값 | 설명 |
53
+ | --- | --- | --- | --- |
54
+ | label | string | 필수 | 표 접근성 이름(화면에 제목을 그리지 않는다) |
55
+ | variableColumnLabel | string | 필수 | 첫 열 머리글 |
56
+ | data.variables | `readonly WorkflowField[]` | 필수 | 행·열 머리글. id 중복이면 오류 |
57
+ | data.values | `(number \| null)[][]` | 필수 | 변수 수와 같은 정사각, 대칭(오차 1e-10), 대각선은 1 또는 `null`, 값은 -1~1 |
58
+ | formatValue | `(value) => string` | 필수 | 관측 칸 문구 |
59
+ | missingLabel | string | 필수 | `null` 칸 문구 |
60
+
61
+ ## 배치
62
+
63
+ | 항목 | 값 | 근거 |
64
+ | --- | --- | --- |
65
+ | 크기 | 본문 폭을 채우는 DataTable. 열 = 변수 이름 + 변수 수 | [DataTable](data-table.md) |
66
+ | 간격 | DataTable 셀 여백(`regular` 세로 12·가로 16) | `dataTableRecipe.density` |
67
+ | 순서·정렬 | 변수 이름 열(`pinned: "start"`) → `variables` 순서의 열. 행도 같은 순서 | 양 renderer |
68
+ | 고정·스크롤 | 이름 열을 시작 쪽에 고정. Native는 나머지 열을 가로 `ScrollView`에 넣는다 | [DataTable](data-table.md) |
69
+ | 좁은 폭·큰 글자 | 열을 빼지 않고 이름 열 고정 + 가로 스크롤로 보존한다 | [DataTable](data-table.md) |
70
+
71
+ ## 꼭 지킬 것
72
+
73
+ - 양수는 high(dataviz 계열 0번), 음수는 low(계열 1번) 색이며 |값| 0.35·0.7 경계로 세 단계다. 0과 `null`은 색을 칠하지 않는다.
74
+ - 비대칭·범위 밖 값은 렌더 시 `RangeError`가 난다. 표시를 위해 값을 잘라 넣지 말고 계산을 고친다.
75
+ - 정렬·행 선택이 없는 읽기 전용 표다. 변수 순서는 제품이 정한다.
76
+
77
+ ## 상태와 상호작용
78
+
79
+ 읽기 전용이다. heat 색은 현재 테마의 dataviz 색을 따른다. 색만으로 방향을 전하지 않도록 `formatValue`가
80
+ 부호를 포함한 값을 보여 줘야 한다.
@@ -14,6 +14,10 @@
14
14
  DataTable은 다음 정렬 상태와 선택 상태만 판정하고 행을 재배열하지 않는다. 로컬 배열 정렬은 계약의 선택형 helper
15
15
  `sortDataTableRows`로, 서버 정렬·필터·페이지 나누기는 제품이 한다. 선택·정렬이 없는 단순 Web 표는 companion `Table`을 쓴다.
16
16
  Native renderer(기록 표: 고정 열 + 가로 스크롤)는 2026-10-14 추가분이며 미게시(1.20.0 이후)다.
17
+ Native의 열 `width`는 최소 너비다. 제목(정렬 화살표 포함)과 문자열·숫자 셀을 실제 폰트로 측정해
18
+ 전체 내용이 보이는 너비를 확보하며, 화면보다 넓으면 고정 열 옆으로 가로 스크롤한다.
19
+ 2026-10-17 오늘야구에서 긴 지표명과 통산 이닝이 말줄임된 문제를 수정한 동작이며 미게시다.
20
+ 제품이 JSX로 그리는 셀은 해당 내용에 필요한 최소 `width`를 공급한다.
17
21
 
18
22
  ## 쓰지 않을 때
19
23
 
@@ -63,6 +63,7 @@ import { DescriptionList } from "@hjmds/react-native/data-display";
63
63
  | prop | 값 | 기본값 | 설명 |
64
64
  | --- | --- | --- | --- |
65
65
  | `columns` | `1` · `2` | `2` | 요청값일 뿐이며 resolver가 폭·`textScale`을 보고 1열로 접을 수 있다 |
66
+ | `layout` | `stacked` · `inline` | `stacked` | Native는 descriptor, Web은 prop. `inline`은 1열 목록, 라벨 24%, 나머지는 줄바꿈 가능한 값, 행 구분선 |
66
67
  | 항목 | `{ id, label: string, value: string }` | — (필수) | 셋 다 빈 문자열이면 안 된다. 항목이 없거나 `id`가 겹치면 예외가 난다 |
67
68
  | Native `label` | `string` | — (필수) | 목록 전체의 접근성 이름 |
68
69
  | Native `availableWidth` | 양수 | 측정값 | 주면 측정보다 우선 |
@@ -76,7 +77,7 @@ import { DescriptionList } from "@hjmds/react-native/data-display";
76
77
  | --- | --- | --- |
77
78
  | 크기 | 항목 최소 폭 160 × `textScale` | `src/description-list.ts`(`descriptionListRecipe.group.minItemWidth`) |
78
79
  | 간격 | 항목 사이 가로·세로 `spacing.sm` 12. 각 항목의 라벨–값 `spacing.xxs` 4 | `src/description-list.ts`, `packages/react/src/styles.css` `.hjm-description-list*`, `packages/react-native/src/data-display.tsx` |
79
- | 순서·정렬 | [Card](card.md)·[Section](section.md) 본문 안에 둔다. 각 항목은 라벨 위·값 아래. 항목은 `items` 순서대로 행 우선으로 채운다(왼쪽 → 오른쪽, 위 → 아래) | 같은 파일 |
80
+ | 순서·정렬 | [Card](card.md)·[Section](section.md) 본문 안에 둔다. 기본은 라벨 위·값 아래. `inline`은 라벨 왼쪽·값 오른쪽이며 행 세로 여백은 `spacing.sm`이다. 항목은 `items` 순서대로 행 우선으로 채운다(왼쪽 → 오른쪽, 위 → 아래) | 같은 파일 |
80
81
  | 고정·스크롤 | — | — |
81
82
  | 좁은 폭·큰 글자 | 폭이 `2 × 160 × textScale + 12` 미만이면 1열로 접는다(배율 1이면 332, 배율 1.6이면 524). 값이 길면 줄바꿈한다(Web `overflow-wrap: anywhere`) | `src/description-list.ts`(열 판정 함수), `packages/react/src/styles.css` |
82
83