@hjmds/design-contracts 1.12.0 → 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,125 @@
1
+ # BottomCTA
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [화면 제목과 마지막 행동](../../screen-chrome.md), recipe `bottomCtaRecipe`(`src/component-recipes.ts`)
9
+ - 스토리북: `배포/컴포넌트/동작/하단 실행 버튼`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 화면의 결론 행동(저장·다음·결제·가입)을 본문 아래 하단 영역에 둘 때 쓴다. 주 행동 하나,
14
+ 선택적인 보조 행동 하나, 그 위의 짧은 설명 한 줄을 담는다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 본문 중간의 일반 행동 | [Button](button.md) |
21
+ | 목록 위에 떠 있는 "새 항목" 행동 | [FloatingActionButton](floating-action-button.md) |
22
+ | 주 행동 아래 붙는 약관·수수료 같은 상시 조건 | [BottomInfo](bottom-info.md) |
23
+ | 최상위 화면 사이 이동 | [BottomNavigation](bottom-navigation.md) |
24
+ | 지금 생긴 오류·성공 알림 | [Notice](notice.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `BottomCTA` | 기본 | `@hjmds/react`, `/bottom-cta` | `@hjmds/react-native`, `/actions`, `/bottom-cta` |
31
+
32
+ Native의 `/bottom-cta`는 `/actions` 모듈의 alias다(번들 감소 아님).
33
+
34
+ ## 최소 사용 예
35
+
36
+ ```tsx
37
+ // Web
38
+ import { BottomCTA } from "@hjmds/react/bottom-cta";
39
+
40
+ <BottomCTA
41
+ position="sticky"
42
+ description={t("checkout.feeNotice")}
43
+ primaryAction={{ label: t("checkout.pay"), onClick: pay, loading: paying }}
44
+ secondaryAction={{ label: t("common.cancel"), onClick: cancel }}
45
+ />
46
+ ```
47
+
48
+ ```tsx
49
+ // Native
50
+ import { BottomCTA } from "@hjmds/react-native/bottom-cta";
51
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
52
+
53
+ const insets = useSafeAreaInsets();
54
+ <BottomCTA
55
+ safeAreaBottom={insets.bottom}
56
+ primaryAction={{ label: t("checkout.pay"), onPress: pay, loading: paying }}
57
+ secondaryAction={{ label: t("common.cancel"), onPress: cancel }}
58
+ />
59
+ ```
60
+
61
+ ## 축과 기본값
62
+
63
+ | prop | 값 | 기본값 | 설명 |
64
+ | --- | --- | --- | --- |
65
+ | `primaryAction` | Web `{ label, onClick, accessibilityLabel?, disabled?, loading?, loadingLabel?, size?, tone? }` · Native `{ label, onPress, accessibilityLabel?, accessibilityHint?, disabled?, loading?, loadingLabel?, size?, tone? }` | 필수, tone `primary`, size `medium` | Web `onClick: (event: MouseEvent<HTMLButtonElement>) => void`, Native `onPress: (event: GestureResponderEvent) => void` |
66
+ | `secondaryAction` | 같은 action 객체 또는 제품이 만든 `ReactNode` | tone `secondary` | 객체면 HJM Button으로 그린다. Web은 `label`과 `onClick`이 있어야 객체로 본다 |
67
+ | `description` | `string` | — | 행동 위 caption 한 줄 |
68
+ | `accessibilityLabel` | `string` | — | 바 전체(Web `role="group"`, Native `toolbar`)의 이름 |
69
+ | `safeAreaBottom` | 0 이상의 유한수 | `0` | 음수·무한대는 `RangeError` |
70
+ | Web `position` | `flow` · `sticky` | `flow` | fixed는 제공하지 않는다 |
71
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. Native `style`은 deprecated — layoutStyle 또는 tone/토큰 |
72
+ | 큰 글자 | — | — | 두 행동을 세로로 쌓고 **주 행동을 위**에 둔다(`column-reverse`, Native `textScale >= 1.6`, Web `isLargeTextScale`) |
73
+
74
+ ## 배치
75
+
76
+ | 항목 | 값 | 근거 |
77
+ | --- | --- | --- |
78
+ | 크기 | 화면 폭을 꽉 채우는 하단 바. 최소 높이 64(Native는 64 + `safeAreaBottom`). 행동 버튼은 칸을 꽉 채우고 높이는 Button `size`를 따른다(기본 medium 44) | `bottomCtaRecipe.minHeight`, `.hjm-bottom-cta__actions .hjm-button`, `react-native/src/actions.tsx` |
79
+ | 간격 | 좌우 `layout.pagePadding.regular` 20, 위 `spacing.sm` 12, 아래 `spacing.sm` 12와 하단 inset 중 큰 값(Web은 `env(safe-area-inset-bottom)`·`safeAreaBottom`도 비교). 설명↔행동, 행동 사이 `spacing.sm` 12. 위 경계는 `border.default` 1px(`stroke.default`), Native는 위로 드리우는 그림자(opacity 0.08, radius 8) | `bottomCtaRecipe`, `.hjm-bottom-cta` |
80
+ | 순서·정렬 | 위→아래 [설명 한 줄(caption, muted)] → 행동 줄. 행동 줄은 [보조][주] 순서로 같은 폭을 나눈다. 주 행동은 하나다 | `react/src/bottom-cta.tsx`, `react-native/src/actions.tsx` |
81
+ | 고정·스크롤 | 본문 스크롤 영역 아래에 붙는다. Web은 `flow`(문서 흐름 끝) 또는 `sticky`(아래 0에 붙음, z-index `layer.sticky` 100). Native는 제품 화면 레이아웃이 스크롤 영역 밖 아래에 둔다. 키보드가 열리는 폼은 `KeyboardAvoiding` 또는 `KeyboardDock`으로 감싼다 | `.hjm-bottom-cta[data-position="sticky"]`, `react-native/src/keyboard-controller.tsx` |
82
+ | 좁은 폭·큰 글자 | Web은 행동 칸 기준 폭 132(`control.minTouchTarget` × 3)보다 좁으면 줄바꿈한다. 큰 글자에서는 세로로 쌓고 주 행동이 위에 온다 | `.hjm-bottom-cta__actions > div`, `.hjm-bottom-cta[data-large-text="true"]` |
83
+
84
+ ```text
85
+ ┌──────────────────────────────┐
86
+ │ 스크롤 본문 │
87
+ │ … │
88
+ ├──────────────────────────────┤ ← border 1px (Native 그림자)
89
+ │ 설명 한 줄(caption) │
90
+ │ [ 보조 ] [ 주 행동 ] │ ← 같은 폭, 사이 spacing.sm 12
91
+ │ ░░ 하단 안전 영역 ░░ │ ← max(spacing.sm, inset)
92
+ └──────────────────────────────┘
93
+ 큰 글자: [ 주 행동 ] / [ 보조 ] (주 행동 위)
94
+ ```
95
+
96
+ ## 꼭 지킬 것
97
+
98
+ - action `label`은 i18n 키로 넣고 비우지 않는다. Web은 빈 label에 `TypeError`를 던진다.
99
+ - 진행 중은 action의 `loading`으로 표시한다. 버튼 위에 Spinner를 따로 겹치지 않는다.
100
+ - 주 행동은 하나다. 세 번째 행동이 필요하면 화면 구조를 다시 본다.
101
+ - 폼 제출을 BottomCTA로 옮기면 [Form](form.md)의 제출 버튼과 중복하지 않는다. Web은 Form `actions`에 submit Button을 넣지 않고,
102
+ Native Form은 내장 제출 버튼(`submitLabel` 필수)을 항상 그리므로 Form 대신 필드 + contracts `createFormSubmitSession`
103
+ (`@hjmds/design-contracts/components/form`)으로 제출 세션을 잡고 그 상태를 `primaryAction.loading`에 연결한다.
104
+ - 배치는 `layoutStyle`로만 한다. Native `style`은 deprecated(개발 모드 1회 경고, 다음 major 제거)다.
105
+ - 하단 inset을 넘긴다. Native는 자동으로 읽지 않으므로 `safeAreaBottom`이 없으면 홈 인디케이터에 붙는다.
106
+ - 키보드가 열리는 폼 화면(Native)은 `KeyboardAvoiding`(`/keyboard`) 또는 `KeyboardDock`(`/keyboard-controller`,
107
+ optional peer `react-native-keyboard-controller` 설치 필요)로 감싼다.
108
+
109
+ ## 플랫폼 차이
110
+
111
+ | 항목 | Web | Native |
112
+ | --- | --- | --- |
113
+ | 이벤트 | `onClick` | `onPress` |
114
+ | 위치 | `position="sticky"`로 문서 흐름 안 고정 | 제품의 화면 레이아웃이 배치 |
115
+ | 하단 inset | `env(safe-area-inset-bottom)`과 `safeAreaBottom` 중 큰 값 | `safeAreaBottom`(과 recipe padding 중 큰 값)만 |
116
+ | `loadingLabel` 타입 | `string`(접근성 이름) | `ReactNode` |
117
+ | `accessibilityHint` | 없음 | 있음 |
118
+ | root 역할 | `role="group"` | `accessibilityRole="toolbar"` |
119
+ | 그 밖 | HTML 속성·`className`·`style` 전달 | `testID`(`style`은 deprecated) |
120
+ | 빈 `label` | `TypeError` | 검사 없음(Button이 빈 이름으로 그려진다) |
121
+
122
+ ## 함정
123
+
124
+ - Web `BottomCTA`의 `style`은 recipe CSS 변수 뒤에 펼쳐지므로 `--hjm-bottom-cta-*` 변수를 덮을 수 있다. 외형은 recipe 소유이므로 `style`로 변수를 바꾸지 않는다.
125
+ - Native는 하단 inset을 스스로 읽지 않는다. `safeAreaBottom`을 빠뜨려도 오류가 없고 홈 인디케이터에 붙어 보인다.
@@ -0,0 +1,99 @@
1
+ # BottomInfo
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [BottomInfo](../../bottom-info.md), recipe `bottomInfoRecipe`(`src/bottom-info.ts`)
9
+ - 스토리북: `배포/컴포넌트/상태와 알림/하단 안내`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 주 행동 아래에 늘 붙어 있는 작은 조건 문장에 쓴다. "가입하면 약관에 동의하는 것으로 봅니다",
14
+ "수수료는 결제 시점에 확정됩니다" 같은 문장이다. 한 줄은 문장으로, 두 줄 이상은 목록으로 그린다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 지금 생긴 오류·성공·경고 | [Notice](notice.md) |
21
+ | 사용자가 직접 체크해야 하는 동의 | [Agreement](agreement.md) |
22
+ | 입력 하나에 딸린 설명·오류 | [Field](field.md) |
23
+ | 하단 행동 영역 자체 | [BottomCTA](bottom-cta.md) (`description`은 행동 위 한 줄) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `BottomInfo` | 기본 | `@hjmds/react`, `/bottom-info` | `@hjmds/react-native`, `/bottom-info` |
30
+
31
+ ## 최소 사용 예
32
+
33
+ ```tsx
34
+ // Web
35
+ import { BottomInfo } from "@hjmds/react/bottom-info";
36
+
37
+ <BottomInfo
38
+ items={[t("signup.termsNotice"), t("signup.ageNotice")]}
39
+ renderItem={(item, index) => index === 0 ? <TermsSentence text={item} /> : item}
40
+ />
41
+ ```
42
+
43
+ ```tsx
44
+ // Native
45
+ import { BottomInfo } from "@hjmds/react-native/bottom-info";
46
+
47
+ <BottomInfo items={[t("checkout.feeNotice")]} tone="emphasis" />
48
+ ```
49
+
50
+ ## 축과 기본값
51
+
52
+ | prop | 값 | 기본값 | 설명 |
53
+ | --- | --- | --- | --- |
54
+ | `items` | 비지 않은 문자열 배열 | 필수 | 빈 배열·빈 문자열은 거부한다. 2개 이상이면 목록 표식이 붙는다(`listMarkerFrom: 2`) |
55
+ | `tone` | `muted` · `emphasis` | `muted` | 법적 고지처럼 놓치면 안 되는 문장만 `emphasis`. danger·success tone은 없다 |
56
+ | `renderItem` | `(item: string, index: number) => ReactNode` | — | 한 줄을 rich copy로 바꾼다(문장 안 약관 링크 등). `null`·`undefined`를 돌려주면 원래 문장을 그린다 |
57
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. Native `style`은 deprecated — layoutStyle 또는 tone/토큰 |
58
+
59
+ 상태가 없는 표시 컴포넌트다. 이벤트 콜백이 없다.
60
+
61
+ ## 배치
62
+
63
+ | 항목 | 값 | 근거 |
64
+ | --- | --- | --- |
65
+ | 크기 | 폭을 채우는 caption 글자 블록(`typography.caption` 11/16). 높이는 줄 수가 정한다. 누르는 요소가 아니다(문장 안 링크는 `renderItem`이 소유) | `bottomInfoRecipe.textVariant`, `.hjm-bottom-info` |
66
+ | 간격 | 위 `spacing.sm` 12(주 행동과의 간격), 줄 사이 `spacing.xxs` 4, 목록일 때 시작 쪽 들여쓰기 `spacing.md` 16(Web) | `bottomInfoRecipe.paddingTop`·`gap`, `.hjm-bottom-info__list` |
67
+ | 순서·정렬 | 주 행동 **바로 아래**. 주 행동과의 간격은 BottomInfo 자신의 위 여백(`spacing.sm` 12)이므로 둘을 감싸는 Stack에 간격을 더하지 않는다. 한 줄은 문장, 두 줄 이상은 목록 | `bottomInfoRecipe.paddingTop`·`listMarkerFrom` |
68
+ | 고정·스크롤 | 자체 고정이 없다. 하단에 고정한 [BottomCTA](bottom-cta.md) 아래에 둘 때는 그 바와 같은 영역(스크롤 밖)에 넣는다. BottomCTA `description`은 행동 **위** 한 줄로 자리가 다르다 | `../../bottom-info.md` |
69
+ | 좁은 폭·큰 글자 | 줄바꿈되고 자르지 않는다(`overflow-wrap: anywhere`) | `.hjm-bottom-info__item` |
70
+
71
+ ```text
72
+ ┌──────────────────────────────┐
73
+ │ [ 가입하고 시작하기 ] │ ← 주 행동
74
+ │ ↕ spacing.sm 12 │
75
+ │ 가입하면 약관에 동의…(caption)│ ← BottomInfo 한 줄
76
+ │ • 수수료는 결제 시점에 확정 │ ← 2줄 이상은 목록
77
+ │ • 환불은 7일 이내 │
78
+ └──────────────────────────────┘
79
+ ```
80
+
81
+ ## 꼭 지킬 것
82
+
83
+ - 문장은 i18n 키로 넣는다. 자르지 않는다(법적 고지가 많다).
84
+ - 문장 안 링크의 주소·라우팅은 제품 소유다. `renderItem`으로 그 줄만 바꾼다.
85
+ - `items`가 React key로 쓰인다. 같은 문장을 두 번 넣지 않는다.
86
+ - 상태 알림 용도로 쓰지 않는다. `role="status"`가 없어 낭독되지 않는다.
87
+
88
+ ## 플랫폼 차이
89
+
90
+ | 항목 | Web | Native |
91
+ | --- | --- | --- |
92
+ | root | `<aside>`, 여러 줄이면 `<ul>/<li>` | `View` + 줄마다 `·` 표식 텍스트 |
93
+ | 전달 가능 속성 | HTML 속성·`className`·ref·`layoutStyle` | `layoutStyle`(`style`은 deprecated) |
94
+ | `renderItem` 결과 위치 | `<li>`/`<p>` 안 | `Text` 안. 인라인 텍스트(문자열·`Text`·인라인 링크)만 돌려준다 |
95
+
96
+ ## 함정
97
+
98
+ - Native는 `renderItem` 결과를 `Text` 안에 넣는다. `View`를 돌려주면 Text 안 View가 되어 플랫폼마다 배치가 깨진다.
99
+ - 같은 문장이 두 번 들어가면 React key가 겹친다(두 플랫폼 모두 `items` 문자열이 key).
@@ -0,0 +1,136 @@
1
+ # BottomNavigation
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [BottomNavigation](../../bottom-navigation.md), recipe `bottomNavigationRecipe`
9
+ - 스토리북: `배포/컴포넌트/탐색/하단 탐색`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 앱의 안정된 최상위 route(홈·검색·메시지·내 정보) 2~6개 사이를 이동하는 하단 막대에 쓴다.
14
+ 선택 상태는 제품 router가 소유하고, 컴포넌트는 이동 의도만 알린다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 같은 화면 안에서 패널만 바꿈 | [Tabs](tabs.md) |
21
+ | 화면의 결론 행동 | [BottomCTA](bottom-cta.md) |
22
+ | 넓은 화면의 측면 탐색 | [Sidebar](sidebar.md) |
23
+ | 화면 제목과 뒤로 가기 | [TopBar](top-bar.md) |
24
+ | 몇 개 중 하나를 고르는 값 | [SegmentedControl](segmented-control.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `BottomNavigation` | 기본 | `@hjmds/react`, `/navigation` | `@hjmds/react-native`, `/navigation`, `/top-bar` |
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web (Next.js)
36
+ import { BottomNavigation } from "@hjmds/react/navigation";
37
+
38
+ <BottomNavigation
39
+ descriptor={{
40
+ accessibilityLabel: t("nav.main"),
41
+ selectedKey: currentRoute,
42
+ items: [
43
+ { id: "home", label: t("nav.home"), icon: { name: "home" } },
44
+ { id: "inbox", label: t("nav.inbox"), icon: { name: "notifications" },
45
+ badge: { count: unread, accessibilityLabel: t("nav.unread", { count: unread }) } },
46
+ ],
47
+ }}
48
+ getHref={(item) => routes[item.id]}
49
+ renderLink={(props) => <Link {...props} />}
50
+ renderIcon={({ name, size, strokeWidth }) => <AppIcon name={name} size={size} strokeWidth={strokeWidth} />}
51
+ />
52
+ ```
53
+
54
+ ```tsx
55
+ // Native
56
+ import { BottomNavigation } from "@hjmds/react-native/navigation";
57
+
58
+ <BottomNavigation
59
+ descriptor={descriptor}
60
+ safeAreaBottom={insets.bottom}
61
+ onActivate={({ key, reason }) => reason === "reselect" ? scrollToTop(key) : navigation.navigate(key)}
62
+ renderIcon={({ name, color, size, strokeWidth }) => <AppIcon name={name} color={color} size={size} strokeWidth={strokeWidth} />}
63
+ />
64
+ ```
65
+
66
+ ## 축과 기본값
67
+
68
+ | prop | 값 | 기본값 | 설명 |
69
+ | --- | --- | --- | --- |
70
+ | `descriptor` | `{ accessibilityLabel: string; selectedKey: Key; items: { id: Key; label: string; accessibilityLabel?: string; icon: { name: IconName }; badge?: { count: number; max?: number; accessibilityLabel: string }; disabled?: boolean }[] }` | 필수 | 항목 2~6개. `selectedKey`는 router가 확정한 값 |
71
+ | `configuration.presentation` | `bar` · `floating` · `capsule` | `bar` | — |
72
+ | `configuration.distribution` | `equal` · `center-gap` | `equal` | `center-gap`은 짝수 개 항목만, `capsule`과 함께 쓰면 오류다 |
73
+ | `configuration.density` | `regular` · `compact` | `regular` | — |
74
+ | `configuration.direction` | `ltr` · `rtl` | Provider 환경(없으면 `ltr`) | — |
75
+ | `configuration.keyboardBehavior` | `hide` · `remain` | `hide` | 기본은 소프트 키보드가 열리면 막대를 숨긴다 |
76
+ | `primaryAction` | `ReactNode`(Button/IconButton) | — | `configuration` 밖의 별도 prop. 생성 같은 비목적지 행동이며 `center-gap` 또는 `capsule`과 짝짓는다 |
77
+ | 항목 `badge` | `{ count, max?, accessibilityLabel }` | `max` 99 | 숫자만. 점 배지는 없다. `count`가 0이면 그리지 않는다 |
78
+ | `onActivate` | `(activation: { key: Key; reason: "navigate" \| "reselect" }) => void` | Web 선택 · Native 필수 | 이동 의도만 알린다. 선택은 바꾸지 않는다. `reselect`는 이미 선택된 항목을 다시 누른 경우 |
79
+ | Native `onLongActivate` | 같은 시그니처 | — | 길게 누름(tabLongPress 등) |
80
+ | `renderIcon` | `(props: { item; name: IconName; selected: boolean; color: string; size: number; strokeWidth: number }) => ReactNode` | 필수 | Web `color`는 `"currentColor"`이고 `scale: number`가 더 있다 |
81
+ | Web `getHref` | `(item: ResolvedBottomNavigationItemDescriptor) => string` | 필수 | 빈 문자열은 `TypeError` |
82
+ | Web `renderLink` | `(props: AnchorHTMLAttributes & { href: string; children: ReactNode; "data-state": "idle" \| "selected" }) => ReactElement` | 일반 `<a>` | 라우터 Link를 연결한다. props를 그대로 펼친다 |
83
+ | Native `renderBadge` | `(props: { item; badge: { visibleLabel: string; hiddenFromAccessibility: true }; count: number; max?: number; selected: boolean }) => ReactNode` | 기본 배지 | 결과 subtree는 보조기기에서 숨겨진다 |
84
+ | Native `getItemTestID` | `(item) => string \| undefined` | — | — |
85
+ | Native `safeAreaBottom` | 0 이상 숫자 | 0 | recipe 최소 하단 padding에 더한다 |
86
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. Native `style`·`surfaceStyle`·`listStyle`·`primaryActionStyle`은 deprecated — layoutStyle 또는 `configuration` |
87
+
88
+ ## 배치
89
+
90
+ | 항목 | 값 | 근거 |
91
+ | --- | --- | --- |
92
+ | 크기 | 항목 최소 `regular` 56×64 · `compact` 52×52. 표면 최대 폭 `bar` 제한 없음(화면 폭) · `floating` 384 · `capsule` 480. 모서리 `floating` `radius.xl` 24 · `capsule` `radius.full`. 아이콘·배지 자리 40×28 | `bottomNavigationRecipe.density`·`presentations`·`indicator` |
93
+ | 간격 | 항목 안쪽 `regular` `spacing.xs` 8 · `compact` `spacing.xxs` 4, 아이콘↔라벨 `regular` `spacing.xxs` 4 · `compact` 2. `floating`·`capsule`은 바깥 좌우 `spacing.md` 16 · 위 `spacing.xs` 8. 하단은 안전 영역 + `spacing.xs` 8(Native는 `safeAreaBottom`을 최소 padding에 더함). `center-gap` 가운데 빈칸 `control.buttonHeight.large` + `spacing.md` = 68 | `presentations`, `distributions`, `safeArea`, `.hjm-bottom-navigation` |
94
+ | 순서·정렬 | 항목은 논리 순서(RTL이면 뒤집힘)로 같은 폭을 나눈다. 라벨은 아이콘 아래 가운데. `primaryAction`은 막대 정가운데에 겹친다(`center-gap` 빈칸 또는 `capsule`). 배지는 아이콘 끝·위 모서리(`blockStart` −4, `inlineEnd` −8) | `.hjm-bottom-navigation__list`·`__primary-action`, `bottomNavigationRecipe.badge`·`direction` |
95
+ | 고정·스크롤 | Web은 화면 아래 `position: fixed`(z-index `layer.sticky` 100). 본문 마지막 내용이 막대 아래로 들어가므로 제품 레이아웃이 하단 여백을 둔다. Native는 navigator 탭 막대 자리에 제품 레이아웃이 둔다. 키보드가 열리면 기본으로 숨는다 | `.hjm-bottom-navigation`, `bottomNavigationRecipe.adaptive`·`keyboard` |
96
+ | 좁은 폭·큰 글자 | 라벨은 줄바꿈되고 항목 높이가 늘어난다(고정 높이 없음). 글자 배율은 최대 1.4배까지만 커진다 | `bottomNavigationRecipe.largeText`·`label.wrap` |
97
+
98
+ ```text
99
+ bar(기본) center-gap + primaryAction
100
+ ┌──────────────────────────────┐ ┌──────────────────────────────┐
101
+ │ 스크롤 본문(하단 여백 필요) │ │ 스크롤 본문 │
102
+ ├──────────────────────────────┤ ├──────────────────────────────┤
103
+ │ (홈) (검색) (알림) (나) │ │ (홈) (검색) [+] (알림) (나)│
104
+ │ 홈 검색 알림 나 │ │ 홈 검색 ↑68↑ 알림 나 │
105
+ │ ░ 안전 영역 + spacing.xs 8 ░ │ │ ░ 안전 영역 + spacing.xs 8 ░ │
106
+ └──────────────────────────────┘ └──────────────────────────────┘
107
+ floating·capsule: 바깥 좌우 16 · 위 8 띄운 둥근 표면, 최대 폭 384 · 480
108
+ ```
109
+
110
+ ## 꼭 지킬 것
111
+
112
+ - `selectedKey`는 router가 확정한 값으로 넘긴다. `onActivate`는 의도일 뿐이며 컴포넌트가 선택을 바꾸지 않는다.
113
+ - 선택된 항목은 `disabled`일 수 없다. id 중복, 빈·앞뒤 공백 label, 2~6개 밖은 거부한다.
114
+ - icon은 `name`만 넘긴다. 크기·색·굵기는 `renderIcon`이 받은 값을 그대로 적용한다(덮어쓰기 금지).
115
+ - label과 badge `accessibilityLabel`은 i18n 키로 만든다. icon 이름·아이콘 세트는 제품 소유다.
116
+ - 생성 버튼을 항목으로 넣지 않는다. `primaryAction`으로 둔다.
117
+ - 배치는 `layoutStyle`로만 한다. Native의 `style`·`surfaceStyle`·`listStyle`·`primaryActionStyle`은 deprecated(개발 모드 1회 경고, 다음 major 제거)이며 외형은 `configuration`이 소유한다.
118
+
119
+ ## 플랫폼 차이
120
+
121
+ | 항목 | Web | Native |
122
+ | --- | --- | --- |
123
+ | 항목 | `aria-current` 링크(`getHref` 필수, `renderLink`로 라우터 연결) | tab(iOS는 button role) + `onActivate` 필수 |
124
+ | `onActivate` | 선택, 수정키 없는 왼쪽 클릭에만 | 필수, 추가로 `onLongActivate` |
125
+ | 배치 | CSS `position: fixed` 하단 | 제품 레이아웃이 배치 |
126
+ | 하단 inset | `env(safe-area-inset-bottom)` 자동 | `safeAreaBottom`(recipe 최소 padding에 더함) |
127
+ | 키보드 감지 | `visualViewport` 높이 | `Keyboard` 이벤트 |
128
+ | `renderIcon`의 `color` | `"currentColor"` | 해석된 색 문자열 |
129
+ | 그 밖 | `className`·`style`·HTML 속성·`layoutStyle` | `renderBadge`, `getItemTestID`, `layoutStyle`(`style`·`surfaceStyle`·`listStyle`·`primaryActionStyle`은 deprecated) |
130
+
131
+ ## 함정
132
+
133
+ - Web root가 `position: fixed`라 본문 마지막 내용이 막대 아래로 들어간다. 제품 레이아웃이 하단 여백을 둔다.
134
+ - `renderLink` 없이 쓰면 일반 `<a>`로 그려 SPA 전환이 일어나지 않는다.
135
+ - Web `onActivate`는 링크 기본 이동을 막지 않는다. `renderLink`의 라우터 Link가 이동하고 `onActivate`는 계측·맨 위로 스크롤 같은 부수 동작에만 쓴다. 수정키·가운데 클릭에는 불리지 않는다.
136
+ - 현재 Native 내비게이션 연구 스토리(`showcase/native/src/reference-navigation-bars.tsx`)는 deprecated `style`(`paddingHorizontal: 0`)·`listStyle`(`borderRadius`)로 recipe 여백·모서리를 덮고, 제목을 `Text variant="heading"`으로 그린다. 규칙은 `configuration`·`layoutStyle`과 [Heading](heading.md)이다(스토리 수정 후보).
@@ -0,0 +1,81 @@
1
+ # Breadcrumb
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Breadcrumb](../../breadcrumb.md), recipe `breadcrumbRecipe`(`src/breadcrumb.ts`)
9
+ - 스토리북: `배포/컴포넌트/탐색/이동 경로`
10
+
11
+ ## 언제 쓰나
12
+
13
+ Web의 깊은 계층 화면에서 현재 위치까지의 경로를 보여 주고 상위 계층으로 바로 돌아가게 할 때 쓴다.
14
+ `구단 목록 › LG 트윈스 › 선수단`처럼 순서가 곧 계층이다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | Native 화면 | 플랫폼 back + [TopBar](top-bar.md) 제목 (Native Breadcrumb는 없다) |
21
+ | 같은 계층의 형제 화면 전환 | [Tabs](tabs.md) |
22
+ | 최상위 목적지 이동 | [BottomNavigation](bottom-navigation.md), [Sidebar](sidebar.md) |
23
+ | 문장 안 링크 하나 | [Link](link.md) |
24
+ | 단계 진행 표시 | [Steps](steps.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `Breadcrumb` | 기본 | `@hjmds/react`, `/breadcrumb`, `/navigation` | 없음 |
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { Breadcrumb } from "@hjmds/react/breadcrumb";
37
+
38
+ <Breadcrumb
39
+ label={t("nav.breadcrumb")}
40
+ items={[
41
+ { id: "teams", label: t("teams.title"), destination: { kind: "internal", href: "/teams" } },
42
+ { id: "lg", label: team.name, destination: { kind: "internal", href: `/teams/${team.id}` } },
43
+ { id: "squad", label: t("teams.squad") },
44
+ ]}
45
+ />
46
+ ```
47
+
48
+ Native renderer는 없다.
49
+
50
+ ## 축과 기본값
51
+
52
+ | prop | 값 | 기본값 | 설명 |
53
+ | --- | --- | --- | --- |
54
+ | `label` | 문자열 | 필수 | navigation landmark 이름. 비우면 `TypeError` |
55
+ | `items` | `readonly { id: Id; label: string; destination?: { kind: "internal" \| "external"; href: string } }[]` | 필수 | 마지막 항목만 현재 위치이며 `destination`이 없어야 한다. 그 앞 항목은 `destination`이 필수다. 빈 trail·중복 id·빈 label도 거부한다. 항목 하나(현재 화면만)는 유효하다 |
56
+ | 항목 `destination` | Link의 `LinkDestination`(`internal` · `external`) | — | href 규칙은 [Link](link.md)를 따른다 |
57
+ | `separator` | 노드 · `null` | `›`(RTL에서 미러링) | 직접 넘긴 구분자는 뒤집지 않고, `null`이면 숨긴다. 구분자는 항상 `aria-hidden`이다 |
58
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. `style`도 받지만 외형을 덮지 않는다 |
59
+
60
+ 콜백이 없다. 조상 항목은 일반 `<a href>`이고 이동은 브라우저가 한다.
61
+
62
+ ## 배치
63
+
64
+ | 항목 | 값 | 근거 |
65
+ | --- | --- | --- |
66
+ | 크기 | 내용 폭만 차지하는 한 줄 경로. 글자는 `label` 변형(12/18). 링크 높이가 44에 못 미치므로 터치 중심 화면의 주 탐색으로 쓰지 않는다 | `breadcrumbRecipe.link`·`current` |
67
+ | 간격 | 항목·구분자 사이 `spacing.xxs` 4. 바깥 여백이 없으므로 아래 제목·본문과의 간격은 화면의 세로 [Stack](stack.md)이 정한다 | `breadcrumbRecipe.gap`, `.hjm-breadcrumb` |
68
+ | 순서·정렬 | 페이지 제목 바로 위, 본문 시작 쪽 정렬. 상위 → 현재 순서이고 현재 위치(마지막)는 링크가 아니며 `semibold`로 표시한다 | `.hjm-breadcrumb__current` |
69
+ | 고정·스크롤 | 고정되지 않고 본문과 함께 스크롤한다 | `.hjm-breadcrumb__list` |
70
+ | 좁은 폭·큰 글자 | 줄바꿈된다(`flex-wrap: wrap`). 항목 label도 줄바꿈되고 구분자는 줄어들지 않는다 | `.hjm-breadcrumb__list`, `.hjm-breadcrumb__separator { flex-shrink: 0 }` |
71
+
72
+ ## 꼭 지킬 것
73
+
74
+ - `label`과 항목 label은 i18n 키 또는 제품 데이터로 넣는다.
75
+ - 긴 경로를 `...`로 접지 않는다. 축약 축이 없고 renderer는 전체 trail을 줄바꿈해 그린다.
76
+ - 마지막 항목은 링크가 아니다(`aria-current="page"` 텍스트). 현재 화면에 href를 주지 않는다.
77
+
78
+ ## 함정
79
+
80
+ - 조상 항목은 일반 `<a href>`로 그린다. 라우터 adapter prop(`renderLink` 등)이 없으므로
81
+ Next.js `Link` 같은 클라이언트 전환을 기대하지 않는다.
@@ -0,0 +1,118 @@
1
+ # Button
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [버튼 라벨 줄바꿈](../../button-label.md), `src/base-recipes.ts`(`buttonRecipe`), `src/foundations.ts`(`control.buttonHeight`)
9
+ - 스토리북: `배포/컴포넌트/동작/버튼`, `배포/컴포넌트/동작/버튼 안에서 확인`, `배포/컴포넌트/동작/반응 선택`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 사용자가 누르면 무언가가 일어나는 텍스트 행동에 쓴다. 저장·확인·다음 같은 화면의 주 행동,
14
+ 보조 행동, 삭제처럼 되돌리기 어려운 행동, 켜고 끄는 토글 버튼(`selected`)이 여기에 속한다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 아이콘만 있는 행동 | [IconButton](icon-button.md) |
21
+ | 화면 하단에 고정된 주 행동 | [BottomCTA](bottom-cta.md) |
22
+ | 다른 페이지·URL로 이동 | [Link](link.md) (`tone="link"` 버튼은 같은 화면 안의 행동용) |
23
+ | 여러 선택지 중 하나를 고름 | [SegmentedControl](segmented-control.md), [ToggleGroup](toggle-group.md) |
24
+ | 소셜 로그인 | [AuthProviderButton](auth-provider-button.md) |
25
+ | 복사 | `ClipboardButton`(Web, 아래 표) |
26
+ | 떠 있는 주 행동 | [FloatingActionButton](floating-action-button.md) |
27
+
28
+ ## 공개 이름과 import
29
+
30
+ | 이름 | 역할 | Web | Native |
31
+ | --- | --- | --- | --- |
32
+ | `Button` | 기본 | `@hjmds/react`, `/actions` | `@hjmds/react-native`, `/actions` |
33
+ | `ClipboardButton` | 동반(복사) | `/clipboard` | — |
34
+ | `InlineConfirm` | 확장(버튼 자리에서 한 번 더 확인) | `/inline-confirm` | `/inline-confirm` |
35
+ | `ReactionPicker` | 확장(반응 고르기) | `/reaction-picker` | `/reaction-picker` |
36
+
37
+ ## 최소 사용 예
38
+
39
+ ```tsx
40
+ // Web
41
+ import { Button } from "@hjmds/react/actions";
42
+
43
+ <Button tone="primary" loading={saving} onClick={save}>
44
+ {t("profile.save")}
45
+ </Button>
46
+ ```
47
+
48
+ ```tsx
49
+ // Native
50
+ import { Button } from "@hjmds/react-native/actions";
51
+
52
+ <Button tone="primary" loading={saving} onPress={save} fullWidth>
53
+ {t("profile.save")}
54
+ </Button>
55
+ ```
56
+
57
+ ## 축과 기본값
58
+
59
+ | prop | 값 | 기본값 | 설명 |
60
+ | --- | --- | --- | --- |
61
+ | `tone` | `primary` · `secondary` · `ghost` · `danger` · `link` | `primary` | 한 화면의 `primary`는 하나(예외는 아래 꼭 지킬 것) |
62
+ | `size` | `small` · `medium` · `large` | `medium` | 높이 36 · 44 · 52 |
63
+ | `shape` | `rounded` · `pill` | `rounded` | `radius.md` · `full` |
64
+ | `align` | `center` · `leading` | `center` | `leading`은 꽉 찬 폭의 행 행동 |
65
+ | `selected` | `boolean` | — | 주면 토글 버튼. Web `aria-pressed`, Native 접근성 state |
66
+ | `loading` | `boolean` | `false` | 기존 문구·아이콘을 시각적으로 숨기고 중앙 스피너 하나만 표시한다. 같은 children을 유지해 크기를 보존하고 누름을 막는다. 접근성 이름·포커스는 유지한다(Web `aria-disabled`, Native는 `disableWhileLoading`으로만 옛 disabled 동작) |
67
+ | Web `onClick` | `(event: MouseEvent<HTMLButtonElement>) => void` | — | `disabled`·`loading`·`aria-disabled`이면 부르지 않는다 |
68
+ | Native `onPress` | `(event: GestureResponderEvent) => void` | — | 같은 조건에서 부르지 않는다 |
69
+ | `leading` · `trailing` | `ReactNode` | — | 라벨 앞뒤 아이콘 |
70
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치. Web 꽉 찬 폭은 `layoutStyle={{ width: "100%" }}` |
71
+ | Native `fullWidth` | `boolean` | `false` | 부모 폭을 채운다 |
72
+ | Native `loadingLabel` · `renderLoadingIndicator` | `ReactNode` · `(props: { color: string; size: "small" }) => ReactNode` | — | 로딩 중 읽기 문구·스피너 교체 |
73
+ | Native `growWithContent` | `boolean` | `false` | 이미지 등 사용자 콘텐츠가 recipe 높이를 넘어 늘어나게 한다. 문자열·숫자 라벨은 이 옵션 없이도 줄 수에 맞춰 늘어난다 |
74
+ | `ClipboardButton` | `value: string`, `labels: { idle; copied }`, `onCopy?: (value: string) => void`, `onCopyError?: (error: unknown) => void`, `feedbackDuration?`(ms, 2000) | — | Button props를 물려받는다(`tone` 기본 `secondary`, 미게시(1.12.1 이후). 1.12.1은 `primary`). 쓰는 법은 [CodeBlock](code-block.md) |
75
+ | `InlineConfirm` | `label`·`prompt`·`confirmLabel`·`cancelLabel`·`pendingLabel`·`successLabel`·`errorLabel`(모두 `string`), `onConfirm: () => void \| Promise<void>` | — | danger 버튼 → 같은 자리 확인 묶음. Promise가 거부되면 `errorLabel`을 alert로 보인다. Web만 `layoutStyle` |
76
+ | `ReactionPicker` | `label: string`, `options: readonly { id; emoji; label; count?; disabled? }[]`, `value: string \| null`, `onValueChange: (value: string \| null) => void`, `layout?: "wrap" \| "strip"`, `more?: { label; options }` | `layout` `wrap` | 제어 전용. 같은 반응을 다시 누르면 `null`. 이모지는 `strip`(대화 반응 줄)에서만 `typography.title` 18/26으로 커지고 `wrap`은 버튼 글자 크기 그대로다. `more`를 주면 끝에 `+` 토글이 생겨 카탈로그를 펼치고, 카탈로그에만 있는 이모지를 고르면 접히면서 Web 키보드 포커스가 `+` 버튼으로 간다. Web만 `layoutStyle`(루트 기본 `minWidth`보다 우선). `layout`·`more`·`layoutStyle`은 미게시(1.12.1 이후) |
77
+
78
+ ## 배치
79
+
80
+ | 항목 | 값 | 근거 |
81
+ | --- | --- | --- |
82
+ | 크기 | 높이 `small` 36 · `medium` 44 · `large` 52. `small`만 hitSlop 4로 터치 영역 44 | `control.buttonHeight`, `control.buttonHitSlop` |
83
+ | 간격 | 나란한 버튼 사이 `spacing.sm` 12. Dialog·Sheet 행동 영역 좌우·아래 여백 `spacing.lg` 20 | `.hjm-dialog__footer` |
84
+ | 순서·정렬 | 가로 행동 줄은 **보조 → 주**, 끝 정렬. 세로 순서는 배치 소유자의 계약을 따른다: Dialog는 보조 → 주, Native Sheet는 주 → 보조, 좁은 AlertDialog는 주 → 보조. 본문의 짧은 선택·확정 구성은 해당 구성의 순서다. 목록 행 끝은 `small` + `ghost`/`secondary`, `primary` 금지 | `renderAction` 순서(Native overlays), `.hjm-dialog__footer` |
85
+ | 고정·스크롤 | 화면 맨 아래 고정 주 행동은 Button 대신 [BottomCTA](bottom-cta.md)(안전 영역·키보드 처리) | — |
86
+ | 좁은 폭·큰 글자 | 폭 < 600(`breakpoint.medium`)이면 AlertDialog 행동을 세로로 쌓고 주 행동이 위(DOM은 [취소][확인] 유지). 라벨은 두 줄까지, 큰 글자에서는 상한 해제 | `.hjm-alert-dialog__actions`, [버튼 라벨](../../button-label.md) |
87
+
88
+ ```text
89
+ 대화상자·시트 하단 폭 < 600인 AlertDialog
90
+ ┌──────────────────────────────┐ ┌──────────────────────┐
91
+ │ [취소] [저장] │ │ [ 삭제 ] │ ← primary(danger)
92
+ │ secondary ─┘ primary ─┘ │ │ [ 취소 ] │
93
+ └──────────────────────────────┘ └──────────────────────┘
94
+ ```
95
+
96
+ ## 꼭 지킬 것
97
+
98
+ - 라벨은 i18n 키로 넣는다. 자르지 말고 두 줄을 넘으면 카피를 고친다([라벨 정책](../../button-label.md)).
99
+ - 배치는 `layoutStyle`로만 한다. 색·radius·높이를 `style`/`className`으로 덮지 않는다.
100
+ Native에서 `style`·`labelStyle`을 넘기면 실행 중 `TypeError`가 난다.
101
+ - 색은 tone과 제품 테마 토큰으로 바꾼다. 버튼마다 브랜드 색을 하드코딩하지 않는다.
102
+ - 진행 중 상태는 `loading`으로 표시하고 같은 자리에 별도 Spinner를 겹치지 않는다. children을 빈 문자열이나 다른 길이의 진행 문구로 바꾸지 않는다. 렌더러가 문구·아이콘을 숨긴 자리에 중앙 스피너만 표시하고 기존 크기·접근성 이름을 유지한다. 2026-10-06 독립 검증에서 이 사용자 요구가 사용 지침에는 빠져 있음을 확인해 명시했다.
103
+ - "한 화면 `primary` 하나"의 예외는 다음과 같다. `selected`를 준 버튼은 tone과 관계없이 `buttonRecipe.states.selected`(배경 `bg`, 글자·테두리 `contentBrand`)로 칠해져 primary 채움이 아니므로 세지 않고(Web `.hjm-button[data-selected="true"]`, Native `internal/recipe-button.tsx`, 검색·작품 탐색 스토리의 필터 줄), Sheet·Dialog 안의 행동은 그 표면 안에서 primary 하나를 센다([Sheet](sheet.md) `footer`, 작품 탐색·랜딩 스토리의 시트).
104
+
105
+ - 긴 소개 화면에서 위·아래 CTA가 **동일한 행동**으로 이어지고 서로 다른 스크롤 구간에 있으면 둘 다 primary를 허용한다. 큰 글자에서 첫 CTA가 멀어지는 문제를 보완하기 위한 예외이며, 서로 다른 가입·구매 행동을 동시에 강조하는 근거로 쓰지 않는다.
106
+
107
+ ## 플랫폼 차이
108
+
109
+ | 항목 | Web | Native |
110
+ | --- | --- | --- |
111
+ | 이벤트 | `onClick` | `onPress` |
112
+ | 꽉 찬 폭 | `layoutStyle={{ width: "100%" }}` | `fullWidth` |
113
+ | 로딩 문구·스피너 교체 | 없음 | `loadingLabel`, `renderLoadingIndicator` |
114
+ | 내용에 맞춰 높이 증가 | CSS가 처리 | 문자열·숫자 라벨은 자동. 사용자 콘텐츠는 `growWithContent` |
115
+ | 기본 `type` | `"button"`(폼 submit은 `type="submit"`을 명시) | 해당 없음 |
116
+
117
+
118
+ - 2026-10-06 독립 지침 재구현에서 footer 순서를 모든 본문 행동에 강제하는 것으로 읽혔다. 시간 선택처럼 주 행동 다음에 초기화가 오는 구성과 AlertDialog의 좁은 폭 순서는 각각 명시된 구성 계약을 따른다.