@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,104 @@
1
+ # ActivityHeatmap
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Activity heatmap](../../activity-heatmap.md), descriptor `resolveActivityHeatmap`(`src/activity-heatmap.ts`)
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/활동 히트맵`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 최대 1년(366일) 범위의 일별 활동량을 한눈에 보여 주는 읽기 전용 개요에 쓴다. 프로필의 기록 빈도,
14
+ 습관 달성 일수 같은 화면이다. 카탈로그 계약이 아닌 별도 보조 기능(supplemental)이다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 날짜를 고르거나 일정 보기 | [Calendar](calendar.md), [DatePicker](date-picker.md) |
21
+ | 366일보다 긴 기록 | 제품이 범위를 나눠 페이지로 보인다(한 번에 넘기면 throw) |
22
+ | 숫자 하나의 요약 | [Statistic](statistic.md) |
23
+ | 시간 순 사건 목록 | [Timeline](timeline.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `ActivityHeatmap` | 기본(supplemental, granular subpath만) | `@hjmds/react/activity-heatmap` | `@hjmds/react-native/activity-heatmap` |
30
+ | `ActivityHeatmapDescriptor` 타입 | 보조(입력 descriptor) | `@hjmds/design-contracts/activity-heatmap` | 같음 |
31
+
32
+ root entry에서는 export되지 않는다. `ActivityHeatmaps` 같은 복수형 공개 이름은 없다.
33
+ optional peer는 필요 없다(Native는 `react-native` 기본 View·ScrollView만 쓴다).
34
+
35
+ ## 최소 사용 예
36
+
37
+ ```tsx
38
+ // Web
39
+ import { ActivityHeatmap } from "@hjmds/react/activity-heatmap";
40
+
41
+ <ActivityHeatmap
42
+ label={t("profile.activity.label")}
43
+ descriptor={{ startDate: "2026-01-01", endDate: "2026-12-31", days }}
44
+ formatDay={(date, value) =>
45
+ value === null ? t("profile.activity.unknown", { date }) : t("profile.activity.day", { date, count: value })}
46
+ view={view}
47
+ />
48
+ ```
49
+
50
+ ```tsx
51
+ // Native
52
+ import { ActivityHeatmap } from "@hjmds/react-native/activity-heatmap";
53
+
54
+ <ActivityHeatmap
55
+ label={t("profile.activity.label")}
56
+ descriptor={{ startDate: "2026-01-01", endDate: "2026-12-31", days, weekStartsOn: 0 }}
57
+ formatDay={(date, value) =>
58
+ value === null ? t("profile.activity.unknown", { date }) : t("profile.activity.day", { date, count: value })}
59
+ />
60
+ ```
61
+
62
+ ## 축과 기본값
63
+
64
+ | prop | 값 | 기본값 | 설명 |
65
+ | --- | --- | --- | --- |
66
+ | `label` | `string` | 필수 | 영역 접근성 이름 |
67
+ | `descriptor` | `{ startDate, endDate, days, thresholds?, weekStartsOn? }`(`ActivityHeatmapDescriptor`) | 필수 | 날짜는 `YYYY-MM-DD`, 범위 1~366일 |
68
+ | `formatDay` | `(date: string, value: number \| null) => string` | 필수 | 칸 접근성 이름·hover `title`·list 문구. `null`은 미상 |
69
+ | `view` | `grid` · `list` | `grid` | list는 같은 정보를 보이는 문구로 나열한다. 전환 버튼은 제품이 [Button](button.md)으로 둔다 |
70
+ | descriptor `thresholds` | 양수이고 증가하는 세 수 `[number, number, number]` | `[1, 3, 7]` | 0보다 크면 4단계 세기로 나뉜다 |
71
+ | descriptor `weekStartsOn` | `1`(월요일) · `0`(일요일) | `1` | — |
72
+ | descriptor `days` | `{ date: string, value: number }[]`(`ActivityDay`) | — | 목록에 없는 날은 `formatDay`에 `null`(미상, 점선 테두리)로 온다. 명시한 `0`은 0이다. 둘 다 중립 면 색이다 |
73
+ | Web `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 배치 전용. Native는 없다(바깥 wrapper) |
74
+
75
+ ## 배치
76
+
77
+ | 항목 | 값 | 근거 |
78
+ | --- | --- | --- |
79
+ | 크기 | 칸 16×16(Web `1rem`, Native `spacing.md`), 7행(요일) × 주 수 열. 366일이면 53열 × 20 − 4 = 1056 폭이다. 칸은 터치 대상이 아니다(44 미만) | `react/src/activity-heatmap.tsx`, `react-native/src/activity-heatmap.tsx` |
80
+ | 간격 | 칸 사이 `spacing.xxs` 4. 위 제목·범례 블록과는 `layout.contentGap` 16 | 같은 파일, `foundations.ts` `layout` |
81
+ | 순서·정렬 | 제목·범례·보기 전환 버튼은 제품이 Heatmap 위에 둔다. 칸은 위→아래 요일, 시작→끝 주 순서다 | `design-contracts/src/activity-heatmap.ts`(`row`·`column`) |
82
+ | 고정·스크롤 | 화면 폭보다 넓으면 컴포넌트 자체가 가로 스크롤한다(Web `overflow-x: auto` 영역, Native `ScrollView horizontal`). 바깥에서 폭을 줄이거나 칸 크기를 덮지 않는다 | 같은 renderer 파일 |
83
+ | 좁은 폭·큰 글자 | 칸 크기는 글자 크기를 따라 커지지 않는다. 날짜별 행동·큰 글자 사용자에게는 `view="list"` 전환을 제공한다 | 같은 renderer 파일 |
84
+
85
+ ## 꼭 지킬 것
86
+
87
+ - `label`과 `formatDay`는 i18n 문구로 넣고 `value === null`을 따로 처리한다. 색만으로 값을 전하지 않는다.
88
+ - 날짜는 `YYYY-MM-DD`이고 범위 안에서 고유해야 한다. 값은 유한한 0 이상. 어기면 렌더 중 throw다.
89
+ - 세기 색은 테마의 brand semantic 색이 정한다. 제공자별 색·데이터 fetch는 들어 있지 않다(제품 소유).
90
+ - 배치는 Web `layoutStyle`, Native는 바깥 wrapper로 한다. 칸 크기·색을 덮는 스타일 통로는 없다.
91
+
92
+ ## 플랫폼 차이
93
+
94
+ | 항목 | Web | Native |
95
+ | --- | --- | --- |
96
+ | grid 넘침 | 가로 스크롤 region(`tabIndex=0`) | 가로 `ScrollView` |
97
+ | 값 확인 | 셀 접근성 이름 + hover `title` | 셀 접근성 이름 |
98
+ | list 길이 | 페이지 흐름 | 세로 `ScrollView`는 host가 감싼다 |
99
+ | 배치 prop | `layoutStyle` | 없음(바깥 wrapper) |
100
+
101
+ ## 함정
102
+
103
+ - grid 셀은 읽기 전용이라 눌러서 상세로 가는 동작이 없다. 셀 단위 상호작용이 필요하면 계약 공백으로 올린다.
104
+ - 실기기 렌더와 VoiceOver 청취 순회는 아직 검증되지 않았다([계약 문서](../../activity-heatmap.md)).
@@ -0,0 +1,86 @@
1
+ # Affix
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Affix — Web 상단 고정](../../affix.md), `affixRecipe`(`src/affix.ts`)
9
+ - 스토리북: `배포/컴포넌트/기반 기능/고정 배치`
10
+
11
+ ## 언제 쓰나
12
+
13
+ Web에서 스크롤하는 동안 요약·필터·저장 버튼 같은 작은 영역을 가장 가까운 스크롤 조상의 상단에
14
+ 붙여 두고, 부모가 끝나면 함께 풀리게 할 때 쓴다. CSS sticky라 원래 DOM 자리와 초점·입력값이 유지된다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 화면 하단에 고정된 주 행동 | [BottomCTA](bottom-cta.md) |
21
+ | 화면 상단 앱 바·제목 | [TopBar](top-bar.md), [Top](top.md) |
22
+ | 페이지 안 섹션 목차 | [Anchor](anchor.md) |
23
+ | 떠 있는 주 행동 | [FloatingActionButton](floating-action-button.md) |
24
+ | Native 화면 | 없음. Native renderer에는 Affix가 없다 |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `Affix` | 기본(root entry에 없다) | `@hjmds/react/affix` | 없음 |
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { Affix } from "@hjmds/react/affix";
37
+ import { Button } from "@hjmds/react/actions";
38
+
39
+ <Affix offset={16} onChange={setPinned}>
40
+ <Button onClick={save}>{t("form.save")}</Button>
41
+ </Affix>
42
+ ```
43
+
44
+ Native 사용 예는 없다(renderer 없음).
45
+
46
+ ## 축과 기본값
47
+
48
+ | prop | 값 | 기본값 | 설명 |
49
+ | --- | --- | --- | --- |
50
+ | `offset` | 유한한 0 이상의 CSS px | `0` | 스크롤 조상 상단에서 띄울 거리. 아니면 `TypeError` |
51
+ | `disabled` | `boolean` | `false` | 고정을 끄고 일반 흐름으로 둔다. 자식은 재마운트되지 않는다 |
52
+ | `onChange` | `(affixed: boolean) => void` | — | 최초 측정 결과와 이후 붙음↔풀림 전환 때만 호출된다(스크롤마다 호출하지 않는다) |
53
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | sticky 상자의 배치 전용. `position`·`top`은 Affix가 소유한다 |
54
+ | `children` | `ReactNode` | 필수 | 고정할 영역 |
55
+
56
+ ## 배치
57
+
58
+ | 항목 | 값 | 근거 |
59
+ | --- | --- | --- |
60
+ | 크기 | 자식이 정한다. Affix는 여백·높이를 더하지 않고 화면 배경(`--hjm-color-bg`)만 깔아 아래 내용을 가린다 | `.hjm-affix` |
61
+ | 간격 | 스크롤 조상 상단에서 `offset`만큼 띄운다. 상단 바가 있는 화면은 그 높이를 준다(스토리는 `offset={8}`) | `react/src/affix.tsx`, `showcase/web/src/patterns/WebAdditions.stories.tsx` |
62
+ | 순서·정렬 | 고정하려는 영역이 원래 있던 자리(DOM 순서)에 그대로 둔다. z-index를 지정하지 않으므로 뒤에 오는 positioned 요소와 겹치면 바깥 wrapper에서 쌓임 순서를 정한다(`layer.sticky` 100) | `react/src/affix.tsx`, `foundations.ts` `layer` |
63
+ | 고정·스크롤 | 상단 sticky만 된다. 고정 구간은 부모 높이 안에서만 생기고 부모가 끝나면 함께 밀려 올라간다. 한 스크롤 영역에 Affix를 여러 개 두지 않는다 | `affixRecipe`(`edge: "top"`, `position: "sticky"`) |
64
+ | 좁은 폭·큰 글자 | 자식이 스크롤 영역 높이 − `offset`보다 크면 고정하지 않는다(`data-oversize`). 좁은 폭·큰 글자에서는 자식을 한 줄 요약으로 줄인다 | `affixRecipe.oversize: "flow"` |
65
+
66
+ ```text
67
+ 스크롤 영역
68
+ ┌──────────────────────────────┐
69
+ │ TopBar (고정, 높이 = offset) │
70
+ ├──────────────────────────────┤ ← offset
71
+ │ [필터 요약] [저장] │ ← Affix(붙은 상태)
72
+ │ ↑ 스크롤 콘텐츠 │
73
+ └──────────────────────────────┘
74
+ ```
75
+
76
+ ## 꼭 지킬 것
77
+
78
+ - 부모에 스크롤할 공간이 있어야 고정 구간이 생긴다. 부모의 overflow 설정이 sticky 기준을 바꾼다.
79
+ - 자식의 의미·접근성 이름은 제품이 소유한다. Affix는 role이나 알림을 더하지 않는다.
80
+ - `className`·`style` prop이 없다. 배치는 `layoutStyle`이나 `offset`으로 한다(`position`·`top`은 덮을 수 없다).
81
+ - 상단 고정만 된다. 하단 고정·portal·여러 sticky 영역 충돌 조정은 없다.
82
+
83
+ ## 함정
84
+
85
+ - 콘텐츠가 스크롤 영역 높이에서 `offset`을 뺀 값보다 크면 고정이 풀리고 일반 흐름으로 돌아간다
86
+ (`data-oversize`). 고정이 안 된다고 보이면 콘텐츠 높이부터 확인한다.
@@ -0,0 +1,129 @@
1
+ # Agreement
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Agreement contract](../../agreement.md), `agreementRecipe`·`resolveAgreementState`(`src/agreement.ts`)
9
+ - 스토리북: `배포/컴포넌트/입력/약관 동의`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 가입·결제·서비스 시작 앞의 약관 동의 묶음에 쓴다. 전체 동의 한 줄, 필수/선택 항목, 항목별 전문
14
+ 보기가 한 덩어리로 움직이고, 제출 가능 여부(`satisfied`)를 HJM이 판정한다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 법적 의미 없는 여러 선택 | [CheckboxGroup](checkbox-group.md) |
21
+ | 동의 하나를 켜고 끄는 설정(마케팅 수신 등) | [Switch](switch.md), [Checkbox](checkbox.md) |
22
+ | 로그인 화면 하단의 "계속하면 동의" 고지 | [AuthScreenLayout](auth-screen-layout.md)의 `footer` |
23
+ | 약관 전문 표시 | 제품 화면·[Sheet](sheet.md)·[Link](link.md) (Agreement는 여는 경로만 가진다) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `Agreement` | 기본 | `@hjmds/react`, `/agreement` | `@hjmds/react-native`, `/agreement` |
30
+ | `AgreementDescriptor`·`AgreementState` 타입 | 보조(입력·파생 상태) | `@hjmds/design-contracts/components/agreement` | 같음 |
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { Agreement } from "@hjmds/react/agreement";
37
+
38
+ <Agreement
39
+ descriptor={{
40
+ accessibilityLabel: t("signup.terms.group"),
41
+ allLabel: t("signup.terms.all"),
42
+ items: [
43
+ { id: "tos", label: t("signup.terms.tos"), required: true, detail: { label: t("common.view"), href: "/legal/terms" } },
44
+ { id: "marketing", label: t("signup.terms.marketing") },
45
+ ],
46
+ }}
47
+ requiredLabel={t("signup.terms.required")}
48
+ optionalLabel={t("signup.terms.optional")}
49
+ onStateChange={(state) => setCanSubmit(state.satisfied)}
50
+ />
51
+ ```
52
+
53
+ ```tsx
54
+ // Native
55
+ import { Agreement } from "@hjmds/react-native/agreement";
56
+
57
+ <Agreement
58
+ descriptor={descriptor}
59
+ requiredLabel={t("signup.terms.required")}
60
+ optionalLabel={t("signup.terms.optional")}
61
+ onDetail={(id) => openTermsSheet(id)}
62
+ onStateChange={(state) => setCanSubmit(state.satisfied)}
63
+ />
64
+ ```
65
+
66
+ ## 축과 기본값
67
+
68
+ | prop | 값 | 기본값 | 설명 |
69
+ | --- | --- | --- | --- |
70
+ | `descriptor` | `{ accessibilityLabel, allLabel, items }`(`AgreementDescriptor`) | 필수 | 문구는 모두 i18n |
71
+ | 항목 | `{ id, label, description?, disabled?, required?, detail?: { label, href? } }` | — | 필수이면서 `disabled`인 항목은 throw |
72
+ | 항목 `required` | `boolean` | `false`(선택) | 필수 항목이 하나라도 비면 `satisfied`가 `false`다 |
73
+ | 전체 동의 | tri-state(파생) | — | 저장되는 값이 아니라 개별 항목에서 파생한다. 비활성 항목은 분모에서 빠진다 |
74
+ | `checkedIds` · `defaultCheckedIds` | `ReadonlySet<Id>` | 비제어 빈 Set | 제어·비제어. 목록에서 빠진 항목의 체크는 자동으로 버려진다(`reconcileAgreementSelection`) |
75
+ | `onCheckedIdsChange` | `(ids: ReadonlySet<Id>) => void` | — | 체크 목록이 바뀔 때 |
76
+ | `onStateChange` | `(state: AgreementState) => void`, `state` = `{ all: boolean \| "mixed", satisfied: boolean, missingRequiredIds: readonly Id[] }` | — | 마운트 때 초기 상태로 한 번, 그 뒤 체크를 바꿀 때마다 호출된다(마운트 호출은 미게시(1.12.1 이후)) |
77
+ | `onDetail` | `(id: Id) => void` | — | 전문 보기. Web은 `detail.href`가 없을 때만 호출 |
78
+ | `requiredLabel` · `optionalLabel` | `string` | 필수 | 행 끝 "(필수)"·"(선택)" 문구 |
79
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 배치 전용 |
80
+ | Native `style` | — | — | deprecated — `layoutStyle` 또는 recipe(개발 모드 1회 경고, 다음 major 제거) |
81
+
82
+ ## 배치
83
+
84
+ | 항목 | 값 | 근거 |
85
+ | --- | --- | --- |
86
+ | 크기 | 폭을 꽉 채운다. 전체 동의 줄 최소 44(`control.minTouchTarget`), 항목 줄 최소 44, [전문 보기] 높이 44, 체크 표시 16×16(`spacing.md`) | `agreementRecipe`, `collectionItemContract`, `.hjm-agreement__mark` |
87
+ | 간격 | 전체 동의 ↔ 목록 `spacing.xs` 8. 전체 동의 안쪽 위아래 `spacing.sm` 12 · 좌우 `spacing.md` 16, 배경 `canvas`, 모서리 `radius.md` 12. 항목 좌우 `spacing.sm` 12, 체크↔라벨 `spacing.sm` 12, [전문 보기] 좌우 `spacing.xs` 8 | `agreementRecipe`, `.hjm-agreement*` |
88
+ | 순서·정렬 | 가입·결제 화면에서 입력 필드 아래, 제출 버튼 바로 위. 안쪽은 [전체 동의] → 항목 목록, 항목은 체크·라벨이 시작 쪽, [전문 보기]가 끝 쪽, 설명은 라벨 아래 줄. 스토리는 Top → TextField → Agreement → 남은 필수 항목 안내(`role="status"`) → Button을 `Stack gap="md"`(16)로 쌓는다 | `showcase/web/src/patterns/Agreement.stories.tsx` |
89
+ | 고정·스크롤 | Agreement는 스크롤 본문에 둔다. 하단에 고정할 제출 버튼은 [BottomCTA](bottom-cta.md)로 두고 `satisfied`가 `false`면 비활성 | 같은 스토리 |
90
+ | 좁은 폭·큰 글자 | 라벨이 줄바꿈되고(`overflow-wrap: anywhere`) [전문 보기]는 끝 쪽에 남는다 | `.hjm-agreement__copy` |
91
+
92
+ ```text
93
+ ┌──────────────────────────────┐
94
+ │ Top: 제목·설명 │
95
+ │ [이메일 TextField] │
96
+ │ ┌──────────────────────────┐ │
97
+ │ │ ☐ 전체 동의하기 │ │ ← all(canvas 배경)
98
+ │ └──────────────────────────┘ │
99
+ │ ☐ 이용약관 (필수) 전문 보기 │
100
+ │ ☐ 마케팅 (선택) 전문 보기 │
101
+ │ 설명 한 줄 │
102
+ │ 남은 필수 항목 안내(status) │
103
+ │ [ 가입하고 시작하기 ] │ ← primary, satisfied 전 disabled
104
+ └──────────────────────────────┘
105
+ ```
106
+
107
+ ## 꼭 지킬 것
108
+
109
+ - 제출 버튼은 `onStateChange`로 받은 `satisfied`만 읽는다. 남은 필수 항목 안내는 `missingRequiredIds`로 제품이 문장을 만든다.
110
+ - `accessibilityLabel`·`allLabel`·항목 `label`·`requiredLabel`·`optionalLabel`은 모두 제품의 i18n 문구다.
111
+ 빈 문자열, 빈 목록, 중복 id, 필수이면서 `disabled`인 항목은 throw다.
112
+ - 약관 문구·링크 주소·법적 유효성·동의 기록 저장은 제품 소유다. HJM은 판정과 배치만 가진다.
113
+ - 전문 보기 컨트롤은 체크박스와 별개이며 눌러도 체크되지 않는다. 같은 행에 다른 누름 영역을 덧붙이지 않는다.
114
+ - 색·간격은 recipe가 정한다. 배치는 `layoutStyle`로만 한다. Native `style`은 deprecated다([소비 정책 §3](../../consumer-policy.md)).
115
+
116
+ ## 플랫폼 차이
117
+
118
+ | 항목 | Web | Native |
119
+ | --- | --- | --- |
120
+ | 전문 열기 | `detail.href`가 있으면 `<a>`, 없으면 버튼이 `onDetail(id)` 호출 | `href`를 쓰지 않고 항상 `onDetail(id)` |
121
+ | 배치 prop | `layoutStyle`(+`className`) | `layoutStyle`(`style`은 deprecated) |
122
+ | `ref` | root `div`로 전달 | 없음 |
123
+
124
+ ## 함정
125
+
126
+ - 1.12.1까지 `onStateChange`는 사용자가 체크를 바꿀 때만 호출돼, `defaultCheckedIds`로 필수 항목을 미리 채운 경우 첫
127
+ `satisfied`를 받지 못했다. 미게시(1.12.1 이후) 버전은 마운트 때 초기 상태를 한 번 알린다. 1.12.1에서는 제출 버튼의 첫 상태를
128
+ `resolveAgreementState(descriptor, checkedIds)`(`@hjmds/design-contracts/components/agreement`)로 직접 계산한다.
129
+ 호출 횟수를 세는 코드는 마운트 1회가 늘어난다.
@@ -0,0 +1,130 @@
1
+ # AlertDialog
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: `AlertDialogRequest`·`createAlertDialogSession`(`src/alert-dialog.ts`), recipe `alertDialogRecipe`, Popover와의 경계 [ConfirmPopover 결정](../../confirm-popover.md), Native 긴 문구 처리 [Dialog](../../dialog.md)
9
+ - 스토리북: `배포/컴포넌트/오버레이/확인 대화상자`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 삭제·결제·탈퇴처럼 되돌릴 수 없는 행동 직전의 확인(`mode="confirm"`)과, 사용자가 반드시 읽고
14
+ 닫아야 하는 짧은 알림(`mode="alert"`)에 쓴다. 비동기 확인 중 중복 누름·닫힘 차단·실패 문구를 세션이 소유한다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 폼·자유 콘텐츠가 들어가는 모달 | [Dialog](dialog.md) |
21
+ | 되돌릴 수 있는 가벼운 확인 | [Popover](popover.md), `InlineConfirm`([Button](button.md) 확장) |
22
+ | 화면을 막지 않는 결과 알림 | [Toast](toast.md) |
23
+ | 화면 안에 남는 안내 | [Notice](notice.md) |
24
+ | 아래에서 올라오는 선택지 | [Sheet](sheet.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `AlertDialog` | 기본 | `@hjmds/react`, `/overlays` | `@hjmds/react-native`, `/overlays` |
31
+ | `AlertDialogRequest` 타입 | 보조(문구·행동 descriptor) | `@hjmds/design-contracts/components/alert-dialog` | 같음 |
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { AlertDialog } from "@hjmds/react/overlays";
38
+
39
+ <AlertDialog
40
+ open={open}
41
+ onOpenChange={(next) => setOpen(next)}
42
+ request={{
43
+ mode: "confirm",
44
+ tone: "danger",
45
+ title: t("post.delete.title"),
46
+ description: t("post.delete.body"),
47
+ confirmLabel: t("post.delete.confirm"),
48
+ cancelLabel: t("common.cancel"),
49
+ onConfirm: () => deletePost(id),
50
+ fallbackErrorMessage: t("post.delete.failed"),
51
+ }}
52
+ />
53
+ ```
54
+
55
+ ```tsx
56
+ // Native
57
+ import { AlertDialog } from "@hjmds/react-native/overlays";
58
+
59
+ <AlertDialog
60
+ open={open}
61
+ onOpenChange={(next) => setOpen(next)}
62
+ onResult={(result) => { if (result.outcome === "confirmed") router.back(); }}
63
+ request={request}
64
+ />
65
+ ```
66
+
67
+ ## 축과 기본값
68
+
69
+ | prop | 값 | 기본값 | 설명 |
70
+ | --- | --- | --- | --- |
71
+ | `request` | `{ mode, tone?, title, description, confirmLabel, cancelLabel?, onConfirm?, fallbackErrorMessage?, resolveErrorMessage? }`(`AlertDialogRequest`) | 필수 | `mode`에 따라 허용 key가 갈린다(아래) |
72
+ | `request.mode` | `alert` · `confirm` | — | alert는 확인 버튼 하나, confirm은 취소 + 확인(`cancelLabel` 필수) |
73
+ | `request.tone` | `attention` · `info` · `success` · `danger` | `attention` | `danger`는 `confirm` 모드에서만 허용된다(타입으로 막힌다) |
74
+ | `request.onConfirm` | `() => void \| Promise<void>` | — | confirm 모드만. 주면 `fallbackErrorMessage: string`도 필수, 선택 `resolveErrorMessage: (error: unknown) => string` |
75
+ | `open` · `defaultOpen` | `boolean` | 비제어 `false` | 제어가 기본 |
76
+ | `onOpenChange` | `(open: boolean, detail: { reason: "trigger" \| "confirm" \| "cancel-action" \| "escape" \| "back" \| "programmatic" \| "interrupted" }) => void` | — | 닫힌 이유를 `detail.reason`으로 받는다 |
77
+ | Native `onResult` | `(result: { outcome: "confirmed" } \| { outcome: "cancelled", reason }) => void` | — | 결과 한 번 |
78
+ | 첫 초점 | — | — | `alert`는 확인, `confirm`은 취소 버튼 |
79
+ | 닫기 | — | — | 바깥 누름으로 닫히지 않는다. Escape(Web)·뒤로(Native)는 취소로 끝난다. 처리 중(`busy`)에는 닫을 수 없다 |
80
+ | `request.onConfirm` 실패 | — | — | 열린 채로 `resolveErrorMessage` 또는 `fallbackErrorMessage`를 보여 준다 |
81
+ | Web `trigger` | `ReactNode` | — | 비제어면 필수 |
82
+ | Web `size` | `small` · `medium` · `large` | `medium`(Dialog recipe) | — |
83
+ | Web `modalPriority` | `number` | `0` | 높은 우선순위 모달이 뒤에 열린 낮은 모달 위에서 동작한다 |
84
+ | Native `contentStyle` | 배치 key(margin·width·flex·`alignSelf`)만 | — | 색·radius·padding 등 시각 key는 deprecated(개발 모드 1회 경고, 다음 major에서 배치 key로 좁힘) |
85
+
86
+ ## 배치
87
+
88
+ | 항목 | 값 | 근거 |
89
+ | --- | --- | --- |
90
+ | 크기 | Web `size` `medium` `min(36rem, 100%)` · `small` 28rem · `large` 48rem, 최대 높이 `100dvh − 32`. Native는 recipe 기본 `small`로 최대 폭 320, 폭 100%. 아이콘 원 44(`control.minTouchTarget`). Native 행동 버튼 최소 폭 96 | `.hjm-alert-dialog`, `alertDialogRecipe`·`dialogRecipe.sizes`, `react-native/src/overlays.tsx` |
91
+ | 간격 | 화면 가장자리와 최소 `spacing.md` 16. 안쪽 여백 `spacing.lg` 20(Web·Native small). 요소 사이 Web `spacing.sm` 12 · Native `dialogRecipe.content.gap` `spacing.md` 16. 행동 사이 `spacing.sm` 12, 세로로 쌓이면 `spacing.xs` 8. Native 버튼 좌우 `spacing.md` 16 | `.hjm-overlay`, `.hjm-alert-dialog__actions`, `alertDialogRecipe.actions` |
92
+ | 순서·정렬 | 화면 가운데. 위→아래 [아이콘] → 제목 → 설명 → 오류 문구 → 행동. 행동은 끝 정렬 한 줄 [취소][확인]. 버튼 수는 `mode`가 정한다(alert 1 · confirm 2) | `.hjm-overlay`, `alertDialogRecipe.slots` |
93
+ | 고정·스크롤 | 배경막(`backdrop.modal`)이 화면 전체를 덮고 하단 고정 바보다 위에 쌓인다(Web z-index `layer.modal` 900). 내용이 길면 대화상자 안에서 스크롤한다 | `.hjm-overlay`, `.hjm-alert-dialog` |
94
+ | 좁은 폭·큰 글자 | 폭 < 600(`breakpoint.medium`)이면 행동을 세로로 쌓고 **확인이 위**에 온다(Web DOM은 [취소][확인] 유지). Native는 글자 배율 1.6 이상(`largeTextThreshold`)에서도 쌓는다 | `alertDialogRecipe.actions.stackBelow`·`stackedOrder`, `react-native/src/overlays.tsx` |
95
+
96
+ ```text
97
+ 넓은 폭 폭 < 600 또는 Native 큰 글자
98
+ ┌────────────────────────────┐ ┌──────────────────────┐
99
+ │ (!) │ │ (!) │
100
+ │ 제목 │ │ 제목 │
101
+ │ 설명 │ │ 설명 │
102
+ │ 오류 문구(실패 시) │ │ [ 삭제 ] │ ← confirm(위)
103
+ │ [취소] [삭제]│ │ [ 취소 ] │
104
+ └────────────────────────────┘ └──────────────────────┘
105
+ ```
106
+
107
+ ## 꼭 지킬 것
108
+
109
+ - `title`·`description`·`confirmLabel`·`cancelLabel`·`fallbackErrorMessage`는 i18n 문구로 넣는다. 빈 문자열이면 `TypeError`.
110
+ - `onConfirm`을 주면 `fallbackErrorMessage`도 필수다. 확인 버튼에 별도 Spinner를 얹지 않는다(세션이 `loading`을 그린다).
111
+ - 열림은 제어(`open` + `onOpenChange`)로 두는 것이 기본이다. 비제어 Web은 `trigger`가 필수다.
112
+ Native는 제어/비제어를 마운트 뒤 바꾸면 throw한다.
113
+ - 버튼 색·배치는 tone과 recipe가 정한다. Web은 `layoutStyle`이 없는 제외 대상(위치를 recipe가 고정)이고, `className`으로 색·radius를 덮지 않는다.
114
+ Native `contentStyle`에는 배치 key만 넣는다([소비 정책 §3](../../consumer-policy.md)).
115
+
116
+ ## 플랫폼 차이
117
+
118
+ | 항목 | Web | Native |
119
+ | --- | --- | --- |
120
+ | 여는 요소 | `trigger`(선택, 비제어면 필수) | 없음. `open`/`defaultOpen`으로만 연다 |
121
+ | 결과 콜백 | 없음(`onOpenChange` 사유 `confirm`/`cancel-action`/`escape`) | `onResult({ outcome })` |
122
+ | 아이콘 | `icon` | 없음 |
123
+ | 크기·우선순위·portal | `size`, `modalPriority`, `portalContainer` | 없음(RN `Modal` props 일부를 그대로 받는다) |
124
+ | 초점 복귀 | `returnFocusRef`(없으면 trigger) | `returnFocusRef` |
125
+ | 배치 prop | 없음(`layoutStyle` 제외 대상) | `contentStyle` 배치 key만 |
126
+
127
+ ## 함정
128
+
129
+ - Native에서 매우 긴 확인 문구는 본문이 스크롤되도록 바뀌었지만 큰 글자 실기기 검증은 끝나지 않았다([Dialog](../../dialog.md)).
130
+ 문구를 자르거나 글자 크기 상한으로 피하지 않는다.
@@ -0,0 +1,96 @@
1
+ # Anchor
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Anchor — 같은 문서 안의 목차](../../anchor.md), `anchorRecipe`(`src/anchor.ts`)
9
+ - 스토리북: `배포/컴포넌트/탐색/문서 내 바로가기`
10
+
11
+ ## 언제 쓰나
12
+
13
+ Web의 긴 문서·가이드·약관에서 같은 페이지 안 섹션으로 이동하는 목차에 쓴다. 현재 읽는 섹션을
14
+ 실제 스크롤 위치로 계산해 `aria-current="location"`으로 표시하고, 클릭하면 대상 섹션에 초점을 옮긴다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 다른 페이지·URL로 이동 | [Link](link.md) |
21
+ | 상위 경로 표시 | [Breadcrumb](breadcrumb.md) |
22
+ | 같은 자리의 화면 전환 | [Tabs](tabs.md) |
23
+ | 앱 전역 탐색 | [Sidebar](sidebar.md), [BottomNavigation](bottom-navigation.md) |
24
+ | 키보드 사용자를 본문으로 건너뛰기 | [SkipNav](skip-nav.md) |
25
+ | Native 화면 | 없음. Native renderer에는 Anchor가 없다 |
26
+
27
+ ## 공개 이름과 import
28
+
29
+ | 이름 | 역할 | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `Anchor` | 기본 | `@hjmds/react`, `/anchor` | 없음 |
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { Anchor } from "@hjmds/react/anchor";
38
+
39
+ <Anchor
40
+ label={t("guide.toc")}
41
+ offset={64}
42
+ items={[
43
+ { id: "start", label: t("guide.start") },
44
+ { id: "next", label: t("guide.next") },
45
+ ]}
46
+ />
47
+ // 같은 문서에 <section id="start">, <section id="next">를 둔다.
48
+ ```
49
+
50
+ Native 사용 예는 없다(renderer 없음).
51
+
52
+ ## 축과 기본값
53
+
54
+ | prop | 값 | 기본값 | 설명 |
55
+ | --- | --- | --- | --- |
56
+ | `label` | `string` | 필수 | nav 접근성 이름 |
57
+ | `items` | `{ id: string, label: string }[]` | 필수 | 한 개 이상, id 고유·공백 없음 |
58
+ | `onNavigate` | `(id: string, event: MouseEvent<HTMLAnchorElement>) => void` | — | 기본 동작 전에 호출. `event.preventDefault()`면 HJM 스크롤·초점·history를 건너뛴다 |
59
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 배치 전용 |
60
+ | `orientation` | `vertical` · `horizontal` | `vertical` | 좁은 폭에서 줄바꿈하고 label을 자르지 않는다 |
61
+ | `offset` | 0 이상의 CSS px | `0` | 상단 고정 헤더 아래 남길 거리. 음수·비유한 값은 `RangeError` |
62
+ | `historyMode` | `push` · `replace` · `none` | `push` | URL을 쓰면 안 되는 미리보기는 `none` |
63
+ | `container` | `HTMLElement` · `null` | 생략(문서 스크롤) | `HTMLElement`면 그 영역 안만, `null`이면 ref를 기다리며 관찰하지 않는다 |
64
+
65
+ ## 배치
66
+
67
+ | 항목 | 값 | 근거 |
68
+ | --- | --- | --- |
69
+ | 크기 | 링크 안쪽 여백 `spacing.xs` 8. 링크 높이는 44를 보장하지 않으므로 터치 중심 화면에서는 항목을 줄이거나 [Tabs](tabs.md)를 검토한다 | `anchorRecipe.link`, `.hjm-anchor__link` |
70
+ | 간격 | 항목 사이 `spacing.xxs` 4. 스토리는 Section 제목 아래 `Stack gap="md"`(16)로 [Anchor] → [본문 스크롤 영역]을 쌓는다 | `anchorRecipe.gap`, `showcase/web/src/patterns/Anchor.stories.tsx` |
71
+ | 순서·정렬 | 가리키는 본문 앞에 둔다. 넓은 문서 화면은 `vertical`로 본문 옆 열, 좁은 폭은 `horizontal`로 본문 위. 현재 위치는 세로형 시작 쪽·가로형 아래쪽 2px 선(`stroke.strong`, `border.focus`) | `anchorRecipe.current`, `.hjm-anchor*` |
72
+ | 고정·스크롤 | 스크롤 중에도 보이게 하려면 [Affix](affix.md)로 감싸고, 상단 고정 헤더가 있으면 `offset`을 그 높이로 맞춘다 | `getAnchorCurrentId`(`src/anchor.ts`) |
73
+ | 좁은 폭·큰 글자 | 가로 목차는 줄바꿈되며 가로 스크롤을 만들지 않는다. label은 줄바꿈되고 잘리지 않는다 | `.hjm-anchor[data-orientation="horizontal"] .hjm-anchor__list`(`flex-wrap: wrap`) |
74
+
75
+ ```text
76
+ 넓은 폭 좁은 폭
77
+ ┌──────────┬────────────────────┐ ┌──────────────────────┐
78
+ │ ▍시작하기 │ 본문 섹션(스크롤) │ │ 시작하기 · 다음 단계 │ ← horizontal
79
+ │ 다음 단계│ │ │ ▔▔▔▔▔▔ │
80
+ │ (Affix) │ │ │ 본문 섹션(스크롤) │
81
+ └──────────┴────────────────────┘ └──────────────────────┘
82
+ ```
83
+
84
+ ## 꼭 지킬 것
85
+
86
+ - `label`(nav 접근성 이름)과 항목 `label`은 i18n 문구로 넣는다. 빈 문자열이면 `TypeError`.
87
+ - 항목 `id`는 공백 없는 HTML id이고 문서 안에서 고유해야 한다. 중복·빈 목록은 throw다.
88
+ - 목차의 sticky 배치와 문서 레이아웃은 호스트가 소유한다. 고정이 필요하면 [Affix](affix.md)나 호스트 CSS로 감싼다.
89
+ - 앱 라우터로 처리하려면 `onNavigate(id, event)`에서 `event.preventDefault()`를 호출한다.
90
+ - 배치는 `layoutStyle`로 한다. `className`으로 현재 표시 색·선 굵기를 덮지 않는다([소비 정책 §3](../../consumer-policy.md)).
91
+
92
+ ## 함정
93
+
94
+ - DOM에 대상이 없는 항목은 현재 위치 후보에서 빠지고 기본 href 동작을 한다. 지연 렌더 섹션은 삽입되면 자동으로 관찰된다.
95
+ - modifier 클릭(새 탭 등)은 브라우저 기본 동작에 맡긴다. 스크롤·초점 이동이 일어나지 않는 것이 정상이다.
96
+ - 중첩 트리 목차·제목 자동 수집은 없다. 항목은 호스트가 넘긴다.
@@ -0,0 +1,89 @@
1
+ # AspectRatio
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [AspectRatio contract](../../aspect-ratio.md), recipe `aspectRatioRecipe`(`src/aspect-ratio.ts`)
9
+ - 스토리북: `배포/컴포넌트/레이아웃/화면 비율`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 이미지·동영상·지도처럼 늦게 로드되는 매체의 자리를 미리 잡아 레이아웃 흔들림을 막을 때 쓴다.
14
+ 폭은 부모를 채우고 높이는 비율로 정해진다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 이미지 로드·실패 상태까지 다룸 | [Image](image.md) (비율 틀이 필요하면 AspectRatio 안에 넣는다) |
21
+ | 아이콘·이미지·Lottie를 같은 정사각 액자에 맞춤 | [Asset](asset.md) |
22
+ | 프로필 사진 | [Avatar](avatar.md) |
23
+ | 비율이 아닌 고정 폭 컬럼 | [Grid](grid.md), [Container](container.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `AspectRatio` | 기본 | `@hjmds/react`, `/layout` | `@hjmds/react-native`, `/primitives` |
30
+
31
+ ## 최소 사용 예
32
+
33
+ ```tsx
34
+ // Web
35
+ import { AspectRatio } from "@hjmds/react/layout";
36
+
37
+ <AspectRatio ratio="wide">
38
+ <img src={cover.url} alt={t("post.coverAlt")} style={{ objectFit: "cover" }} />
39
+ </AspectRatio>
40
+ ```
41
+
42
+ ```tsx
43
+ // Native
44
+ import { Image } from "react-native";
45
+ import { AspectRatio } from "@hjmds/react-native/primitives";
46
+
47
+ <AspectRatio ratio="landscape">
48
+ <Image source={{ uri: cover.url }} accessibilityLabel={t("post.coverAlt")}
49
+ style={{ width: "100%", height: "100%" }} resizeMode="cover" />
50
+ </AspectRatio>
51
+ ```
52
+
53
+ ## 축과 기본값
54
+
55
+ | prop | 값 | 기본값 | 설명 |
56
+ | --- | --- | --- | --- |
57
+ | `ratio` | `square`(1) · `portrait`(3/4) · `landscape`(4/3) · `wide`(16/9) 또는 양의 유한수(가로/세로) | `wide` | 0·음수·`Infinity`는 `RangeError`, 모르는 preset 이름은 `TypeError`로 렌더 중에 거절한다. Web은 `data-ratio`에 preset 이름 또는 `custom`을 남긴다 |
58
+ | `children` | `ReactNode` | — | 매체 하나 |
59
+ | Web `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 배치 전용. 비율·`width`는 덮지 않는다 |
60
+ | Native `style` | `ViewProps["style"]` | — | layout primitive라 deprecated가 아니다. 배치 key만 넣는다 |
61
+
62
+ ## 배치
63
+
64
+ | 항목 | 값 | 근거 |
65
+ | --- | --- | --- |
66
+ | 크기 | 폭은 항상 부모를 꽉 채우고(`width: 100%`) 높이는 비율에서 나온다. AspectRatio에 높이를 주지 않는다. 자식은 틀을 100%×100%로 채운다 | `aspectRatioRecipe.sizing`, `.hjm-aspect-ratio`, `react-native/src/primitives.tsx` |
67
+ | 간격 | 자체 여백이 없다. 카드 안 썸네일이면 카드의 안쪽 간격을 따른다 | `.hjm-aspect-ratio` |
68
+ | 순서·정렬 | 카드·상세의 맨 위 매체 자리에 두고 제목·본문이 아래로 이어진다. 모서리는 감싸는 Card나 자식이 정한다 | `aspectRatioRecipe.slots` |
69
+ | 고정·스크롤 | 고정 영역이 없다 | — |
70
+ | 좁은 폭·큰 글자 | 비율은 그대로이고 높이만 줄어든다. 세로가 긴 비율(`portrait`, `9 / 16`)은 화면 높이를 넘지 않도록 부모 폭을 제한한다 | `aspectRatioRecipe.ratios` |
71
+
72
+ ## 꼭 지킬 것
73
+
74
+ - 대체 텍스트, `object-fit`/`resizeMode`, 모서리, 로딩·오류 표시는 자식과 제품이 소유한다. 틀은 비율만 보장한다.
75
+ - 숫자 비율은 가로/세로다. 9:16 세로 영상은 `9 / 16`이다.
76
+
77
+ ## 플랫폼 차이
78
+
79
+ | 항목 | Web | Native |
80
+ | --- | --- | --- |
81
+ | 구현 | CSS `aspect-ratio` | `ViewStyle.aspectRatio` + `width: "100%"` |
82
+ | 자식 크기 | CSS가 직계 자식을 틀 크기로 늘린다 | 늘리지 않는다. 자식에 `width/height: "100%"`나 `flex: 1`을 준다 |
83
+ | 추가 props | `div` 속성 전체(`className`, `style`) + `layoutStyle` | `View` 속성 전체(`style`). `layoutStyle` 없음 |
84
+
85
+ ## 함정
86
+
87
+ - Native에서 `Image`에 크기를 주지 않으면 틀만 잡히고 그림이 보이지 않는다(위 표).
88
+ - Web `style`에 `aspectRatio`를 넣으면 `ratio`를 덮는다(사용자 `style`이 뒤에 펼쳐진다). 비율은 `ratio`로만 주고 배치는 `layoutStyle`로 준다.
89
+ `layoutStyle`은 `aspectRatio`보다 먼저 펼쳐지므로 비율을 덮지 못한다.