@hjmds/design-contracts 1.12.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/dist/avatar-fallback.d.ts +11 -0
  2. package/dist/avatar-fallback.d.ts.map +1 -1
  3. package/dist/avatar-fallback.js +21 -0
  4. package/dist/avatar-fallback.js.map +1 -1
  5. package/dist/base-recipes.d.ts +17 -0
  6. package/dist/base-recipes.d.ts.map +1 -1
  7. package/dist/base-recipes.js +17 -0
  8. package/dist/base-recipes.js.map +1 -1
  9. package/dist/catalog.d.ts +25 -0
  10. package/dist/catalog.d.ts.map +1 -1
  11. package/dist/command-palette.d.ts +14 -9
  12. package/dist/command-palette.d.ts.map +1 -1
  13. package/dist/command-palette.js +8 -9
  14. package/dist/command-palette.js.map +1 -1
  15. package/dist/component-recipes.d.ts +17 -0
  16. package/dist/component-recipes.d.ts.map +1 -1
  17. package/dist/component-recipes.js +5 -0
  18. package/dist/component-recipes.js.map +1 -1
  19. package/dist/provider-button.d.ts.map +1 -1
  20. package/dist/provider-button.js +3 -0
  21. package/dist/provider-button.js.map +1 -1
  22. package/dist/reactions.d.ts +10 -0
  23. package/dist/reactions.d.ts.map +1 -1
  24. package/dist/reactions.js +7 -0
  25. package/dist/reactions.js.map +1 -1
  26. package/dist/screen-patterns.d.ts +147 -0
  27. package/dist/screen-patterns.d.ts.map +1 -0
  28. package/dist/screen-patterns.js +149 -0
  29. package/dist/screen-patterns.js.map +1 -0
  30. package/dist/slider.d.ts +8 -0
  31. package/dist/slider.d.ts.map +1 -1
  32. package/dist/slider.js +6 -1
  33. package/dist/slider.js.map +1 -1
  34. package/dist/upload-item.d.ts +5 -0
  35. package/dist/upload-item.d.ts.map +1 -1
  36. package/dist/upload-item.js +5 -0
  37. package/dist/upload-item.js.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/dist/version.js.map +1 -1
  41. package/docs/action-session.md +3 -3
  42. package/docs/agreement.md +5 -0
  43. package/docs/avatar-fallback.md +7 -0
  44. package/docs/bottom-navigation.md +6 -0
  45. package/docs/brand-boundary.md +1 -1
  46. package/docs/button-label.md +6 -0
  47. package/docs/clipboard.md +3 -0
  48. package/docs/command-palette.md +45 -2
  49. package/docs/consumer-policy.md +5 -1
  50. package/docs/data-table.md +6 -4
  51. package/docs/dialog.md +8 -2
  52. package/docs/form.md +51 -0
  53. package/docs/generated/component-maturity.md +1 -1
  54. package/docs/generated/renderer-evidence.json +3 -3
  55. package/docs/generated/renderer-evidence.md +1 -1
  56. package/docs/generated/showcase-manifest.json +1 -1
  57. package/docs/link.md +8 -0
  58. package/docs/migration-native-legacy-removal.md +45 -1
  59. package/docs/optional-adapters.md +1 -1
  60. package/docs/password-field.md +5 -0
  61. package/docs/product-composition-adoption.md +40 -0
  62. package/docs/progress.md +19 -1
  63. package/docs/provider-button.md +13 -0
  64. package/docs/result.md +3 -0
  65. package/docs/screen-chrome.md +10 -0
  66. package/docs/screen-patterns.md +376 -0
  67. package/docs/sheet.md +12 -0
  68. package/docs/splitter.md +8 -2
  69. package/docs/theming.md +36 -29
  70. package/docs/toggle-group.md +7 -0
  71. package/docs/tour.md +7 -1
  72. package/docs/tree.md +5 -2
  73. package/docs/upload-item.md +7 -0
  74. package/docs/usage/README.md +236 -0
  75. package/docs/usage/STANDARD.md +108 -0
  76. package/docs/usage/components/accordion.md +107 -0
  77. package/docs/usage/components/activity-heatmap.md +104 -0
  78. package/docs/usage/components/affix.md +86 -0
  79. package/docs/usage/components/agreement.md +129 -0
  80. package/docs/usage/components/alert-dialog.md +130 -0
  81. package/docs/usage/components/anchor.md +96 -0
  82. package/docs/usage/components/aspect-ratio.md +89 -0
  83. package/docs/usage/components/asset.md +126 -0
  84. package/docs/usage/components/auth-provider-button.md +116 -0
  85. package/docs/usage/components/auth-screen-layout.md +129 -0
  86. package/docs/usage/components/avatar.md +114 -0
  87. package/docs/usage/components/badge.md +84 -0
  88. package/docs/usage/components/bottom-cta.md +125 -0
  89. package/docs/usage/components/bottom-info.md +99 -0
  90. package/docs/usage/components/bottom-navigation.md +136 -0
  91. package/docs/usage/components/breadcrumb.md +81 -0
  92. package/docs/usage/components/button.md +118 -0
  93. package/docs/usage/components/calendar.md +122 -0
  94. package/docs/usage/components/card.md +110 -0
  95. package/docs/usage/components/carousel.md +113 -0
  96. package/docs/usage/components/celebration.md +96 -0
  97. package/docs/usage/components/chat-message.md +122 -0
  98. package/docs/usage/components/chat-screen.md +112 -0
  99. package/docs/usage/components/checkbox-group.md +104 -0
  100. package/docs/usage/components/checkbox.md +103 -0
  101. package/docs/usage/components/chip.md +104 -0
  102. package/docs/usage/components/code-block.md +111 -0
  103. package/docs/usage/components/collapsible.md +112 -0
  104. package/docs/usage/components/color-picker.md +86 -0
  105. package/docs/usage/components/combobox.md +137 -0
  106. package/docs/usage/components/command-palette.md +125 -0
  107. package/docs/usage/components/comment-thread-screen.md +125 -0
  108. package/docs/usage/components/container.md +98 -0
  109. package/docs/usage/components/content-transition.md +101 -0
  110. package/docs/usage/components/context-menu.md +136 -0
  111. package/docs/usage/components/counter-badge.md +107 -0
  112. package/docs/usage/components/data-table.md +122 -0
  113. package/docs/usage/components/date-picker.md +142 -0
  114. package/docs/usage/components/date-range-picker.md +111 -0
  115. package/docs/usage/components/description-list.md +103 -0
  116. package/docs/usage/components/design-system-provider.md +124 -0
  117. package/docs/usage/components/dialog.md +176 -0
  118. package/docs/usage/components/divider.md +89 -0
  119. package/docs/usage/components/editor-screen.md +126 -0
  120. package/docs/usage/components/effect-surface.md +120 -0
  121. package/docs/usage/components/empty-state.md +114 -0
  122. package/docs/usage/components/field.md +129 -0
  123. package/docs/usage/components/file-picker.md +114 -0
  124. package/docs/usage/components/floating-action-button.md +138 -0
  125. package/docs/usage/components/form.md +162 -0
  126. package/docs/usage/components/grid.md +99 -0
  127. package/docs/usage/components/heading.md +87 -0
  128. package/docs/usage/components/icon-button.md +126 -0
  129. package/docs/usage/components/icon.md +105 -0
  130. package/docs/usage/components/image.md +122 -0
  131. package/docs/usage/components/keyboard-avoiding.md +93 -0
  132. package/docs/usage/components/keyboard-dock.md +110 -0
  133. package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
  134. package/docs/usage/components/keyboard-motion-provider.md +86 -0
  135. package/docs/usage/components/layout.md +117 -0
  136. package/docs/usage/components/link.md +121 -0
  137. package/docs/usage/components/list-detail-screen.md +103 -0
  138. package/docs/usage/components/list-row.md +124 -0
  139. package/docs/usage/components/list.md +119 -0
  140. package/docs/usage/components/load-more.md +115 -0
  141. package/docs/usage/components/masonry.md +109 -0
  142. package/docs/usage/components/media-selection-screen.md +119 -0
  143. package/docs/usage/components/mentions.md +119 -0
  144. package/docs/usage/components/menu.md +129 -0
  145. package/docs/usage/components/menubar.md +93 -0
  146. package/docs/usage/components/message-composer.md +124 -0
  147. package/docs/usage/components/moderation-screen.md +113 -0
  148. package/docs/usage/components/notice.md +106 -0
  149. package/docs/usage/components/notification-inbox-screen.md +97 -0
  150. package/docs/usage/components/notification-item.md +98 -0
  151. package/docs/usage/components/number-field.md +131 -0
  152. package/docs/usage/components/onboarding-screen.md +106 -0
  153. package/docs/usage/components/otp-field.md +101 -0
  154. package/docs/usage/components/pagination.md +82 -0
  155. package/docs/usage/components/password-field.md +137 -0
  156. package/docs/usage/components/permission-screen.md +107 -0
  157. package/docs/usage/components/photo-source-sheet.md +119 -0
  158. package/docs/usage/components/popover.md +108 -0
  159. package/docs/usage/components/profile-screen.md +89 -0
  160. package/docs/usage/components/progress.md +122 -0
  161. package/docs/usage/components/qr-code.md +122 -0
  162. package/docs/usage/components/radio-group.md +124 -0
  163. package/docs/usage/components/radio.md +104 -0
  164. package/docs/usage/components/result.md +116 -0
  165. package/docs/usage/components/saved-items-screen.md +126 -0
  166. package/docs/usage/components/screen-layout.md +119 -0
  167. package/docs/usage/components/search-field.md +120 -0
  168. package/docs/usage/components/search-screen.md +215 -0
  169. package/docs/usage/components/section.md +111 -0
  170. package/docs/usage/components/segmented-control.md +138 -0
  171. package/docs/usage/components/select.md +142 -0
  172. package/docs/usage/components/settings-screen.md +126 -0
  173. package/docs/usage/components/shared-transition-element.md +111 -0
  174. package/docs/usage/components/shared-transition-screen.md +86 -0
  175. package/docs/usage/components/sheet.md +151 -0
  176. package/docs/usage/components/side-panel.md +104 -0
  177. package/docs/usage/components/sidebar.md +107 -0
  178. package/docs/usage/components/skeleton.md +105 -0
  179. package/docs/usage/components/skip-nav.md +76 -0
  180. package/docs/usage/components/slider.md +121 -0
  181. package/docs/usage/components/sortable-collection.md +127 -0
  182. package/docs/usage/components/spinner.md +86 -0
  183. package/docs/usage/components/splitter.md +103 -0
  184. package/docs/usage/components/stack.md +93 -0
  185. package/docs/usage/components/statistic.md +123 -0
  186. package/docs/usage/components/steps.md +110 -0
  187. package/docs/usage/components/surface.md +91 -0
  188. package/docs/usage/components/swipe-actions.md +124 -0
  189. package/docs/usage/components/switch.md +120 -0
  190. package/docs/usage/components/tabs.md +134 -0
  191. package/docs/usage/components/tag.md +84 -0
  192. package/docs/usage/components/tags-input.md +111 -0
  193. package/docs/usage/components/text-area.md +112 -0
  194. package/docs/usage/components/text-format.md +75 -0
  195. package/docs/usage/components/text-transition.md +104 -0
  196. package/docs/usage/components/text.md +101 -0
  197. package/docs/usage/components/thinking-orb.md +105 -0
  198. package/docs/usage/components/timeline.md +105 -0
  199. package/docs/usage/components/toast.md +145 -0
  200. package/docs/usage/components/toggle-group.md +95 -0
  201. package/docs/usage/components/tooltip.md +103 -0
  202. package/docs/usage/components/top-bar.md +124 -0
  203. package/docs/usage/components/top.md +89 -0
  204. package/docs/usage/components/tour.md +118 -0
  205. package/docs/usage/components/transfer-list.md +115 -0
  206. package/docs/usage/components/tree.md +91 -0
  207. package/docs/usage/components/upload-item.md +99 -0
  208. package/docs/usage/components/virtual-list.md +105 -0
  209. package/docs/usage/components/visually-hidden.md +72 -0
  210. package/docs/usage/components/watermark.md +78 -0
  211. package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
  212. package/docs/usage/compositions/action-recovery-save.md +235 -0
  213. package/docs/usage/compositions/action-recovery-undo.md +193 -0
  214. package/docs/usage/compositions/common-message.md +132 -0
  215. package/docs/usage/compositions/common-notification.md +101 -0
  216. package/docs/usage/compositions/compound-controls.md +186 -0
  217. package/docs/usage/compositions/data-layouts.md +157 -0
  218. package/docs/usage/compositions/disclosure.md +144 -0
  219. package/docs/usage/compositions/environment-matrix.md +139 -0
  220. package/docs/usage/compositions/expo-interactions.md +149 -0
  221. package/docs/usage/compositions/family-drawer.md +201 -0
  222. package/docs/usage/compositions/floating-action-button.md +197 -0
  223. package/docs/usage/compositions/input-sheet.md +148 -0
  224. package/docs/usage/compositions/interaction-adapters.md +190 -0
  225. package/docs/usage/compositions/interaction-flow-apply.md +205 -0
  226. package/docs/usage/compositions/interaction-flow-draft.md +188 -0
  227. package/docs/usage/compositions/interaction-flow-search.md +171 -0
  228. package/docs/usage/compositions/native-renderers.md +106 -0
  229. package/docs/usage/compositions/navigation-bar-collection.md +164 -0
  230. package/docs/usage/compositions/optional-adapters.md +169 -0
  231. package/docs/usage/compositions/optional-motion.md +109 -0
  232. package/docs/usage/compositions/photo-source.md +104 -0
  233. package/docs/usage/compositions/purpose-input-comment.md +110 -0
  234. package/docs/usage/compositions/purpose-input-message.md +119 -0
  235. package/docs/usage/compositions/reference-first.md +96 -0
  236. package/docs/usage/compositions/reference-review.md +107 -0
  237. package/docs/usage/compositions/reference-settings.md +107 -0
  238. package/docs/usage/compositions/selection-scope.md +174 -0
  239. package/docs/usage/compositions/stea-event-ticket.md +166 -0
  240. package/docs/usage/compositions/stea-flip-card.md +162 -0
  241. package/docs/usage/compositions/stea-order-progress.md +184 -0
  242. package/docs/usage/compositions/stea-otp-verify.md +215 -0
  243. package/docs/usage/compositions/stea-pixel-empty.md +140 -0
  244. package/docs/usage/compositions/stea-schedule-card.md +169 -0
  245. package/docs/usage/compositions/stea-stat-summary.md +154 -0
  246. package/docs/usage/compositions/time-selection.md +174 -0
  247. package/docs/usage/compositions/toast-layout.md +128 -0
  248. package/docs/usage/compositions/visual-foundations.md +185 -0
  249. package/docs/usage/compositions/web-additions.md +146 -0
  250. package/docs/usage/compositions/web-navigation.md +143 -0
  251. package/docs/usage/screens/common-chat.md +127 -0
  252. package/docs/usage/screens/common-comments.md +108 -0
  253. package/docs/usage/screens/common-inbox.md +110 -0
  254. package/docs/usage/screens/common-login.md +98 -0
  255. package/docs/usage/screens/common-profile.md +221 -0
  256. package/docs/usage/screens/common-saved.md +127 -0
  257. package/docs/usage/screens/common-search.md +274 -0
  258. package/docs/usage/screens/common-settings.md +126 -0
  259. package/docs/usage/screens/common-shell.md +108 -0
  260. package/docs/usage/screens/dashboard.md +245 -0
  261. package/docs/usage/screens/discovery-gallery.md +306 -0
  262. package/docs/usage/screens/flow-collection.md +96 -0
  263. package/docs/usage/screens/flow-editor.md +120 -0
  264. package/docs/usage/screens/flow-media.md +111 -0
  265. package/docs/usage/screens/flow-moderation.md +120 -0
  266. package/docs/usage/screens/flow-onboarding.md +193 -0
  267. package/docs/usage/screens/flow-permission.md +103 -0
  268. package/docs/usage/screens/landing.md +347 -0
  269. package/docs/usage/screens/mockup-studio.md +190 -0
  270. package/docs/usage/screens/notification-settings.md +206 -0
  271. package/docs/usage/screens/reference-comparison.md +159 -0
  272. package/docs/usage/templates/component.md +61 -0
  273. package/docs/usage/templates/composition.md +47 -0
  274. package/docs/usage/templates/screen.md +56 -0
  275. package/docs/usage/templates/token.md +32 -0
  276. package/docs/usage/tokens/color.md +142 -0
  277. package/docs/usage/tokens/elevation-opacity.md +86 -0
  278. package/docs/usage/tokens/layers.md +98 -0
  279. package/docs/usage/tokens/layout.md +114 -0
  280. package/docs/usage/tokens/motion.md +88 -0
  281. package/docs/usage/tokens/radius.md +53 -0
  282. package/docs/usage/tokens/size.md +74 -0
  283. package/docs/usage/tokens/spacing.md +73 -0
  284. package/docs/usage/tokens/stroke.md +50 -0
  285. package/docs/usage/tokens/theme-studio.md +70 -0
  286. package/docs/usage/tokens/typography-studio.md +70 -0
  287. package/docs/usage/tokens/typography.md +89 -0
  288. package/package.json +7 -1
@@ -0,0 +1,122 @@
1
+ # Calendar
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Calendar](../../calendar.md), recipe `calendarRecipe`(`src/calendar.ts`)
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/달력`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 화면에 항상 펼쳐진 한 달 격자에서 날짜 하나를 고를 때 쓴다. 날짜마다 점·개수 같은 짧은
14
+ 제품 콘텐츠를 붙일 수 있다. 월 계산·로케일·오늘 날짜는 제품이 만들어 넘긴다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 필드를 눌러 열고 고른 뒤 닫힘 | [DatePicker](date-picker.md) (같은 Calendar를 담는다) |
21
+ | 시작·끝 기간 | [DateRangePicker](date-range-picker.md) (Calendar에 range 없음) |
22
+ | 시간 순 사건 목록 | [Timeline](timeline.md) |
23
+ | 기간별 활동량 격자 | [ActivityHeatmap](activity-heatmap.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `Calendar` | 기본 | `@hjmds/react`, `/calendar` | `@hjmds/react-native`, `/calendar` |
30
+
31
+ ## 최소 사용 예
32
+
33
+ ```tsx
34
+ // Web
35
+ import { Calendar } from "@hjmds/react/calendar";
36
+
37
+ <Calendar
38
+ descriptor={{
39
+ grid: { cells, weekdayLabels, todayDate },
40
+ monthLabel: formatMonth(month),
41
+ focusedMonth: month, onFocusedMonthChange: setMonth,
42
+ selectedDate: date, onSelectionChange: setDate,
43
+ }}
44
+ previousMonth={{ month: prevMonthKey, label: t("calendar.previousMonth") }}
45
+ nextMonth={{ month: nextMonthKey, label: t("calendar.nextMonth") }}
46
+ composeAccessibleName={({ date, isToday }) => formatDayName(date, isToday)}
47
+ onNavigateBeyondGrid={({ overflow }, focusDate) => { const next = shiftMonth(overflow); setMonth(next.month); focusDate(next.date); }}
48
+ />
49
+ ```
50
+
51
+ ```tsx
52
+ // Native
53
+ import { Calendar } from "@hjmds/react-native/calendar";
54
+
55
+ <Calendar
56
+ descriptor={descriptor}
57
+ previousMonth={{ month: prevMonthKey, label: t("calendar.previousMonth") }}
58
+ nextMonth={{ month: nextMonthKey, label: t("calendar.nextMonth") }}
59
+ composeAccessibleName={composeName}
60
+ renderCellContent={(cell) => cell.content ? <Dot /> : null}
61
+ />
62
+ ```
63
+
64
+ ## 축과 기본값
65
+
66
+ | prop | 값 | 기본값 | 설명 |
67
+ | --- | --- | --- | --- |
68
+ | `descriptor.grid.cells` | `readonly { date?: string; outsideFocusedMonth?: boolean; disabled?: boolean; content?: Content }[]`, 7열 row-major, 길이 7의 배수 | 필수 | `date`(ISO `YYYY-MM-DD`)가 없는 셀은 빈칸이다. 중복 날짜는 거부한다 |
69
+ | `descriptor.grid.weekdayLabels` · `grid.todayDate` · `monthLabel` | 요일 7개 튜플 · ISO 날짜 · 문자열 | 필수 | 제품이 현지화한 값. HJM은 `Date.now()`를 부르지 않는다 |
70
+ | `descriptor.selectedDate` + `onSelectionChange` · `defaultSelectedDate` | `string \| null`, `(date: string \| null) => void` | 비제어 `null` | 제어(`selectedDate`와 `onSelectionChange` 둘 다 필수)·비제어 중 하나만 타입이 허용한다. 단일 날짜만 |
71
+ | `descriptor.focusedMonth` + `onFocusedMonthChange` · `defaultFocusedMonth` | ISO 월(`YYYY-MM`), `(month: string, reason: "previous" \| "next" \| "jump") => void` | — | 실제 `grid`·`monthLabel` 교체는 제품이 한다. `onFocusedMonthChange`가 없으면 월 버튼이 비활성이다 |
72
+ | `previousMonth` · `nextMonth` | `{ month: string; label: string }` | — | 없으면 그 자리 버튼이 없다. 빈 `label`은 `TypeError` |
73
+ | `composeAccessibleName` | `(info: { date: string; isToday: boolean; isSelected: boolean; disabled: boolean; content?: Content }) => string` | 필수 | 날짜 칸의 접근성 이름 |
74
+ | `renderCellContent` | `(cell: ResolvedCalendarDateCell<Content>) => ReactNode` | — | `cell`은 셀 descriptor에 `date`·`row`·`column`·`isToday`·`isSelected`·`selectable`·`accessibleName`이 더해진 값. 결과는 접근성에서 숨는다 |
75
+ | Web `onNavigateBeyondGrid` | `(detail: { date: string; intent: "next-day" \| "previous-day" \| "next-week" \| "previous-week" \| "first-of-week" \| "last-of-week"; overflow: "before" \| "after" }, focusDate: (date: string) => void) => void` | — | 방향키가 격자 밖으로 나갈 때. 제품이 다음 달 grid를 넘긴 뒤 `focusDate`를 부른다 |
76
+ | `size` | `medium` · `large` | `medium` | — |
77
+ | `ref` | `{ focusDate(date: string): void }` | — | 날짜에 초점을 요청한다 |
78
+ | Web `autoFocus` | `boolean` | `false` | — |
79
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. Native도 같은 배치 슬롯을 받는다 |
80
+
81
+ ## 배치
82
+
83
+ | 항목 | 값 | 근거 |
84
+ | --- | --- | --- |
85
+ | 크기 | 날짜 칸 44(`medium` `control.minTouchTarget`, `large` `glyph.xxl` 44 — large는 글자만 `bodyLarge`로 커진다). 격자 최소 폭 7 × 44 = 308. 월 이동 버튼 44 원(`control.buttonHeight.medium`). 셀 `content`는 날짜 아래 label 한 줄 높이 | `calendarRecipe.sizes`·`header.navButton`, `.hjm-calendar__grid` |
86
+ | 간격 | 머리↔격자, 머리 안 `spacing.xs` 8. 요일 줄 위아래 `spacing.xs` 8. 날짜↔`content` `spacing.xxs` 4. Web 격자 바깥 `spacing.xxs` 4 | `calendarRecipe.header.gap`, `.hjm-calendar*` |
87
+ | 순서·정렬 | 위→아래 [이전 월][월 제목(가운데, title)][다음 월] → 요일 줄 → 주 단위 7열. 화면 본문에 인라인으로 두며 팝업·시트를 열지 않는다 | `.hjm-calendar__header`, `calendarRecipe` 주석(`shared`) |
88
+ | 고정·스크롤 | 폭이 308보다 좁으면 격자가 가로 스크롤한다(Web `.hjm-calendar__viewport` `overflow-x: auto`, Native `ScrollView horizontal`) | `.hjm-calendar__viewport`, `react-native/src/calendar.tsx` |
89
+ | 좁은 폭·큰 글자 | 칸 크기는 고정이고 월 제목·요일·`content`가 줄바꿈된다(`overflow-wrap: anywhere`). 큰 글자로 칸이 넘치면 가로 스크롤 | `.hjm-calendar__header strong`, `.hjm-calendar__content` |
90
+
91
+ ```text
92
+ ┌──────────────────────────────┐
93
+ │ [‹] 2026년 10월 [›] │ ← 머리(44 버튼, 가운데 제목)
94
+ │ 월 화 수 목 금 토 일 │ ← 요일(label, muted)
95
+ │ (1) (2) (3) (4) (5) (6) (7) │ ← 44 칸 × 7
96
+ │ · · │ ← content(선택)
97
+ │ … │
98
+ └──────────────────────────────┘
99
+ ```
100
+
101
+ ## 꼭 지킬 것
102
+
103
+ - `composeAccessibleName`이 날짜·오늘·비활성·제품 콘텐츠의 의미를 모두 문장으로 만든다. `renderCellContent`는 접근성에서 숨는다.
104
+ - `renderCellContent`에 버튼·입력을 넣지 않는다. 장식 콘텐츠만 넣는다.
105
+ - 월 이동 label, 요일, 월 제목은 i18n 키·제품 로케일 포맷으로 넣는다.
106
+ - 비활성 날짜는 `disabled`로 표시한다. 초점은 가지만 선택되지 않는다.
107
+
108
+ ## 플랫폼 차이
109
+
110
+ | 항목 | Web | Native |
111
+ | --- | --- | --- |
112
+ | 키보드 격자 탐색 | 방향키·Home/End, roving tabindex | 없음(월 버튼으로 이동) |
113
+ | 격자 밖 이동 | `onNavigateBeyondGrid` | 없음 |
114
+ | `autoFocus` | 있음(기본 `false`) | 없음 |
115
+ | `className`·`layoutStyle` | 있음 | `layoutStyle` 지원, `className` 없음 |
116
+ | 날짜 셀 역할 | `gridcell` + `aria-selected` | `button` + `accessibilityState.selected` |
117
+ | 좁은 폭 | 격자 영역 가로 스크롤 | 격자를 가로 `ScrollView`로 감쌈 |
118
+ | `focusDate` | DOM 초점 | 접근성 초점(RN Web은 DOM 초점) |
119
+
120
+ ## 함정
121
+
122
+ - Web에서 `onNavigateBeyondGrid`를 연결하지 않으면 방향키가 월 경계에서 멈춘다. 연속 키보드 탐색이 필요하면 반드시 연결한다.
@@ -0,0 +1,110 @@
1
+ # Card
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: recipe `cardRecipe`(`src/card.ts`), 바탕 `surfaceRecipe`(`src/base-recipes.ts`). 별도 계약 문서는 없다
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/카드`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 제목·설명·본문·행동이 한 덩어리로 읽히는 독립된 콘텐츠 단위에 쓴다. 주문 요약, 설정 묶음,
14
+ 미리보기처럼 화면 안에서 경계가 보여야 하는 블록이 여기에 속한다. 위에서부터
15
+ `media` → `leading`+`title`+`description` → `children` → `actions` 순서가 고정이다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 목록의 한 행(누르면 상세로 이동) | [ListRow](list-row.md) |
22
+ | 제목 없는 배경·테두리 영역만 필요 | [Surface](surface.md) |
23
+ | 숫자 하나가 주인공인 요약 | [Statistic](statistic.md) |
24
+ | 비어 있음·결과 안내 | [EmptyState](empty-state.md), [Result](result.md) |
25
+ | 접고 펴는 묶음 | [Collapsible](collapsible.md), [Accordion](accordion.md) |
26
+ | 화면 섹션 제목과 본문 묶음 | [Section](section.md) |
27
+
28
+ ## 공개 이름과 import
29
+
30
+ | 이름 | 역할 | Web | Native |
31
+ | --- | --- | --- | --- |
32
+ | `Card` | 기본 | `@hjmds/react`, `/display` | `@hjmds/react-native`, `/data-display` |
33
+
34
+ ## 최소 사용 예
35
+
36
+ ```tsx
37
+ // Web
38
+ import { Card } from "@hjmds/react/display";
39
+ import { Button } from "@hjmds/react/actions";
40
+
41
+ <Card
42
+ title={t("order.summary.title")}
43
+ description={t("order.summary.description")}
44
+ actions={<Button tone="secondary" onClick={openDetail}>{t("order.summary.detail")}</Button>}
45
+ >
46
+ {summary}
47
+ </Card>
48
+ ```
49
+
50
+ ```tsx
51
+ // Native
52
+ import { Card } from "@hjmds/react-native/data-display";
53
+ import { Button } from "@hjmds/react-native/actions";
54
+
55
+ <Card
56
+ title={t("order.summary.title")}
57
+ description={t("order.summary.description")}
58
+ actions={<Button tone="secondary" onPress={openDetail}>{t("order.summary.detail")}</Button>}
59
+ >
60
+ {summary}
61
+ </Card>
62
+ ```
63
+
64
+ ## 축과 기본값
65
+
66
+ | prop | 값 | 기본값 | 설명 |
67
+ | --- | --- | --- | --- |
68
+ | `tone` | `default` · `raised` · `accent` · `sunken` · `subtle`(Surface tone) | `default` | — |
69
+ | `bordered` | `boolean` | `true` | Surface 기본 `false`와 다르다 |
70
+ | `padding` | spacing 이름 | `md`(16) | — |
71
+ | `radius` | radius 이름 | `lg`(16) | — |
72
+ | `selected` | `boolean` | `false` | `true`면 tone이 `accent`로 바뀐다(`cardRecipe.selectedTone`) |
73
+ | `headingLevel`(Web만) | `2` · `3` · `4` | `3` | 제목이 `h3`로 렌더되므로 문서 위계에 맞춰 고른다 |
74
+ | `title` · `description` · `leading` · `media` · `actions` · `children` | `ReactNode` | — | 슬롯. 순서는 HJM이 고정한다 |
75
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. 두 플랫폼 모두 root Surface에 적용된다 |
76
+
77
+ Card 자체에는 콜백이 없다. 누름 행동은 `actions`의 Button(Web `onClick: (event: MouseEvent<HTMLButtonElement>) => void`, Native `onPress`)이 갖는다.
78
+
79
+ ## 배치
80
+
81
+ | 항목 | 값 | 근거 |
82
+ | --- | --- | --- |
83
+ | 크기 | 부모 폭을 채우고 높이는 내용이 정한다. 모서리 `radius.lg` 16, 테두리 기본 있음. `media` 자식은 폭 100%(비율은 [AspectRatio](aspect-ratio.md)로) | `cardRecipe.defaults`, `.hjm-card__media > *` |
84
+ | 간격 | 본문 안쪽 `spacing.md` 16, 본문 요소 사이 `spacing.xs` 8, leading↔제목 `spacing.sm` 12. 행동 영역은 좌우·아래 `spacing.md` 16, 버튼 사이 `spacing.sm` 12. 카드 목록 사이 간격은 부모 [Stack](stack.md)·[Grid](grid.md)이 정한다 | `cardRecipe.body`·`header`·`actions`, `.hjm-card*` |
85
+ | 순서·정렬 | 위→아래 `media` → [`leading` + `title`/`description`] → `children` → `actions` 고정. leading은 시작 쪽 위 정렬. 행동은 시작 쪽부터 한 줄로 놓인다 | `cardRecipe.slots`, `.hjm-card__header`·`__actions` |
86
+ | 고정·스크롤 | 고정 영역이 없다. 카드 안에서 스크롤 영역을 만들지 않는다(넘치면 `overflow: hidden`으로 잘린다) | `.hjm-card { overflow: hidden }` |
87
+ | 좁은 폭·큰 글자 | 제목·설명이 줄바꿈되고, 행동은 줄바꿈된다(`flex-wrap: wrap`) | `.hjm-card__title`, `.hjm-card__actions`, `react-native/src/data-display.tsx` |
88
+
89
+ ## 꼭 지킬 것
90
+
91
+ - 제목·설명·행동 라벨은 i18n 키로 넣는다. 제품 데이터는 `children`과 `media`에 둔다.
92
+ - `media`(대표 이미지·일러스트)는 제품 소유다. 카드 모서리 clip과 구조는 HJM이 맡는다.
93
+ - 색·radius·padding은 `tone`·`radius`·`padding` 축으로만 바꾼다. 카드마다 브랜드 색을 칠하지 않는다.
94
+ - 배치는 두 플랫폼 모두 `layoutStyle`로 한다. Native `CardProps`에는 `style`이 없다. Web의 `className`·`style`로 recipe 값을 덮지 않는다.
95
+ 카드 목록 사이 간격은 바깥 [Stack](stack.md)·[Grid](grid.md)이 정한다.
96
+ - Card 자체는 누름 행동이 없다(Native는 `onPress`가 없다). 카드 안 행동은 `actions`의 Button으로 둔다.
97
+
98
+ ## 플랫폼 차이
99
+
100
+ | 항목 | Web | Native |
101
+ | --- | --- | --- |
102
+ | root | `<article>` | `View`(Surface) |
103
+ | 제목 | `h{headingLevel}` | `Text` + `accessibilityRole="header"`, 수준 지정 없음 |
104
+ | 배치 | `layoutStyle` | `layoutStyle` |
105
+ | 내용 clip | tone의 `clipsContent`(`raised`는 그림자 때문에 clip 안 함) | 내부 `View`가 항상 clip, 그림자는 바깥에 남는다 |
106
+
107
+ ## 함정
108
+
109
+ - `selected`는 tone만 바꾼다. Web은 `data-state="selected"` 속성뿐이고 Native는 접근성 state를
110
+ 알리지 않는다. 선택 가능한 카드 목록이라면 선택 상태를 문구나 다른 컨트롤로도 전달한다.
@@ -0,0 +1,113 @@
1
+ # Carousel
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Carousel](../../carousel.md), `CarouselMotion`은 [선택형 어댑터](../../optional-adapters.md), contract `src/carousel.ts`
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/캐러셀`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 한 번에 카드 하나만 보이고 사용자가 순서대로 넘겨 보는 유한한 묶음에 쓴다. 오늘 경기 스트립,
14
+ 소개 카드 몇 장이 여기에 속한다. 끝에서 처음으로 돌아가지 않고, 자동 재생은 `autoplay`를 줄 때만 켜진다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 여러 항목을 한눈에 비교·훑기 | [List](list.md), [Masonry](masonry.md) |
21
+ | 항목 수가 많거나 끝이 없음 | [VirtualList](virtual-list.md), [LoadMore](load-more.md) |
22
+ | 같은 자리의 보기 전환 | [Tabs](tabs.md), [SegmentedControl](segmented-control.md) |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `Carousel` | 기본(추가 peer 없음) | `@hjmds/react`, `/carousel` | `@hjmds/react-native`, `/carousel` |
29
+ | `CarouselMotion` | 확장(스와이프 모션, optional-extension) | `/carousel-motion` | `/carousel-motion` |
30
+
31
+ `CarouselMotion`은 granular subpath로만 import 된다. 필요한 optional peer는 Web `embla-carousel-react`,
32
+ Native `react-native-reanimated-carousel`·`react-native-worklets`(직접 import)와 그 라이브러리의 peer인
33
+ `react-native-reanimated`·`react-native-gesture-handler`다. 앱에 없으면 tsc·테스트는 통과해도 기기 Metro 번들에서 실패한다.
34
+
35
+ ## 최소 사용 예
36
+
37
+ ```tsx
38
+ // Web
39
+ import { Carousel } from "@hjmds/react/carousel";
40
+
41
+ <Carousel
42
+ label={t("home.games.label")}
43
+ slides={games.map((game) => ({ id: game.id, label: game.title }))}
44
+ renderSlide={(slide) => <GameCard gameId={slide.id} />}
45
+ composeAccessibleName={({ position, total, label }) =>
46
+ t("home.games.slideName", { position, total, label })}
47
+ labels={{
48
+ previous: t("carousel.previous"), next: t("carousel.next"),
49
+ pause: t("carousel.pause"), resume: t("carousel.resume"),
50
+ navigation: t("carousel.navigation"),
51
+ }}
52
+ />
53
+ ```
54
+
55
+ ```tsx
56
+ // Native
57
+ import { Carousel } from "@hjmds/react-native/carousel";
58
+
59
+ <Carousel
60
+ label={t("home.games.label")}
61
+ slides={slides}
62
+ currentKey={currentId}
63
+ onCurrentKeyChange={setCurrentId}
64
+ renderSlide={(slide) => <GameCard gameId={slide.id} />}
65
+ composeAccessibleName={composeSlideName}
66
+ labels={carouselLabels}
67
+ />
68
+ ```
69
+
70
+ ## 축과 기본값
71
+
72
+ | prop | 값 | 기본값 | 설명 |
73
+ | --- | --- | --- | --- |
74
+ | `label` | `string` | 필수 | 묶음 전체의 접근성 이름. 비우면 `TypeError` |
75
+ | `slides` | `readonly { id: string; label: string }[]` | 필수 | 하나 이상. `label`은 슬라이드 접근성 이름이고 시각 콘텐츠는 `renderSlide`가 그린다 |
76
+ | `renderSlide` | `(slide: { id: string; label: string }) => ReactNode` | 필수 | — |
77
+ | `composeAccessibleName` | `(info: { position: number; total: number; label: string }) => string` | 필수 | `position`은 1부터 |
78
+ | `labels` | `{ previous: string; next: string; pause: string; resume: string; navigation: string }` | 필수 | 다섯 값 모두 비우면 `TypeError` |
79
+ | `currentKey` + `onCurrentKeyChange` · `defaultCurrentKey` | 슬라이드 `id`, `(key: string) => void` | 첫 슬라이드 | 제어(둘 다 필수)·비제어 중 하나만 타입이 허용한다. 값은 인덱스가 아니라 `id`다 |
80
+ | `autoplay` | `{ intervalMs: number }`(0보다 큼) | 없음 | 주면 일시정지/재개 버튼이 생긴다. 마지막 슬라이드, reduced motion, 사용자 조작 뒤에는 멈추고 명시적인 재개 전까지 다시 돌지 않는다 |
81
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. Native `style`은 deprecated — layoutStyle 또는 tone/토큰 |
82
+ | `CarouselMotion` 선택 | `currentKey: string`, `onCurrentKeyChange(key: string): void` | 필수(항상 제어) | autoplay가 없다. `slides`는 `{ id, label, disabled? }` |
83
+ | `CarouselMotion` 라벨 | `label`·`previousLabel`·`nextLabel`, 선택 `composeAccessibleName` | — | `composeAccessibleName`이 없으면 슬라이드 `label`을 쓴다 |
84
+ | `CarouselMotion` 크기(Native) | `width`·`height` 측정값 | 필수 | 양수가 아니면 `TypeError`. Web `CarouselMotion`은 `layoutStyle`을 받고 Native는 받지 않는다 |
85
+
86
+ ## 배치
87
+
88
+ | 항목 | 값 | 근거 |
89
+ | --- | --- | --- |
90
+ | 크기 | 부모 폭을 채우고 슬라이드 높이는 내용이 정한다(현재 슬라이드만 보인다). 이전·다음은 ghost Button(medium 44), Web 점은 지름 8에 터치 영역 44(`control.minTouchTarget`) | `carouselRecipe.dot`, `.hjm-carousel__dot` |
91
+ | 간격 | 영역 사이(재생 버튼·슬라이드·조작 줄) `spacing.sm` 12, 조작 줄 안 `spacing.xs` 8 | `carouselRecipe.sizes.medium.gap`, `.hjm-carousel__controls` |
92
+ | 순서·정렬 | 위→아래 [일시정지/재개(autoplay일 때, 시작 쪽)] → [슬라이드] → [이전][점 · · ·][다음] 가운데 정렬. Native는 점 대신 번호 버튼(현재 `secondary`, 나머지 `ghost`)을 이전·다음 사이에 둔다 | `react/src/carousel.tsx`, `react-native/src/carousel.tsx` |
93
+ | 고정·스크롤 | 고정 영역이 없다. 슬라이드 넘김은 버튼·점(또는 `CarouselMotion` 스와이프)으로 하고 가로 스크롤 영역을 만들지 않는다 | `.hjm-carousel__slide[hidden]` |
94
+ | 좁은 폭·큰 글자 | 조작 줄은 줄바꿈된다. Web 큰 글자에서는 점이 첫 줄, [이전][다음]이 둘째 줄 두 칸으로 나뉜다 | `.hjm-carousel[data-large-text="true"] .hjm-carousel__controls` |
95
+
96
+ ## 꼭 지킬 것
97
+
98
+ - `label`, `labels`의 다섯 값, 슬라이드 `label`은 비어 있으면 `TypeError`를 던진다. 모두 i18n 키로 넣는다.
99
+ - 슬라이드 id는 유일하고 앞뒤 공백이 없어야 한다. 빈 배열은 던지므로 로딩·빈 상태는 마운트 전에 제품이 처리한다.
100
+ - `composeAccessibleName`의 어순·조사는 제품 문구다. HJM은 위치 정보만 넘긴다.
101
+ - 슬라이드 안의 시각 콘텐츠(카드·이미지)는 제품 소유다. 컨트롤·점·접근성 구조는 HJM 소유라 다시 만들지 않는다.
102
+ - 배치는 `layoutStyle`로만 한다. Native `style`은 deprecated(개발 모드 1회 경고, 다음 major 제거)다.
103
+
104
+ ## 플랫폼 차이
105
+
106
+ | 항목 | Web | Native |
107
+ | --- | --- | --- |
108
+ | 배치 | `layoutStyle`(HTML `className`도 전달) | `layoutStyle`(`style`은 deprecated) |
109
+ | 위치 표시 | 점 버튼 | 번호 버튼 + 조정 가능(adjustable) 위치 요소 |
110
+ | 스와이프 | 없음(`CarouselMotion` 필요) | 기본 가로 스와이프 |
111
+ | 자동 재생 정지 조건 | hover, 포커스, 탭 숨김 | 백그라운드, 스크린 리더 켜짐 |
112
+ | 키보드 | 컨트롤 영역에서 좌우 화살표(RTL 반전) | 해당 없음 |
113
+ | `CarouselMotion` 크기 | 컨테이너 폭 | `width`·`height` 필수(측정값, 양수 아니면 던짐) |
@@ -0,0 +1,96 @@
1
+ # Celebration
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: recipe `celebrationRecipe`(`src/interaction-adapters.ts`), 승격 기록 [Stable Core](../../stable-core.md)
9
+ - 스토리북: `배포/구성/직접 조작과 모션/끌기·밀기·화면 전환`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 목표 달성, 첫 완료처럼 드물게 일어나는 성공 순간에 한 번 터지는 색종이 효과에 쓴다.
14
+ 장식이라 화면 리더에는 보이지 않으며, 성공 사실 자체는 다른 요소가 전달해야 한다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 저장·전송 같은 일상 성공 알림 | [Toast](toast.md) |
21
+ | 흐름의 끝을 알리는 완료 화면 | [Result](result.md) |
22
+ | 반복·지속되는 배경 효과 | [EffectSurface](effect-surface.md) |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `Celebration` | 기본(별도 보조 기능, supplemental) | `/celebration` | `/celebration` |
29
+
30
+ root에서 export되지 않고 `@hjmds/react/celebration`, `@hjmds/react-native/celebration`으로만 import 된다.
31
+ optional peer가 필요하다.
32
+
33
+ - Web: `canvas-confetti`
34
+ - Native: `react-native-fast-confetti`와 그 peer `@shopify/react-native-skia`·`react-native-reanimated`·
35
+ `react-native-worklets`. 네 패키지 모두 앱에 설치돼 있어야 한다.
36
+
37
+ ## 최소 사용 예
38
+
39
+ ```tsx
40
+ // Web
41
+ import { Celebration } from "@hjmds/react/celebration";
42
+
43
+ {goal.completedEventId ? (
44
+ <Celebration eventId={goal.completedEventId} preset="milestone" onComplete={clearEvent} />
45
+ ) : null}
46
+ ```
47
+
48
+ ```tsx
49
+ // Native
50
+ import { Celebration } from "@hjmds/react-native/celebration";
51
+
52
+ {eventId ? <Celebration eventId={eventId} onComplete={() => setEventId(null)} /> : null}
53
+ ```
54
+
55
+ ## 축과 기본값
56
+
57
+ | prop | 값 | 기본값 | 설명 |
58
+ | --- | --- | --- | --- |
59
+ | `preset` | `small-burst`(입자 32개·1.6초) · `milestone`(64개·2.4초) | `small-burst` | — |
60
+ | `eventId` | 비지 않은 문자열 | 필수 | 같은 인스턴스에서 같은 id는 한 번만 터진다. 다시 터뜨리려면 새 id를 준다. 빈 문자열은 `TypeError` |
61
+ | `onComplete` | `() => void` | — | 그리기가 끝났거나 그리지 않기로 했을 때 한 번 불린다. 인자가 없다 |
62
+ | 입자 색 | — | 테마 primary + 상태 강조색 네 개 | 색 prop은 없고 제품 테마로만 바뀐다 |
63
+ | `layoutStyle` | 없음 | — | Web `layoutStyle` 제외 15개 중 하나다. 전체 화면 층이라 배치 prop이 없다 |
64
+
65
+ ## 배치
66
+
67
+ | 항목 | 값 | 근거 |
68
+ | --- | --- | --- |
69
+ | 크기 | Web은 뷰포트 전체를 덮는 canvas(`position: fixed; inset: 0`), Native는 부모를 채우는 `StyleSheet.absoluteFill` View다. Native에서 화면 전체에 터뜨리려면 화면 루트 View의 마지막 자식으로 둔다 | `react/src/celebration.tsx`, `react-native/src/celebration.tsx` |
70
+ | 간격 | 레이아웃 공간을 차지하지 않는다. 이웃 간격에 영향이 없다 | 같은 파일 |
71
+ | 순서·정렬 | 성공을 알리는 Toast·Result·문구와 함께 렌더하고 Celebration은 그 위에 겹친다. Web canvas는 z-index를 지정하지 않으므로 z-index가 있는 층(BottomNavigation `layer.sticky` 100, 오버레이 `layer.modal` 900) 아래에 그려질 수 있다 | `react/src/celebration.tsx`, `.hjm-bottom-navigation`, `.hjm-overlay` |
72
+ | 고정·스크롤 | 누름을 막지 않는다(`pointer-events: none`). Web은 스크롤과 무관하게 화면에 고정되고, Native는 부모와 함께 움직인다 | 같은 파일 |
73
+ | 좁은 폭·큰 글자 | 영향 없음. reduced motion에서는 그리지 않는다 | `react-native/src/celebration.tsx`(`environment.reducedMotion`) |
74
+
75
+ ## 꼭 지킬 것
76
+
77
+ - 성공 사실은 Toast·Result·문구로 따로 알린다. Celebration은 `aria-hidden`이라 아무것도 낭독하지 않는다.
78
+ - reduced motion, 숨은 탭(Web), 백그라운드 앱(Native)에서는 그리지 않고 바로 `onComplete`를 부른다.
79
+ `onComplete`를 "애니메이션을 봤다"는 근거로 쓰지 않는다.
80
+ - 위에 덮이는 전체 화면 층이라 누름을 막지 않는다. 배치 prop(`style`·`layoutStyle`)은 없다(Web `layoutStyle` 제외 목록).
81
+ - Web은 `HjmProvider` 안에서만 쓴다(`useHjmTheme`가 Provider 밖에서 던진다).
82
+
83
+ ## 플랫폼 차이
84
+
85
+ | 항목 | Web | Native |
86
+ | --- | --- | --- |
87
+ | 그리는 곳 | `position: fixed` 전체 화면 canvas | 부모를 채우는 absolute View |
88
+ | 종료 시점 | preset 시간 뒤 | 엔진의 끝 콜백, 시작 실패 시 5초 상한 |
89
+
90
+ ## 함정
91
+
92
+ - 2026-10 utilverse에서 `@hjmds/react-native/celebration`을 쓰면서 `react-native-fast-confetti`를 설치하지 않았다.
93
+ tsc·lint·단위 테스트는 모두 통과했지만 기기 Metro가 "Unable to resolve module"로 크래시했다.
94
+ 타입 검사는 `.d.ts`만 보고 테스트는 이 파일을 불러오지 않기 때문이다. 쓰기 전에 위 peer가 모두 설치됐는지
95
+ 확인하고, 기기 번들로 한 번 열어 본 뒤 통과를 보고한다. 같은 위험이 effect-surface·qr-code·thinking-orb·
96
+ toast-liquid subpath에도 있다.
@@ -0,0 +1,122 @@
1
+ # ChatMessage
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screens.tsx`·`src/screen-flows.tsx`; 기존 개별 지침을 새 규격으로 통합. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
+ - 스토리북: `배포/구성/정보 표시/대화 메시지`, `배포/화면/소통/채팅`
10
+
11
+ ## 언제 쓰나
12
+
13
+ DM·대화 타임라인의 메시지 한 개에 쓴다. 발신/수신 정렬, 작성자, 아바타, 답장 인용, 시각과 전송 상태,
14
+ 길게 눌러 여는 반응 메뉴, 좌우 스와이프 답장을 한 번에 합성한다. 별도 보조 기능(supplemental)이라
15
+ `/screens` subpath로만 import 한다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 화면 전체(헤더·타임라인·작성창) | [ChatScreen](chat-screen.md) |
22
+ | 입력창 | [MessageComposer](message-composer.md) |
23
+ | 부모/답글이 있는 댓글 | [CommentThreadScreen](comment-thread-screen.md) |
24
+ | 알림 목록의 한 행 | [NotificationItem](notification-item.md) |
25
+ | 행을 밀어 버튼을 드러내는 동작 | [SwipeActions](swipe-actions.md) (답장 스와이프와 다른 동작) |
26
+
27
+ ## 공개 이름과 import
28
+
29
+ | 이름 | 역할 | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `ChatMessage` | 메시지 한 개 | `/screens` | `/screens` |
32
+ | `ReactionPicker` | `reactions` prop이 받는 타입(`ReactionPickerProps`)의 원본 | `/reaction-picker` | `/reaction-picker` |
33
+
34
+ 루트 barrel에는 없다. `/screens`는 optional native peer를 요구하지 않는다(Native 반응 메뉴는 core `Modal`).
35
+
36
+ ## 최소 사용 예
37
+
38
+ ```tsx
39
+ // Web
40
+ import { Avatar } from "@hjmds/react/display";
41
+ import { ChatMessage } from "@hjmds/react/screens";
42
+
43
+ <ChatMessage
44
+ direction={mine ? "outgoing" : "incoming"}
45
+ author={message.authorName}
46
+ timestamp={formatTime(message.sentAt)}
47
+ {...(mine ? { deliveryLabel: t("chat.delivered") } : { avatar: <Avatar name={message.authorName} /> })}
48
+ replyAction={{ label: t("chat.reply"), onPress: () => setReplyTo(message.id) }}
49
+ reactions={{
50
+ label: t("chat.react"), closeLabel: t("common.close"),
51
+ options: reactionOptions, value: message.myReaction, onValueChange: v => react(message.id, v),
52
+ }}
53
+ >
54
+ {message.text}
55
+ </ChatMessage>
56
+ ```
57
+
58
+ ```tsx
59
+ // Native
60
+ import { ChatMessage } from "@hjmds/react-native/screens";
61
+ import { Text } from "@hjmds/react-native/primitives";
62
+
63
+ <ChatMessage direction="incoming" author={message.authorName} timestamp={formatTime(message.sentAt)}
64
+ replyAction={{ label: t("chat.reply"), onPress: () => setReplyTo(message.id) }}>
65
+ <Text>{message.text}</Text>
66
+ </ChatMessage>
67
+ ```
68
+
69
+ ## 축과 기본값
70
+
71
+ | prop | 값 | 기본값 | 설명 |
72
+ | --- | --- | --- | --- |
73
+ | `direction` | `"incoming"` · `"outgoing"` | 필수 | 수신은 시작 쪽, 발신은 끝 쪽에 붙는다 |
74
+ | `author` | `string` | 필수 | 빈 문자열이면 작성자 줄을 그리지 않는다(연속 메시지 그룹) |
75
+ | `timestamp` | `string` | 필수 | 제품이 포맷한 시각. 빈 문자열이고 `deliveryLabel`·`actions`도 없으면 메타 줄을 그리지 않는다 |
76
+ | `deliveryLabel` | `string` | 없음 | 시각 뒤에 ` · `로 붙는 전송 상태 |
77
+ | `avatar` | `ReactNode` | 없음 | 버블 옆 아바타 |
78
+ | `reply` | `ReactNode` | 없음 | 인용 노드. `replyLink`가 없으면 버블 안 인용으로 그린다 |
79
+ | `replyLink` | `{ label, onPress() }` | 없음 | `reply`와 같이 주면 인용을 버블 밖 ghost 버튼으로 그려 원문 이동에 쓴다 |
80
+ | `replyAction` | `{ label, onPress(), disabled? }` | 없음 | `replySwipeDistance` 60px 이상이고 세로 이동의 2배를 넘는 가로 스와이프로 실행(`isReplySwipe`). 보이는 답장 버튼은 없다 |
81
+ | `reactions` | `ReactionPickerProps & { closeLabel, menuAction? }` | 없음 | `reactionHoldMs` 450ms 길게 누르면 반응 메뉴가 열린다. 같은 반응 재선택은 `null`. 메뉴 안 picker는 `layout="strip"`(이모지 `typography.title` 18/26). `label`은 Web에서 말풍선 이름·Popover 제목, Native에서 접근성 힌트·동작 이름 |
82
+ | `interactiveContent` | `boolean` | `false` | 사진 앨범·링크처럼 자식이 버튼을 가지면 `true`. 안쪽 버튼·링크의 누름과 Web Enter/Space는 그 컨트롤이 받는다 |
83
+ | `actions` | `ReactNode` | 없음 | 시각 옆 슬롯. 반응 집계 배지·재시도는 제품이 넣는다 |
84
+ | `children` | `ReactNode` | 필수 | 메시지 본문. Native는 말풍선이 이 텍스트로 읽힌다 |
85
+ | Web `layoutStyle` | `HjmCompositionStyleProp` | 없음 | 메시지 행(`article`) 배치. `transform`은 스와이프 오프셋이 소유한다. 미게시(1.12.1 이후) |
86
+
87
+ ## 배치
88
+
89
+ | 항목 | 값 | 근거 |
90
+ | --- | --- | --- |
91
+ | 크기 | 말풍선 열 최대 폭 `screenPatternRecipe.messageMaxWidth` 84%; 버블 radius `radius.lg`, 테두리 1(발신은 Native에서 테두리 없음) | `screen-patterns.ts`, Web `styles.css` `.hjm-chat-message__bubble`, Native `screens.tsx` |
92
+ | 간격 | 아바타–버블 열 `spacing.xs` 8; 작성자·버블·메타 줄 사이 `spacing.xxs` 4; 버블 안 padding `spacing.sm` 12; 버블 안 인용 왼쪽 선 2 + `spacing.xs` 8; 메타 줄 시각–`actions` `spacing.xs` 8 | Web `.hjm-chat-message*`, Native `ChatMessage` |
93
+ | 순서·정렬 | incoming 시작 쪽, outgoing 끝 쪽; 작성자 → (replyLink 인용) → 버블 → 시각·전송 상태·actions | 렌더 순서 |
94
+ | 고정·스크롤 | 자체 고정 없음, 타임라인(ChatScreen 본문)이 스크롤; 반응 메뉴는 Web Popover · Native 전체 화면 `Modal`; 답장 스와이프 중 가로 이동은 ±72로 제한 | Web·Native `ChatMessage` |
95
+ | 좁은 폭·큰 글자 | 버블은 84% 안에서 줄바꿈되고 본문을 자르지 않는다(Web `overflow-wrap: anywhere`, `white-space: pre-wrap`); 반대쪽 16% 여백은 작성자 구분을 위해 남긴다 | Web `.hjm-chat-message__bubble`, Native `maxWidth`·`flexShrink: 1` |
96
+
97
+ ## 꼭 지킬 것
98
+
99
+ - 문구(작성자·시각·전송 상태·접근성 이름)는 모두 제품 i18n에서 만든다. 상대시간도 제품이 포맷한다.
100
+ - 영수증·반응 저장·재시도·원문 id·삭제된 메시지 안내는 제품 소유다. HJM은 상태를 저장하지 않는다.
101
+ - Web 배치는 `layoutStyle`로 한다(`transform`은 덮어쓰인다). Native는 `layoutStyle`·`style`이 없어 타임라인 쪽 wrapper가 배치한다.
102
+ - Native 말풍선에 `accessibilityLabel`을 덧씌우지 않는다. 말풍선은 자기 텍스트로 읽히고, 작성자·시각은 따로 읽히며,
103
+ 답장·반응은 접근성 동작(`reply`·`activate`)으로 남는다. 2026-10-06 리뷰에서 행 이름(`author`)과 반응 이름(`picker.label`)이
104
+ 본문을 가려 화면 낭독기가 메시지를 읽지 못했다.
105
+ - 타임라인 전체를 live region으로 감싸지 않는다(과거 메시지 로딩 때 읽기 순서를 가로챈다).
106
+
107
+ ## 플랫폼 차이
108
+
109
+ | 항목 | Web | Native |
110
+ | --- | --- | --- |
111
+ | 반응 메뉴 | Popover(Escape·외부 클릭·포커스 복귀), 우클릭·말풍선 자체의 Enter/Space로도 열림. 시각적으로 숨긴 닫기 버튼은 포커스를 받으면 보인다 | 전체 화면 `Modal`, 접근성 activate로도 열림. 닫기 대상은 표준 `activate` 동작에 답해 VoiceOver·TalkBack 모두 닫을 수 있다 |
112
+ | 낭독 | 메시지 행 `article`의 이름이 `author`, 말풍선 이름이 `picker.label`(반응이 있을 때) | 말풍선은 본문 텍스트로 읽히고 `picker.label`은 힌트. 작성자·시각은 별도 요소 |
113
+ | 키보드·보조기기 답장 | 메시지 포커스 후 Alt+←/→ | 접근성 custom action `reply` |
114
+ | `interactiveContent` | 말풍선을 `group`으로 두고, 안쪽 버튼·링크의 누름·Enter/Space는 반응 메뉴가 아니라 그 컨트롤이 받는다 | 반응이 있으면 별도 `···` IconButton이 메뉴를 열고 `reply` 동작도 가진다. 반응이 없으면 `reply` 동작이 시각 caption(시각이 없으면 작성자 caption)에 붙는다 |
115
+ | `menuAction` | 즉시 실행 | 모달이 닫힌 뒤 실행(iOS 모달 경합 방지) |
116
+
117
+ ## 함정
118
+
119
+ - Native는 반응 모달 안에서 `children`을 미리보기로 한 번 더 렌더한다. 자식은 표시용 콘텐츠로 준다
120
+ (부작용·고유 ref를 두지 않는다).
121
+ - `reactions`의 `options`와 `more.options`는 id가 합쳐서 유일하고 emoji·label이 비어 있지 않아야 한다. `value`가
122
+ 목록에 없는 id면 렌더 중 `TypeError`가 난다(`validateReactions`). 서버가 모르는 반응을 돌려줄 때를 대비한다.