@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,138 @@
1
+ # FloatingActionButton
2
+
3
+ - ๋‹จ๊ณ„: ์ปดํฌ๋„ŒํŠธ
4
+ - ์ƒํƒœ: ๋ฐฐํฌ
5
+ - ์ง€์›: Web ยท Native
6
+ - ์ ์šฉ: 1.12.1
7
+ - ๊ฒ€ํ† ์ผ: 2026-10-06
8
+ - ๊ทผ๊ฑฐ: [FloatingActionButton](../../floating-action-button.md), recipe `floatingActionButtonRecipe`(`src/floating-action-button.ts`)
9
+ - ์Šคํ† ๋ฆฌ๋ถ: `๋ฐฐํฌ/์ปดํฌ๋„ŒํŠธ/๋™์ž‘/ํ”Œ๋กœํŒ… ์‹คํ–‰ ๋ฒ„ํŠผ`
10
+
11
+ ## ์–ธ์ œ ์“ฐ๋‚˜
12
+
13
+ ๋ชฉ๋กยทํ”ผ๋“œ์ฒ˜๋Ÿผ ์Šคํฌ๋กค๋˜๋Š” ์ฝ˜ํ…์ธ  ์œ„์— ๋–  ์žˆ๋Š” **๋‹จ์ผ ์ƒ์„ฑ ํ–‰๋™**(์ƒˆ ๊ธฐ๋ก ์ถ”๊ฐ€, ์ƒˆ ๊ธ€ ์ž‘์„ฑ)์— ์“ด๋‹ค.
14
+ ์Šคํฌ๋กค ๋ฐฉํ–ฅ์— ๋”ฐ๋ผ ์•Œ์•ฝ(๋ผ๋ฒจ ๋ณด์ž„)๊ณผ ์›(์•„์ด์ฝ˜๋งŒ)์œผ๋กœ ์ ‘ํžˆ์ง€๋งŒ ๊ฐ™์€ ๋ฒ„ํŠผ ์ธ์Šคํ„ด์Šค๋ฅผ ์œ ์ง€ํ•œ๋‹ค.
15
+
16
+ ## ์“ฐ์ง€ ์•Š์„ ๋•Œ
17
+
18
+ | ์ƒํ™ฉ | ๋Œ€์‹  ์“ธ ๊ฒƒ |
19
+ | --- | --- |
20
+ | ํ™”๋ฉด์˜ ๊ฒฐ๋ก ์ด ๋˜๋Š” ํ•˜๋‹จ ๊ณ ์ • ์ฃผ ํ–‰๋™ | [BottomCTA](bottom-cta.md) ([ํŒ์ •](../../floating-action-button.md#bottomcta์™€-๊ฒน์น˜์ง€-์•Š๋Š”๋‹ค-ํŒ์ •)) |
21
+ | ์ฝ˜ํ…์ธ  ์•ˆ์˜ ์ผ๋ฐ˜ ํ–‰๋™ | [Button](button.md), [IconButton](icon-button.md) |
22
+ | ํ•˜๋‹จ ํƒญ ๊ฐ€์šด๋ฐ์˜ ์ƒ์„ฑ ๋ฒ„ํŠผ | [BottomNavigation](bottom-navigation.md) ์˜†์— Button/IconButton ํ•ฉ์„ฑ |
23
+ | ๋น„ํ™œ์„ฑยท๋กœ๋”ฉ ์ƒํƒœ๊ฐ€ ํ•„์š”ํ•œ ํ–‰๋™ | [Button](button.md) (FAB๋Š” `disabled`ยท`loading`์ด ์—†๋‹ค) |
24
+
25
+ ## ๊ณต๊ฐœ ์ด๋ฆ„๊ณผ import
26
+
27
+ | ์ด๋ฆ„ | ์—ญํ•  | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `FloatingActionButton` | ๊ธฐ๋ณธ | `@hjmds/react`, `/floating-action-button` | `@hjmds/react-native`, `/floating-action-button` |
30
+ | `useFloatingActionButtonScroll` | ๋ณด์กฐ โ€” ์Šคํฌ๋กค๋กœ `layoutMode` ๊ฒฐ์ • | `/floating-action-button` | `/floating-action-button` |
31
+ | `resolveFloatingActionButtonContentClearance` | ๋ณด์กฐ โ€” ์ฒซ ํ•˜๋‹จ ์—ฌ๋ฐฑ ๊ณ„์‚ฐ | `/floating-action-button` | `/floating-action-button` |
32
+
33
+ ## ์ตœ์†Œ ์‚ฌ์šฉ ์˜ˆ
34
+
35
+ ```tsx
36
+ // Web
37
+ import { useState } from "react";
38
+ import { FloatingActionButton, useFloatingActionButtonScroll } from "@hjmds/react/floating-action-button";
39
+
40
+ const layoutMode = useFloatingActionButtonScroll(); // window ์Šคํฌ๋กค. ์ปจํ…Œ์ด๋„ˆ๋ฉด element, ๋งˆ์šดํŠธ ์ „์—” null
41
+ const [clearance, setClearance] = useState(0);
42
+
43
+ <ul style={{ paddingBottom: clearance }}>{items}</ul>
44
+ <FloatingActionButton
45
+ descriptor={{ icon: { name: "add" }, label: t("memo.new"), layoutMode }}
46
+ renderIcon={({ name, size, color }) => <ProductIcon name={name} size={size} color={color} />}
47
+ onContentClearanceChange={setClearance}
48
+ onClick={openComposer}
49
+ />
50
+ ```
51
+
52
+ ```tsx
53
+ // Native
54
+ import { useState } from "react";
55
+ import { FlatList, View } from "react-native";
56
+ import { FloatingActionButton, useFloatingActionButtonScroll } from "@hjmds/react-native/floating-action-button";
57
+
58
+ const { layoutMode, onScroll } = useFloatingActionButtonScroll();
59
+ const [clearance, setClearance] = useState(0);
60
+
61
+ <View style={{ flex: 1 }}>
62
+ <FlatList data={items} renderItem={renderItem} onScroll={onScroll} contentContainerStyle={{ paddingBottom: clearance }} />
63
+ <FloatingActionButton
64
+ descriptor={{ icon: { name: "add" }, label: t("memo.new"), layoutMode }}
65
+ renderIcon={({ name, size, color }) => <ProductIcon name={name} size={size} color={color} />}
66
+ safeAreaBottomInset={insets.bottom}
67
+ onContentClearanceChange={setClearance}
68
+ onPress={openComposer}
69
+ />
70
+ </View>
71
+ ```
72
+
73
+ ## ์ถ•๊ณผ ๊ธฐ๋ณธ๊ฐ’
74
+
75
+ | prop | ๊ฐ’ | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… |
76
+ | --- | --- | --- | --- |
77
+ | `descriptor.layoutMode` | `expanded` ยท `collapsed` | `expanded` | ์ง์ ‘ ๊ณ ๋ฅด์ง€ ๋ง๊ณ  `useFloatingActionButtonScroll` ๊ฒฐ๊ณผ๋ฅผ ๋„˜๊ธด๋‹ค |
78
+ | `descriptor.label` | ๋ฌธ๊ตฌ(ํ•„์ˆ˜) | โ€” | `collapsed`์—์„œ๋„ ์ ‘๊ทผ์„ฑ ์ด๋ฆ„์€ ํ•ญ์ƒ ์ „์ฒด `label`์ด๋‹ค |
79
+ | `descriptor.icon` | `{ name }` | โ€” | `name`๋งŒ ๋ฐ›๋Š”๋‹ค(`size`ยท`tone`ยท์ ‘๊ทผ์„ฑ ์ด๋ฆ„์€ ๋„ฃ์„ ์ˆ˜ ์—†๋‹ค). ์•„์ด์ฝ˜์€ ํ•ญ์ƒ ์žฅ์‹์ด๋‹ค |
80
+ | `safeAreaBottomInset` | 0 ์ด์ƒ ์ˆซ์ž | `0` | โ€” |
81
+ | `renderIcon` | `(icon: { name: string; size: number; color: string; decorative: true }) => ReactNode` | ํ•„์ˆ˜ | ์ œํ’ˆ ์•„์ด์ฝ˜ ์‹œ์Šคํ…œ์œผ๋กœ ๊ทธ๋ฆฐ๋‹ค |
82
+ | `onContentClearanceChange` | `(clearance: number) => void` | ํ•„์ˆ˜ | ์Šคํฌ๋กค ์ฝ˜ํ…์ธ  ํ•˜๋‹จ padding์œผ๋กœ ์“ด๋‹ค. ํฐ ๊ธ€์žยทinset์ด ๋ฐ”๋€Œ๋ฉด ๋‹ค์‹œ ๋ถˆ๋ฆฐ๋‹ค |
83
+ | Web `onClick` ยท Native `onPress` | Button์˜ ๊ฐ™์€ ์ด๋ฒคํŠธ | โ€” | โ€” |
84
+
85
+ - ํ†คยทํฌ๊ธฐยท๋ชจ์–‘์€ `primary`ยท`large`ยท`pill`๋กœ ๊ณ ์ •์ด๊ณ  prop์ด ์—†๋‹ค.
86
+
87
+ ## ๋ฐฐ์น˜
88
+
89
+ | ํ•ญ๋ชฉ | ๊ฐ’ | ๊ทผ๊ฑฐ |
90
+ | --- | --- | --- |
91
+ | ํฌ๊ธฐ | ๋†’์ดยท์ง€๋ฆ„ 52(IconButton `large`). `expanded`๋Š” ์•Œ์•ฝ(์•„์ด์ฝ˜ + ๋ผ๋ฒจ, ๋ผ๋ฒจ ์ขŒ์šฐ ์—ฌ๋ฐฑ `spacing.lg` 20), `collapsed`๋Š” ์ง€๋ฆ„ 52 ์›์ด๋‹ค. ์ตœ๋Œ€ ํญ์€ ํ™”๋ฉด ํญ โˆ’ 32(์—ฌ๋ฐฑ ์–‘์ชฝ)๋‹ค | `floating-action-button.ts` `floatingActionButtonRecipe`, `styles.css` `.hjm-fab` |
92
+ | ๊ฐ„๊ฒฉ | ๊ฐ€์žฅ์ž๋ฆฌ ์—ฌ๋ฐฑ `spacing.md`(16). ํ•˜๋‹จ ๊ฑฐ๋ฆฌ๋Š” 16 + ํ•˜๋‹จ ์•ˆ์ „ ์˜์—ญ์ด๋‹ค(Web์€ `safeAreaBottomInset`๊ณผ `env(safe-area-inset-bottom)` ์ค‘ ํฐ ๊ฐ’, Native๋Š” `safeAreaBottomInset`). ์Šคํฌ๋กค ์ฝ˜ํ…์ธ  ํ•˜๋‹จ์— `onContentClearanceChange` ๊ฐ’์„ padding์œผ๋กœ ๋‘”๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ 52 + 16ร—2 + ํ•˜๋‹จ inset(= 84 + inset)์ด๋‹ค | `floating-action-button.ts` `resolveFloatingActionButtonContentClearance`, `styles.css` `.hjm-fab`, `react-native/src/floating-action-button.tsx` |
93
+ | ์ˆœ์„œยท์ •๋ ฌ | ์œ„์น˜๋Š” ์Šคํฌ๋กค ์˜์—ญ์˜ **๋…ผ๋ฆฌ์  ๋ ์ชฝ ํ•˜๋‹จ ๋ชจ์„œ๋ฆฌ**๋กœ ๊ณ ์ •์ด๋‹ค(RTL์€ ์™ผ์ชฝ) | `styles.css` `.hjm-fab`, `react-native/src/floating-action-button.tsx` |
94
+ | ๊ณ ์ •ยท์Šคํฌ๋กค | Web์€ `position: fixed`(z-index `layer.sticky` 100)๋กœ ์ฐฝ์— ๋ถ™๋Š”๋‹ค. Native๋Š” positioned ๋ถ€๋ชจ(`flex: 1` View) ์•ˆ์—์„œ `absolute`๋‹ค. [BottomNavigation](bottom-navigation.md)ยท๋„๊ตฌ๋ง‰๋Œ€ ์œ„์— ๋„์šธ ๋•Œ๋Š” ๊ทธ ๋†’์ด๋ฅผ `safeAreaBottomInset`์— ๋”ํ•œ๋‹ค(Native๋Š” ์ œํ’ˆ์ด ๋”ํ•œ๋‹ค) | `styles.css` `.hjm-fab`, `react-native/src/floating-action-button.tsx` |
95
+ | ์ข์€ ํญยทํฐ ๊ธ€์ž | ํฐ ๊ธ€์ž๋กœ ๋ผ๋ฒจ์ด ์—ฌ๋Ÿฌ ์ค„์ด ๋˜๋ฉด clearance ๊ฐ’์ด ์ปค์ง„๋‹ค | `react-native/src/floating-action-button.tsx` |
96
+
97
+ ```text
98
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
99
+ โ”‚ TopBar (๊ณ ์ •) โ”‚
100
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
101
+ โ”‚ ๋ชฉ๋ก (์Šคํฌ๋กค) โ”‚
102
+ โ”‚ ... โ”‚
103
+ โ”‚ ๋งˆ์ง€๋ง‰ ํ•ญ๋ชฉ โ”‚
104
+ โ”‚ โ† padding-bottom = clearanceโ”‚
105
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
106
+ โ”‚ โ”‚ + ์ƒˆ ๊ธฐ๋ก โ”‚ โ”‚ โ† FAB expanded(52, primary pill)
107
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ ๋ ์—ฌ๋ฐฑ 16
108
+ โ”‚ 16 โ”‚
109
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
110
+ โ”‚ ์•ˆ์ „ ์˜์—ญ โ”‚
111
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
112
+ ์Šคํฌ๋กคํ•ด ๋‚ด๋ ค๊ฐ€๋ฉด (+) ์ง€๋ฆ„ 52 ์›์œผ๋กœ ์ ‘ํžŒ๋‹ค.
113
+ ```
114
+
115
+ ## ๊ผญ ์ง€ํ‚ฌ ๊ฒƒ
116
+
117
+ - `onContentClearanceChange`๊ฐ€ ์ค€ ๊ฐ’์„ ์Šคํฌ๋กค ์ฝ˜ํ…์ธ  ํ•˜๋‹จ padding์— ๋ฐ˜๋“œ์‹œ ์ ์šฉํ•œ๋‹ค. ํฐ ๊ธ€์ž์—์„œ
118
+ ๋ผ๋ฒจ์ด ์—ฌ๋Ÿฌ ์ค„์ด ๋˜๋ฉด ๊ฐ’์ด ์ปค์ง„๋‹ค. ์ง€๋ฆ„๋งŒ ์˜ˆ์•ฝํ•˜๋ฉด ๋งˆ์ง€๋ง‰ ํ•ญ๋ชฉ์„ ๊ฐ€๋ฆฐ๋‹ค.
119
+ - ์Šคํฌ๋กค ์ฝ˜ํ…์ธ  **๋’ค**์˜ sibling์œผ๋กœ ๋‘”๋‹ค. ๋ชฉ๋ก ํ•ญ๋ชฉ ์•ˆ์— ๋„ฃ์ง€ ์•Š๋Š”๋‹ค.
120
+ - ๋ผ๋ฒจ์€ i18n ํ‚ค๋กœ ๋„ฃ๋Š”๋‹ค. ์•„์ด์ฝ˜ ๊ทธ๋ฆผ์€ ์ œํ’ˆ ์•„์ด์ฝ˜ ์‹œ์Šคํ…œ์ด `renderIcon`์œผ๋กœ ๊ทธ๋ฆฐ๋‹ค(HJM์€ nameยทsizeยทcolor๋งŒ ์ค€๋‹ค).
121
+ - ํ•œ ํ™”๋ฉด์— FAB๋Š” ํ•˜๋‚˜๋งŒ ๋‘”๋‹ค. ๋ชจ์„œ๋ฆฌ ์œ„์น˜๋Š” ๋ฐ”๊ฟ€ ์ˆ˜ ์—†๋‹ค(๋…ผ๋ฆฌ์  ๋ ์ชฝ ํ•˜๋‹จ, RTL์€ ์™ผ์ชฝ).
122
+
123
+ ## ํ”Œ๋žซํผ ์ฐจ์ด
124
+
125
+ | ํ•ญ๋ชฉ | Web | Native |
126
+ | --- | --- | --- |
127
+ | ์ด๋ฒคํŠธ | `onClick` | `onPress` |
128
+ | ์œ„์น˜ | CSS(`.hjm-fab`) | positioned ๋ถ€๋ชจ ์•ˆ `absolute` |
129
+ | ์Šคํฌ๋กค hook | `useFloatingActionButtonScroll(target?)` โ†’ `layoutMode` | `useFloatingActionButtonScroll()` โ†’ `{ layoutMode, onScroll }` |
130
+ | ์—ฌ๋ฐฑ ์ธก์ • | `ResizeObserver`ยท`resize` | `onLayout` |
131
+ | ๊ทธ ๋ฐ–์˜ prop | `id`, `className`, `onFocus`, `onBlur` | `testID`, `onFocus`, `onBlur` |
132
+ | ref | `HTMLButtonElement` | `View` |
133
+ | ๋ฐฐ์น˜ prop | ์—†์Œ(`layoutStyle`์„ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค. ์œ„์น˜๋Š” CSS๊ฐ€ ์†Œ์œ ) | ์—†์Œ(`layoutStyle`ยท`style` ์—†์Œ) |
134
+
135
+ ## ํ•จ์ •
136
+
137
+ - ๋ชจ๋“œ๋งˆ๋‹ค Button๊ณผ IconButton์„ ๋ฐ”๊ฟ” ๋ผ์šฐ๋ฉด focus์™€ ref๋ฅผ ์žƒ๋Š”๋‹ค. ์ด ์ปดํฌ๋„ŒํŠธ ํ•˜๋‚˜๋กœ ์ ‘๊ณ  ํŽธ๋‹ค.
138
+ - Native `safeAreaBottomInset`์€ OS ํ•˜๋‹จ inset๋งŒ์ด๋‹ค. ํ™”๋ฉด ์•ˆ ๋„๊ตฌ๋ง‰๋Œ€ ๋†’์ด๋Š” ์ œํ’ˆ์ด ๋”ฐ๋กœ ๋”ํ•œ๋‹ค.
@@ -0,0 +1,162 @@
1
+ # Form
2
+
3
+ - ๋‹จ๊ณ„: ์ปดํฌ๋„ŒํŠธ
4
+ - ์ƒํƒœ: ๋ฐฐํฌ
5
+ - ์ง€์›: Web ยท Native
6
+ - ์ ์šฉ: 1.12.1
7
+ - ๊ฒ€ํ† ์ผ: 2026-10-06
8
+ - ๊ทผ๊ฑฐ: [Form](../../form.md), recipe `formRecipe`(`src/form.ts`)
9
+ - ์Šคํ† ๋ฆฌ๋ถ: `๋ฐฐํฌ/์ปดํฌ๋„ŒํŠธ/์ž…๋ ฅ/์ž…๋ ฅ ์–‘์‹`
10
+
11
+ ## ์–ธ์ œ ์“ฐ๋‚˜
12
+
13
+ ์—ฌ๋Ÿฌ [Field](field.md)๋ฅผ ํ•œ ํ™”๋ฉด์— ์Œ“๊ณ  ํ•œ ๋ฒˆ์— ์ œ์ถœํ•  ๋•Œ ์“ด๋‹ค. Form์€ ํ•„๋“œ ์‚ฌ์ด์˜ ์„ธ๋กœ ๋ฆฌ๋“ฌ๊ณผ
14
+ **์ œ์ถœ ์„ธ์…˜**(์ค‘๋ณต ์ œ์ถœ ์ฐจ๋‹จ, ์ง„ํ–‰ ์ค‘ ์ž ๊ธˆ, ํผ ๋‹จ์œ„ ์˜ค๋ฅ˜ ํ‘œ์‹œ)๋งŒ ์†Œ์œ ํ•œ๋‹ค. ๊ฐ’ยท๊ฒ€์ฆยทdirty ํŒ๋‹จ์€
15
+ ์ œํ’ˆ(React Hook Form ๋“ฑ)์ด ์†Œ์œ ํ•œ๋‹ค.
16
+
17
+ ## ์“ฐ์ง€ ์•Š์„ ๋•Œ
18
+
19
+ | ์ƒํ™ฉ | ๋Œ€์‹  ์“ธ ๊ฒƒ |
20
+ | --- | --- |
21
+ | ์ž…๋ ฅ ํ•˜๋‚˜์™€ ์ฆ‰์‹œ ๋ฐ˜์˜(๊ฒ€์ƒ‰, ํ† ๊ธ€) | [SearchField](search-field.md), [Switch](switch.md) |
22
+ | ํ•œ ๋ฒˆ ํ™•์ธํ•˜๊ณ  ๋‹ซํžˆ๋Š” ์œ„ํ—˜ ํ–‰๋™ | [AlertDialog](alert-dialog.md) |
23
+ | ํ•„๋“œ ํ•˜๋‚˜์˜ ๋ผ๋ฒจยท๋„์›€๋งยท์˜ค๋ฅ˜ ํ”„๋ ˆ์ž„ | [Field](field.md) |
24
+ | ์•ฝ๊ด€ ๋™์˜ ๋ฌถ์Œ | [Agreement](agreement.md) |
25
+ | ํ•˜๋‹จ ๊ณ ์ • ์ œ์ถœ ๋ฒ„ํŠผ์ด ํ•„์š”ํ•œ ๊ธด ํ™”๋ฉด | Web: Form ์•ˆ์˜ ํ•„๋“œ + [BottomCTA](bottom-cta.md)(`actions`๋Š” ๋น„์šด๋‹ค). Native: Form์€ ๋‚ด์žฅ ์ œ์ถœ ๋ฒ„ํŠผ์„ ํ•ญ์ƒ ๊ทธ๋ฆฌ๋ฏ€๋กœ Form ์—†์ด ํ•„๋“œ + BottomCTA๋กœ ์ œํ’ˆ์ด ์ œ์ถœ์„ ์†Œ์œ ํ•œ๋‹ค |
26
+ | Native์—์„œ return ํ‚ค๋กœ ์ œ์ถœํ•ด์•ผ ํ•˜๋Š” ํผ(๋กœ๊ทธ์ธยท์‹ฌ์‚ฌ์ž ํผ) | Form ์—†์ด `TextField`(ref) + [Button](button.md)(`loading`). Native Form์€ ๋ฐ–์—์„œ ์ œ์ถœ์„ ๋ถ€๋ฅผ ์ˆ˜ ์—†๋‹ค |
27
+
28
+ ## ๊ณต๊ฐœ ์ด๋ฆ„๊ณผ import
29
+
30
+ | ์ด๋ฆ„ | ์—ญํ•  | Web | Native |
31
+ | --- | --- | --- | --- |
32
+ | `Form` | ๊ธฐ๋ณธ | `@hjmds/react`, `/forms` | `@hjmds/react-native`, `/forms` |
33
+ | `createFormSubmitSession` | ๋ณด์กฐ โ€” ์ œ์ถœ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ’์œผ๋กœ ๋ฐ›๊ฑฐ๋‚˜ ์–ธ๋งˆ์šดํŠธ ๋•Œ ์ •์‚ฐํ•ด์•ผ ํ•  ๋•Œ ์“ฐ๋Š” ์„ธ์…˜ | `@hjmds/design-contracts/components/form` | ๊ฐ™์Œ |
34
+ | `resolveFirstInvalidFieldFocusTarget` | ๋ณด์กฐ โ€” ํ•„๋“œ ์ˆœ์„œ์™€ ๋ฌดํšจ id๋กœ ์ฒซ ์˜ค๋ฅ˜ ํ•„๋“œ๋ฅผ ๊ณ ๋ฅธ๋‹ค | `@hjmds/design-contracts/components/form` | ๊ฐ™์Œ |
35
+
36
+ ## ์ตœ์†Œ ์‚ฌ์šฉ ์˜ˆ
37
+
38
+ ```tsx
39
+ // Web
40
+ import { useRef } from "react";
41
+ import { Form, TextField } from "@hjmds/react/forms";
42
+ import { Button } from "@hjmds/react/actions";
43
+
44
+ const nicknameInputRef = useRef<HTMLInputElement>(null);
45
+
46
+ <Form
47
+ aria-label={t("profile.formLabel")}
48
+ onSubmit={async () => {
49
+ setServerError(null);
50
+ if (nicknameError) { nicknameInputRef.current?.focus(); return; } // Web์€ ์ฒซ ์˜ค๋ฅ˜ ํฌ์ปค์Šค๋ฅผ ์ œํ’ˆ์ด ํ•œ๋‹ค
51
+ try { await save(values); } catch { setServerError(t("profile.saveFailed")); }
52
+ }}
53
+ formError={serverError}
54
+ actions={<Button type="submit">{t("profile.save")}</Button>}
55
+ >
56
+ <TextField ref={nicknameInputRef} label={t("profile.nickname")} value={values.nickname}
57
+ onValueChange={setNickname} error={nicknameError ? t(nicknameError) : undefined} />
58
+ </Form>
59
+ ```
60
+
61
+ ```tsx
62
+ // Native
63
+ import { useEffect, useRef } from "react";
64
+ import type { TextInput } from "react-native";
65
+ import { Form } from "@hjmds/react-native/forms";
66
+ import { TextField } from "@hjmds/react-native/inputs";
67
+
68
+ const nicknameInputRef = useRef<TextInput>(null);
69
+ const firstInvalidRef = useRef<TextInput | null>(null);
70
+ // Form์€ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ ์ˆœ๊ฐ„ current๋ฅผ ์ฝ๋Š”๋‹ค. ๋ Œ๋” ์ค‘์ด ์•„๋‹ˆ๋ผ effect์—์„œ ๊ฐฑ์‹ ํ•œ๋‹ค(์ฒซ ๋ Œ๋”์—” ํ•„๋“œ ref๊ฐ€ null).
71
+ useEffect(() => { firstInvalidRef.current = nicknameError ? nicknameInputRef.current : null; }, [nicknameError]);
72
+
73
+ <Form
74
+ label={t("profile.formLabel")}
75
+ values={values}
76
+ onSubmit={save}
77
+ submitLabel={t("profile.save")}
78
+ fallbackErrorMessage={t("common.saveFailed")}
79
+ firstInvalidFieldRef={firstInvalidRef}
80
+ >
81
+ <TextField ref={nicknameInputRef} label={t("profile.nickname")} value={values.nickname}
82
+ onValueChange={setNickname} {...(nicknameError ? { error: t(nicknameError) } : {})} />
83
+ </Form>
84
+ ```
85
+
86
+ ## ์ถ•๊ณผ ๊ธฐ๋ณธ๊ฐ’
87
+
88
+ | prop | ๊ฐ’ | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… |
89
+ | --- | --- | --- | --- |
90
+ | `density` | `compact` ยท `comfortable` | `comfortable` | ํ•„๋“œ ์‚ฌ์ด ๊ฐ„๊ฒฉ๋งŒ ๋ฐ”๊พผ๋‹ค. ํ•„๋“œ ๋‚ด๋ถ€ ๋ฆฌ๋“ฌ์€ Field์˜ ๋ชซ์ด๋‹ค |
91
+ | Web `onSubmit` | `(event: FormEvent<HTMLFormElement>) => void \| Promise<void>` | ํ•„์ˆ˜ | ๊ธฐ๋ณธ ๋™์ž‘์€ ๋ง‰ํ˜€ ์žˆ๋‹ค. Promise๋ฉด ์ •์‚ฐ๊นŒ์ง€ busy |
92
+ | Native `onSubmit` | `(values: Values) => void \| Promise<void>` | ํ•„์ˆ˜ | ๋ˆ„๋ฅธ ์ˆœ๊ฐ„์˜ `values`๋ฅผ ๋ฐ›๋Š”๋‹ค. rejectํ•˜๋ฉด `failed` |
93
+ | Native `status`/`defaultStatus` | `FormSubmitStatus` = `"idle"` ยท `"submitting"` ยท `"succeeded"` ยท `"failed"` | `idle` | ์ œ์–ดยท๋น„์ œ์–ด ๋‘˜ ๋‹ค ๋œ๋‹ค |
94
+ | Native `onStatusChange` | `(status: FormSubmitStatus) => void` | โ€” | ๋ฌดํšจ ์ œ์ถœ(`firstInvalidFieldRef` non-null)์—์„œ๋Š” ๋ถˆ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค |
95
+ | Native `firstInvalidFieldRef` | `RefObject<TextInput \| null>` | โ€” | ๋ˆ„๋ฅธ ์ˆœ๊ฐ„ non-null์ด๋ฉด ๊ทธ ์นธ์— ํฌ์ปค์Šคํ•˜๊ณ  ์ œ์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค |
96
+ | Native `error` ยท Web `formError` | Native `string` ยท Web `ReactNode` | โ€” | ํผ ๋‹จ์œ„ ์‹คํŒจ ๋ฌธ์žฅ |
97
+ | `busy`(Web) | `true` ยท `false` | `false` | `onSubmit`์ด Promise๋ฅผ ๋Œ๋ ค์ฃผ๋ฉด ๋๋‚  ๋•Œ๊นŒ์ง€ ์Šค์Šค๋กœ busy๊ฐ€ ๋œ๋‹ค |
98
+ | `disabled`(Native) | `true` ยท `false` | `false` | โ€” |
99
+
100
+ - ์„ธ์…˜(`createFormSubmitSession({ onSubmit, fallbackErrorMessage, resolveErrorMessage? })`)์˜ ์ƒํƒœ๋Š”
101
+ `getSnapshot()`์ด ์ฃผ๋Š” `{ status: "idle" | "submitting" | "succeeded" } | { status: "failed", message }`์ด๊ณ ,
102
+ `submit(values)`๋Š” `{ outcome: "succeeded" } | { outcome: "failed", message } | { outcome: "blocked", reason } | { outcome: "interrupted" }`๋ฅผ
103
+ ๋Œ๋ ค์ค€๋‹ค. ์„ธ์…˜์„ ์“ฐ๋ฉด Web์€ `busy`ยท`formError`, Native๋Š” `status`ยท`error`๋ฅผ controlled๋กœ ๋„˜๊ธฐ๊ณ  renderer ๋‚ด์žฅ ์ƒํƒœ๋ฅผ ํ•จ๊ป˜ ์ง„์‹ค๋กœ ๋‘์ง€ ์•Š๋Š”๋‹ค([๊ณ„์•ฝ](../../form.md)).
104
+
105
+ ## ๋ฐฐ์น˜
106
+
107
+ | ํ•ญ๋ชฉ | ๊ฐ’ | ๊ทผ๊ฑฐ |
108
+ | --- | --- | --- |
109
+ | ํฌ๊ธฐ | โ€” | โ€” |
110
+ | ๊ฐ„๊ฒฉ | ํ•„๋“œ ์‚ฌ์ด ๊ฐ„๊ฒฉ: `comfortable` `spacing.lg`(20), `compact` `spacing.sm`(12). Web์€ ํ•„๋“œ ๋ฌถ์Œ ์•„๋ž˜ ํผ ์˜ค๋ฅ˜์™€ `actions`๊ฐ€ ๊ฐ๊ฐ `spacing.sm`(12) ์œ„ ์—ฌ๋ฐฑ์„ ๊ฐ–๊ณ , `actions` ์•ˆ ๋ฒ„ํŠผ ๊ฐ„๊ฒฉ์€ `spacing.sm`(12)์ด๋‹ค. Native๋Š” ํ•„๋“œยท์˜ค๋ฅ˜ยท๋‚ด์žฅ ์ œ์ถœ ๋ฒ„ํŠผ์„ ๊ฐ™์€ `fieldGap`์œผ๋กœ ์„ธ๋กœ๋กœ ์Œ“๋Š”๋‹ค | `form.ts` `formRecipe`, `styles.css` `.hjm-form__*`, `react-native/src/forms.tsx` `Form` |
111
+ | ์ˆœ์„œยท์ •๋ ฌ | ์œ„โ†’์•„๋ž˜ ์ˆœ์„œ๋Š” ํ•„๋“œ๋“ค โ†’ ํผ ์˜ค๋ฅ˜ โ†’ ์ œ์ถœ ํ–‰๋™์ด๋‹ค. ํผ ์˜ค๋ฅ˜๋Š” ํ•ญ์ƒ ํ–‰๋™ ๋ฐ”๋กœ ์œ„์— ์˜จ๋‹ค(`formRecipe.formError.position` `beforeActions`). Web `actions`๋Š” ์ค„๋ฐ”๊ฟˆ๋˜๋Š” ๊ฐ€๋กœ ์ค„์ด๋‹ค. ๋ฒ„ํŠผ ๋‘˜์ด๋ฉด [Button](button.md)์˜ ์ˆœ์„œ(๋ณด์กฐ โ†’ ์ฃผ ํ–‰๋™)๋ฅผ ๋”ฐ๋ฅธ๋‹ค. Native ๊ธฐ๋ณธ ์ œ์ถœ ๋ฒ„ํŠผ์€ ๋งˆ์ง€๋ง‰์— ์žˆ์œผ๋ฉฐ actions ์Šฌ๋กฏ์œผ๋กœ ๊ต์ฒดํ•˜๊ฑฐ๋‚˜ ์ˆจ๊ธธ ์ˆ˜ ์žˆ๋‹ค | `form.ts` `formRecipe`, `styles.css` `.hjm-form__actions` |
112
+ | ๊ณ ์ •ยท์Šคํฌ๋กค | ํผ ํ๋ฆ„ ์•ˆ์— ๋‘”๋‹ค. ๊ธด ํ™”๋ฉด์—์„œ ์ œ์ถœ์„ ํ•˜๋‹จ์— ๊ณ ์ •ํ•˜๋ ค๋ฉด Web์€ Form์„ ์Šคํฌ๋กค ์˜์—ญ์— ๋‘๊ณ  `actions` ๋Œ€์‹  [BottomCTA](bottom-cta.md)๋กœ ๋บ€๋‹ค. Native๋Š” `actions={null}`๋กœ ๋‚ด์žฅ ๋ฒ„ํŠผ์„ ์ˆจ๊ธฐ๊ณ  `FormHandle.submit()`์œผ๋กœ ๊ฐ™์€ ์ œ์ถœ์„ ํ˜ธ์ถœํ•œ๋‹ค | `react-native/src/forms.tsx` `Form` |
113
+ | ์ข์€ ํญยทํฐ ๊ธ€์ž | โ€” | โ€” |
114
+
115
+ ```text
116
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
117
+ โ”‚ ๋ผ๋ฒจ โ”‚
118
+ โ”‚ [ ์ž…๋ ฅ ] โ”‚
119
+ โ”‚ ๋„์›€๋ง โ”‚ โ†• fieldGap(comfortable lg 20 / compact sm 12)
120
+ โ”‚ ๋ผ๋ฒจ โ”‚
121
+ โ”‚ [ ์ž…๋ ฅ ] โ”‚
122
+ โ”‚ ํผ ์˜ค๋ฅ˜(์„œ๋ฒ„ ๊ฑฐ๋ถ€) โ”‚ โ†• Web sm 12
123
+ โ”‚ [์ทจ์†Œ] [์ €์žฅ] โ”‚ โ† actions(Web) / ๋‚ด์žฅ ์ œ์ถœ ๋ฒ„ํŠผ(Native)
124
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
125
+ ```
126
+
127
+ ## ๊ผญ ์ง€ํ‚ฌ ๊ฒƒ
128
+
129
+ - Native `FormHandle`์€ `/forms`์—์„œ importํ•œ๋‹ค. `ref.current?.submit()`์€ ํ‚ค๋ณด๋“œ returnยท์‹œํŠธ footer์—์„œ ๋™์ผ ์ œ์ถœ ๊ฒฝ๋กœ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. footer๋ฅผ ์“ฐ๋ฉด `actions={null}`๋กœ ๋‚ด์žฅ ๋ฒ„ํŠผ์„ ์ˆจ๊ธฐ๊ณ , `onStatusChange`๋กœ footer์˜ loading์„ ์—ฐ๊ฒฐํ•œ๋‹ค. ์ด ๊ฒฝ๋กœ๋Š” 2026-10-06 ๋กœ๊ทธ์ธ/์‹œํŠธ ์ง€์นจ์—์„œ ์ค‘๋ณต ํผ ๊ตฌํ˜„์„ ํ”ผํ•˜๋ ค๊ณ  ์ถ”๊ฐ€ํ–ˆ๋‹ค.
130
+
131
+ - ํ•„๋“œ ์˜ค๋ฅ˜๋Š” ๊ฐ Field์˜ `error`์— ๋‘”๋‹ค. Form์˜ ์˜ค๋ฅ˜(`formError`/`error`)๋Š” ์„œ๋ฒ„ ๊ฑฐ๋ถ€ยท๋„คํŠธ์›Œํฌ ๊ฐ™์€
132
+ ํผ ๋‹จ์œ„ ์‹คํŒจ์—๋งŒ ์“ด๋‹ค. ๊ฐ™์€ ๋ฌธ์žฅ์„ ๋‘ ๊ณณ์— ๋„ฃ์ง€ ์•Š๋Š”๋‹ค.
133
+ - ๋ฌธ๊ตฌ๋Š” ๋ชจ๋‘ i18n ํ‚ค๋กœ ๋„ฃ๋Š”๋‹ค. Native๋Š” `fallbackErrorMessage`๊ฐ€ ํ•„์ˆ˜๋‹ค.
134
+ - ์ง„ํ–‰ ์ค‘ ํ‘œ์‹œ๋Š” Form์— ๋งก๊ธด๋‹ค. Web์€ `fieldset`์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ณ  `aria-busy`๋ฅผ ๊ฑด๋‹ค. Native๋Š” ๋‚ด์žฅ ์ œ์ถœ
135
+ ๋ฒ„ํŠผ์ด `loading`์ด ๋œ๋‹ค. ๋ณ„๋„ Spinner๋ฅผ ๊ฒน์น˜์ง€ ์•Š๋Š”๋‹ค.
136
+ - ์ œ์ถœ ๋ฒ„ํŠผ์€ ํ•˜๋‚˜๋‹ค. Web์€ `actions`์— `type="submit"` Button์„ ๋„ฃ๊ณ , Native๋Š” ๋‚ด์žฅ ๋ฒ„ํŠผ ๋˜๋Š” `actions` ์Šฌ๋กฏ ์ค‘ ํ•˜๋‚˜๋ฅผ ์“ด๋‹ค. Native Form
137
+ ๋‚ด์žฅ ๋ฒ„ํŠผ๊ณผ [BottomCTA](bottom-cta.md) ์ œ์ถœ์„ ํ•จ๊ป˜ ๋‘์ง€ ์•Š๋Š”๋‹ค.
138
+ - Native ํ•„๋“œ `error`๋Š” ์ œ์ถœ ์ „์—(์ž…๋ ฅ ๋ณ€๊ฒฝยทblur ๋•Œ) ์ด๋ฏธ ๋ณด์ด๊ณ  ์žˆ์–ด์•ผ ํ•œ๋‹ค. ๋ฌดํšจ ์ œ์ถœ์€ ํฌ์ปค์Šค๋งŒ ์˜ฎ๊ธฐ๊ณ 
139
+ ์–ด๋–ค ์ฝœ๋ฐฑ๋„ ๋ถ€๋ฅด์ง€ ์•Š์œผ๋ฏ€๋กœ, ๊ทธ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ผœ๋Š” ์ž๋ฆฌ๊ฐ€ ์—†๋‹ค.
140
+
141
+ ## ํ”Œ๋žซํผ ์ฐจ์ด
142
+
143
+ | ํ•ญ๋ชฉ | Web | Native |
144
+ | --- | --- | --- |
145
+ | ์ œ์ถœ ๋ฒ„ํŠผ | ์—†์Œ(`actions` ์Šฌ๋กฏ์— ์ œํ’ˆ์ด ๋„ฃ์Œ) | ๋‚ด์žฅ(`submitLabel`); `actions`๋กœ ๊ต์ฒด, `null`๋กœ ์ˆจ๊น€ |
146
+ | `onSubmit` ์ธ์ž | `FormEvent`(๊ธฐ๋ณธ ๋™์ž‘์€ ๋ง‰ํž˜) | `values` |
147
+ | ์‹คํŒจ ์ฒ˜๋ฆฌ | Promise reject๋ฅผ ์‚ผํ‚จ๋‹ค. ์ œํ’ˆ์ด `formError`๋ฅผ ์„ค์ • | rejectํ•˜๋ฉด `failed`๋กœ ๋ฐ”๊พธ๊ณ  Error ๋ฉ”์‹œ์ง€ ๋˜๋Š” `fallbackErrorMessage`๋ฅผ alert๋กœ ํ‘œ์‹œ |
148
+ | ์ฒซ ์˜ค๋ฅ˜ ํ•„๋“œ๋กœ ์ด๋™ | ์—†์Œ. ์ œํ’ˆ์ด `onSubmit` ์•ˆ์—์„œ ์ฒซ ๋ฌดํšจ ์ž…๋ ฅ์— `.focus()`ํ•œ๋‹ค | `firstInvalidFieldRef`: ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ ์ˆœ๊ฐ„ `current`๋ฅผ ์ฝ๋Š”๋‹ค. non-null์ด๋ฉด ๊ทธ ์ž…๋ ฅ์— focus์™€ ์Šคํฌ๋ฆฐ ๋ฆฌ๋” focus๋ฅผ ์˜ฎ๊ธฐ๊ณ  `onSubmit`ยท`onStatusChange` ์—†์ด ๋ฉˆ์ถ˜๋‹ค(์ƒํƒœ ๊ทธ๋Œ€๋กœ). ref๋Š” `useEffect`์—์„œ ๊ฐฑ์‹ ํ•˜๊ณ , ์ˆœ์„œ ๊ณ„์‚ฐ์€ `resolveFirstInvalidFieldFocusTarget(fieldOrder, invalidIds)`๋ฅผ ์“ด๋‹ค |
149
+ | ์ ‘๊ทผ์„ฑ ์ด๋ฆ„ | `<form>` ์†์„ฑ(`aria-label` ๋“ฑ) | `label` ํ•„์ˆ˜ |
150
+ | ๋ฐฐ์น˜ | `className`, `layoutStyle`(form ์†์„ฑ `style`๋„ ๋ฐ›์Œ) | `layoutStyle`. `style`์€ deprecated(๊ฐœ๋ฐœ ๋ชจ๋“œ 1ํšŒ ๊ฒฝ๊ณ , ๋‹ค์Œ major ์ œ๊ฑฐ) โ€” `layoutStyle` ๋˜๋Š” `density` |
151
+ | ๋ฐ–์—์„œ ์ œ์ถœ | `<form>`์ด๋ฏ€๋กœ `type="submit"` ๋ฒ„ํŠผยทEnter๋กœ ๋œ๋‹ค | `ref: Ref<FormHandle>`์˜ `submit()`์œผ๋กœ ๊ฐ™์€ validationยท์ค‘๋ณต ์ œ์ถœ ๋ฐฉ์ง€๋ฅผ ์‹คํ–‰ |
152
+
153
+ ## ํ•จ์ •
154
+
155
+ - Native๋Š” reject๋œ `Error.message`๋ฅผ ๊ทธ๋Œ€๋กœ ํ™”๋ฉด์— ๋ณด์ธ๋‹ค. ์„œ๋ฒ„ ์›๋ฌธ์ด ๋…ธ์ถœ๋˜์ง€ ์•Š๋„๋ก ์ œํ’ˆ์ด ๋ฒˆ์—ญ๋œ
156
+ ๋ฉ”์‹œ์ง€๋กœ ๊ฐ์‹ธ ๋˜์ง€๊ฑฐ๋‚˜ ๋ฉ”์‹œ์ง€ ์—†๋Š” ์‹คํŒจ๋กœ ๋‘์–ด `fallbackErrorMessage`๊ฐ€ ์“ฐ์ด๊ฒŒ ํ•œ๋‹ค.
157
+ - Web์€ reject๋ฅผ ์‚ผํ‚ค๋ฏ€๋กœ `formError`๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ์‹คํŒจ๊ฐ€ ํ™”๋ฉด์— ์•„๋ฌด๊ฒƒ๋„ ๋‚จ๊ธฐ์ง€ ์•Š๋Š”๋‹ค.
158
+ - Native `firstInvalidFieldRef.current`๋ฅผ ๋ Œ๋” ์ค‘์— ๊ฐฑ์‹ ํ•˜๋ฉด ์ฒซ ๋ Œ๋”์—๋Š” ํ•„๋“œ ref๊ฐ€ null์ด๋ผ ๋นˆ ๊ฐ’์œผ๋กœ ๋ฐ”๋กœ ๋ˆ„๋ฅด๋ฉด
159
+ ๊ทธ๋Œ€๋กœ `onSubmit`์ด ๋‚˜๊ฐ„๋‹ค. ์œ„ ์˜ˆ์ฒ˜๋Ÿผ `useEffect`์—์„œ ๊ฐฑ์‹ ํ•œ๋‹ค.
160
+ - Native ๋‚ด์žฅ ์ œ์ถœ ๋ฒ„ํŠผ์€ tone์ด `primary`๋กœ ๊ณ ์ •์ด๋‹ค. ์†Œ์…œ ๋กœ๊ทธ์ธ ์•„๋ž˜ ์‹ฌ์‚ฌ์ž ํผ์ฒ˜๋Ÿผ ๋‚ฎ์€ tone์ด ํ•„์š”ํ•˜๋ฉด Form ๋Œ€์‹ 
161
+ `actions`์— `Button tone="secondary"`๋ฅผ ๋„ฃ๊ณ  `ref.current?.submit()`์„ ํ˜ธ์ถœํ•œ๋‹ค.
162
+ - Native `error`๋Š” `string`์ด๋ผ `exactOptionalPropertyTypes`์—์„œ `undefined`๋ฅผ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค. ์กฐ๊ฑด๋ถ€ spread๋กœ ๋„˜๊ธด๋‹ค.
@@ -0,0 +1,99 @@
1
+ # Grid
2
+
3
+ - ๋‹จ๊ณ„: ์ปดํฌ๋„ŒํŠธ
4
+ - ์ƒํƒœ: ๋ฐฐํฌ
5
+ - ์ง€์›: Web ยท Native
6
+ - ์ ์šฉ: 1.12.1
7
+ - ๊ฒ€ํ† ์ผ: 2026-10-06
8
+ - ๊ทผ๊ฑฐ: [Responsive values์™€ Grid](../../responsive-grid.md), [๋ ˆ์ด์•„์›ƒ primitive ํŒ์ •](../../layout-primitives.md), recipe `gridRecipe`(`src/grid.ts`)
9
+ - ์Šคํ† ๋ฆฌ๋ถ: `๋ฐฐํฌ/์ปดํฌ๋„ŒํŠธ/๋ ˆ์ด์•„์›ƒ/๊ฒฉ์ž`
10
+
11
+ ## ์–ธ์ œ ์“ฐ๋‚˜
12
+
13
+ ์นด๋“œยทํƒ€์ผ์ฒ˜๋Ÿผ ๊ฐ™์€ ๋ชจ์–‘์˜ ์ž์‹์„ ์ฐฝ ํฌ๊ธฐ์— ๋”ฐ๋ผ ์—ด ์ˆ˜๋ฅผ ๋ฐ”๊ฟ” ๋ฐฐ์น˜ํ•  ๋•Œ ์“ด๋‹ค. ์—ด ์ˆ˜๋Š” ์ฐฝ ๋„ˆ๋น„๋ฅผ
14
+ `compact`ยท`medium`ยท`expanded`ยท`wide` ๋„ค class๋กœ ๋ฒˆ์—ญํ•ด ๊ณ ๋ฅด๊ณ , ์ž์‹ ์ˆœ์„œ๋Š” ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค(row-major).
15
+
16
+ ## ์“ฐ์ง€ ์•Š์„ ๋•Œ
17
+
18
+ | ์ƒํ™ฉ | ๋Œ€์‹  ์“ธ ๊ฒƒ |
19
+ | --- | --- |
20
+ | ํ–‰ยท์—ด ์˜๋ฏธ๊ฐ€ ์žˆ๋Š” ํ‘œ(ํ—ค๋”, ์ •๋ ฌ, ์„ ํƒ) | [DataTable](data-table.md) |
21
+ | ๋†’์ด๊ฐ€ ์ œ๊ฐ๊ฐ์ธ ์นด๋“œ ๋ฒฝ | [Masonry](masonry.md) |
22
+ | ํ•œ ๋ฐฉํ–ฅ์œผ๋กœ ์Œ“๊ธฐยท๊ฐ„๊ฒฉ | [Stack](stack.md) |
23
+ | ํŽ˜์ด์ง€ ํญยท์—ฌ๋ฐฑ ์ œํ•œ | [Container](container.md) |
24
+ | ๊ธด ๋ชฉ๋ก ๊ฐ€์ƒํ™” | [VirtualList](virtual-list.md) |
25
+
26
+ ## ๊ณต๊ฐœ ์ด๋ฆ„๊ณผ import
27
+
28
+ | ์ด๋ฆ„ | ์—ญํ•  | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `Grid` | ๊ธฐ๋ณธ | `@hjmds/react`, `/layout` | `@hjmds/react-native`, `/primitives` |
31
+
32
+ `GridReveal`์€ ์ด๋ฆ„์ด ๋น„์Šทํ•˜์ง€๋งŒ Image ๊ณ„์•ฝ์˜ ํ™•์žฅ์ด๋ฉฐ Grid์™€ ๋ฌด๊ด€ํ•˜๋‹ค.
33
+
34
+ ## ์ตœ์†Œ ์‚ฌ์šฉ ์˜ˆ
35
+
36
+ ```tsx
37
+ // Web
38
+ import { Grid } from "@hjmds/react/layout";
39
+
40
+ <Grid columns={{ compact: 1, medium: 2, expanded: 3 }} gap={{ compact: "md" }} minColumnWidth={{ compact: 160 }}>
41
+ {items.map((item) => <ProductCard key={item.id} item={item} />)}
42
+ </Grid>
43
+ ```
44
+
45
+ ```tsx
46
+ // Native
47
+ import { Grid } from "@hjmds/react-native/primitives";
48
+
49
+ <Grid columns={{ compact: 2, medium: 3 }} gap={{ compact: { row: "lg", column: "sm" } }}>
50
+ {items.map((item) => <ProductCard key={item.id} item={item} />)}
51
+ </Grid>
52
+ ```
53
+
54
+ ## ์ถ•๊ณผ ๊ธฐ๋ณธ๊ฐ’
55
+
56
+ | prop | ๊ฐ’ | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… |
57
+ | --- | --- | --- | --- |
58
+ | `columns` | `ResponsiveValue<number>`(ํ•„์ˆ˜) | โ€” | `compact`๊ฐ€ ํ•„์ˆ˜์ด๊ณ  ๋นˆ class๋Š” ์ข์€ ์ชฝ ๊ฐ’์„ ๋ฌผ๋ ค๋ฐ›๋Š”๋‹ค. ์–‘์˜ ์ •์ˆ˜๋งŒ ๋ฐ›๋Š”๋‹ค |
59
+ | `gap` | `ResponsiveValue<GridGap>`. `GridGap`์€ token(`none` ยท `xxs`~`xxxl`) ๋˜๋Š” `{ row, column }` | `md`(16) | ํ•ญ์ƒ ๊ตฌ๊ฐ„ ๊ฐ์ฒด๋กœ ์ค€๋‹ค: `{ compact: "md" }`, `{ compact: "sm", expanded: "lg" }`, `{ compact: { row: "lg", column: "sm" } }`. ๋งจ token `gap="md"`๋Š” ํƒ€์ž… ์˜ค๋ฅ˜๋‹ค |
60
+ | `minColumnWidth` | `ResponsiveValue<number>` | โ€” | ์„ ํƒ. ์—ด์ด ์ด ํญ๋ณด๋‹ค ์ข์•„์ง€๊ธฐ ์ „์— ์—ด ์ˆ˜๋ฅผ ์ค„์ธ๋‹ค. ํ•œ ์—ด์€ ๊ทธ๋Œ€๋กœ ์ค„์–ด๋“ ๋‹ค |
61
+ | `availableWidth` | ์ˆซ์ž | โ€” | ์ƒ๋žตํ•˜๋ฉด ๋ Œ๋”๋œ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์žฐ๋‹ค(Web `ResizeObserver`, Native `onLayout`) |
62
+ | Native `onLayoutResolved` | `(layout: ResolvedGridLayout) => void` | โ€” | `layout`์€ `{ windowClass, flow: "row-major", requestedColumns, columns, rowGap, columnGap, columnWidth }`. ์—ด ์ˆ˜๊ฐ€ ์ค„์—ˆ๋Š”์ง€(`columns < requestedColumns`) ํ™•์ธํ•  ๋•Œ ์“ด๋‹ค |
63
+ | Web `windowWidth` | ์ˆซ์ž | `window.innerWidth`(SSR 320) | ํ…Œ์ŠคํŠธยทSSR์—์„œ ์ฐฝ ๋„ˆ๋น„๋ฅผ ๋ฎ๋Š”๋‹ค |
64
+
65
+ - `ResponsiveValue<V>`๋Š” `{ compact: V; medium?: V; expanded?: V; wide?: V }`๋‹ค(`@hjmds/design-contracts/responsive`).
66
+
67
+ ## ๋ฐฐ์น˜
68
+
69
+ | ํ•ญ๋ชฉ | ๊ฐ’ | ๊ทผ๊ฑฐ |
70
+ | --- | --- | --- |
71
+ | ํฌ๊ธฐ | Grid๋Š” ๋ถ€๋ชจ ํญ์„ ์ฑ„์šฐ๊ณ  ๋ฐ”๊นฅ ์—ฌ๋ฐฑ์„ ๊ฐ–์ง€ ์•Š๋Š”๋‹ค. ์—ด ํญ์€ Grid๊ฐ€ ์‹ค์ œ๋กœ ๋ฐ›์€ ํญ(`availableWidth`)์œผ๋กœ ๊ณ„์‚ฐํ•œ๋‹ค | `grid.ts` `resolveGridLayout`, `styles.css` `.hjm-grid` |
72
+ | ๊ฐ„๊ฒฉ | ๊ธฐ๋ณธ ๊ฐ„๊ฒฉ `md`(16). ํŽ˜์ด์ง€ ์ขŒ์šฐ ์—ฌ๋ฐฑ์€ [Container](container.md)ยทLayout์ด ์ค€๋‹ค | `grid.ts` `gridDefaults` |
73
+ | ์ˆœ์„œยท์ •๋ ฌ | ์ž์‹ ์ˆœ์„œ๋Š” ํ–‰ ์šฐ์„ (row-major)์ด๊ณ  RTL์—์„œ๋Š” ์˜ค๋ฅธ์ชฝ์—์„œ ์‹œ์ž‘ํ•œ๋‹ค | `grid.ts` `gridDefaults` |
74
+ | ๊ณ ์ •ยท์Šคํฌ๋กค | โ€” | โ€” |
75
+ | ์ข์€ ํญยทํฐ ๊ธ€์ž | ์ฐฝ ๋„ˆ๋น„ class๋Š” `breakpoint` ๊ธฐ์ค€์ด๋‹ค: `compact` 0 ยท `medium` 600 ยท `expanded` 960 ยท `wide` 1280. ์—ด ์ˆ˜๋Š” ์ด class๋กœ ๊ณ ๋ฅธ๋‹ค. `columns`๋Š” `compact` ๊ฐ’๋ถ€ํ„ฐ ์ •ํ•˜๊ณ  ๋„“์€ class์—์„œ ๋Š˜๋ฆฐ๋‹ค(๋นˆ class๋Š” ์ข์€ ์ชฝ ๊ฐ’์„ ๋ฌผ๋ ค๋ฐ›๋Š”๋‹ค). `minColumnWidth`๋ฅผ ์ฃผ๋ฉด ์—ด์ด ๊ทธ ํญ๋ณด๋‹ค ์ข์•„์ง€๊ธฐ ์ „์— ์—ด ์ˆ˜๊ฐ€ ์ค„์–ด๋“ ๋‹ค. ํฐ ๊ธ€์ž๋กœ ์นด๋“œ๊ฐ€ ์ข์•„์งˆ ๋•Œ ์ด ๊ฐ’์œผ๋กœ ์—ด์„ ์ค„์ธ๋‹ค | `responsive.ts` `resolveWindowClass`, `foundations.ts` `breakpoint` |
76
+
77
+ ## ๊ผญ ์ง€ํ‚ฌ ๊ฒƒ
78
+
79
+ - ๊ฐ„๊ฒฉ์€ gap token์œผ๋กœ๋งŒ ์ค€๋‹ค. ์ˆซ์ž px๋‚˜ ์ž์‹ margin์œผ๋กœ ๊ฐ„๊ฒฉ์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค.
80
+ - class ์ด๋ฆ„ ์˜คํƒ€ยท์•Œ ์ˆ˜ ์—†๋Š” keyยท0 ์ดํ•˜ ์—ด ์ˆ˜๋Š” resolver๊ฐ€ ๋˜์ง„๋‹ค. ์กฐ์šฉํžˆ ๋ฌด์‹œ๋˜์ง€ ์•Š๋Š”๋‹ค.
81
+ - Grid๋Š” ์ž์‹์˜ ์˜๋ฏธ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค. ๋ชฉ๋ก์ด๋ฉด ์ž์‹์— ์ œํ’ˆ์ด ๋ชฉ๋ก ์˜๋ฏธ๋ฅผ ์ค€๋‹ค.
82
+ - Web ๋ฐฐ์น˜๋Š” `layoutStyle`๋กœ ํ•œ๋‹ค(์ƒ‰ยทํ…Œ๋‘๋ฆฌ ๋“ฑ ์‹œ๊ฐ key๋Š” ๋ฐ›์ง€ ์•Š๋Š”๋‹ค).
83
+
84
+ ## ํ”Œ๋žซํผ ์ฐจ์ด
85
+
86
+ | ํ•ญ๋ชฉ | Web | Native |
87
+ | --- | --- | --- |
88
+ | ๊ตฌํ˜„ | CSS Grid(`repeat(n, minmax(0, 1fr))`) | `flexWrap` ํ–‰ + ์ž์‹๋งˆ๋‹ค ๊ณ ์ • ํญ ์…€ View |
89
+ | ์ฐฝ ๋„ˆ๋น„ | `window.innerWidth`, `windowWidth`๋กœ ๋ฎ๊ธฐ(ํ…Œ์ŠคํŠธยทSSR) | `useWindowDimensions` |
90
+ | SSR ์ฒซ ๋ Œ๋” | ์ฐฝ ๋„ˆ๋น„๋ฅผ ๋ชจ๋ฅด๋ฉด 320์œผ๋กœ ๊ณ„์‚ฐ | ํ•ด๋‹น ์—†์Œ |
91
+ | ์…€ ์Šคํƒ€์ผ | ์—†์Œ(์ž์‹์ด ์ง์ ‘) | `itemStyle` |
92
+ | ๊ฒฐ๊ณผ ํ†ต์ง€ | `data-columns`ยท`data-state` ์†์„ฑ | `onLayoutResolved(layout)` |
93
+ | ๋ฐฐ์น˜ prop | `layoutStyle`, `className`, `style` | `style`(View). Grid๋Š” layout primitive๋ผ deprecated ๋Œ€์ƒ์ด ์•„๋‹ˆ๊ณ  `layoutStyle`์€ ์—†๋‹ค |
94
+ | RTL | CSS๊ฐ€ ์ฒ˜๋ฆฌ | Provider์˜ `environment.direction` ์ ์šฉ |
95
+
96
+ ## ํ•จ์ •
97
+
98
+ - Native๋Š” ์ž์‹๋งˆ๋‹ค ์…€ View๋ฅผ ๊ฐ์‹ธ๊ณ  ํญ์„ ๊ธฐ๊ธฐ ํ”ฝ์…€ ๋‹จ์œ„๋กœ ๋‚ด๋ฆผํ•œ๋‹ค. ์…€ ์ž์ฒด๋ฅผ ๊พธ๋ฐ€ ๋•Œ๋Š” ์ž์‹์ด ์•„๋‹ˆ๋ผ `itemStyle`์„ ์“ด๋‹ค.
99
+ - Web SSR์—์„œ๋Š” 320 ๊ธฐ์ค€ ์—ด ์ˆ˜๋กœ ๊ทธ๋ ธ๋‹ค๊ฐ€ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋’ค ์‹ค์ œ ๋„ˆ๋น„๋กœ ๋ฐ”๋€๋‹ค. ์ฒซ ํ™”๋ฉด ์—ด ์ˆ˜๊ฐ€ ์ค‘์š”ํ•˜๋ฉด `windowWidth`๋ฅผ ๋„˜๊ธด๋‹ค.
@@ -0,0 +1,87 @@
1
+ # Heading
2
+
3
+ - ๋‹จ๊ณ„: ์ปดํฌ๋„ŒํŠธ
4
+ - ์ƒํƒœ: ๋ฐฐํฌ
5
+ - ์ง€์›: Web ยท Native
6
+ - ์ ์šฉ: 1.12.1
7
+ - ๊ฒ€ํ† ์ผ: 2026-10-06
8
+ - ๊ทผ๊ฑฐ: [Heading](../../heading.md), recipe `headingRecipe`(`src/heading.ts`)
9
+ - ์Šคํ† ๋ฆฌ๋ถ: `๋ฐฐํฌ/์ปดํฌ๋„ŒํŠธ/๊ธ€์ž์™€ ์•„์ด์ฝ˜/์ œ๋ชฉ`
10
+
11
+ ## ์–ธ์ œ ์“ฐ๋‚˜
12
+
13
+ ์ž๋ฆฌ๋ฅผ ๋ชจ๋ฅด๋Š” ํฐ ์ œ๋ชฉ ๊ธ€์ž ํ•˜๋‚˜๊ฐ€ ํ•„์š”ํ•  ๋•Œ ์“ด๋‹ค. ๋žœ๋”ฉ ํžˆ์–ด๋กœ, ๊ฒฐ๊ณผ ํ™”๋ฉด์˜ ํฐ ์ œ๋ชฉ,
14
+ ์นด๋“œยทํ‘œยท๋นˆ ์ƒํƒœ ์•ˆ์˜ ์ œ๋ชฉ์ฒ˜๋Ÿผ `Text`์˜ ์ตœ๋Œ€ ํฌ๊ธฐ(24px)๋กœ๋Š” ๋ชจ์ž๋ผ๊ฑฐ๋‚˜ ๋ฌธ์„œ ์ œ๋ชฉ ๋‹จ๊ณ„๋ฅผ
15
+ ์‹ค์ œ๋กœ ๋‚ด์•ผ ํ•˜๋Š” ์ž๋ฆฌ๋‹ค. ์ฃผ๋ณ€ ์—ฌ๋ฐฑ์€ ๊ฐ–์ง€ ์•Š๋Š”๋‹ค. ๋‹ด๋Š” ๋ธ”๋ก์ด ์ •ํ•œ๋‹ค.
16
+
17
+ ## ์“ฐ์ง€ ์•Š์„ ๋•Œ
18
+
19
+ | ์ƒํ™ฉ | ๋Œ€์‹  ์“ธ ๊ฒƒ |
20
+ | --- | --- |
21
+ | ํ™”๋ฉด ์ฒซ ์ œ๋ชฉ(์—ฌ๋ฐฑยทeyebrowยท๋ณด์กฐ ๋ฌธ์žฅยท๋ณด์กฐ ํ–‰๋™ ํฌํ•จ) | [Top](top.md) |
22
+ | ๋ณธ๋ฌธ ์ค‘๊ฐ„ ๋ฌถ์Œ์˜ ์ œ๋ชฉ ํ–‰ยท์„ค๋ช… | [Section](section.md) |
23
+ | ๋ณธ๋ฌธยท๋ผ๋ฒจยท์บก์…˜ ํฌ๊ธฐ์˜ ๊ธ€์ž | [Text](text.md) |
24
+
25
+ ## ๊ณต๊ฐœ ์ด๋ฆ„๊ณผ import
26
+
27
+ | ์ด๋ฆ„ | ์—ญํ•  | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `Heading` | ๊ธฐ๋ณธ | `@hjmds/react`, `/heading` | `@hjmds/react-native`, `/heading` |
30
+
31
+ ## ์ตœ์†Œ ์‚ฌ์šฉ ์˜ˆ
32
+
33
+ ```tsx
34
+ // Web
35
+ import { Heading } from "@hjmds/react/heading";
36
+
37
+ <Heading level="level2">{t("result.title")}</Heading>
38
+ ```
39
+
40
+ ```tsx
41
+ // Native
42
+ import { Heading } from "@hjmds/react-native/heading";
43
+
44
+ <Heading level="level4" semanticLevel={3}>{t("card.title")}</Heading>
45
+ ```
46
+
47
+ ## ์ถ•๊ณผ ๊ธฐ๋ณธ๊ฐ’
48
+
49
+ | prop | ๊ฐ’ | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… |
50
+ | --- | --- | --- | --- |
51
+ | `level`(ํ•„์ˆ˜) | `level1` 40px ยท `level2` 32px ยท `level3` 24px ยท `level4` 20px ยท `level5` 18px | โ€” | recipe ๊ธฐ๋ณธ์€ `level3`์ด์ง€๋งŒ ๋‘ renderer ๋ชจ๋‘ prop์„ ํ•„์ˆ˜๋กœ ๋ฐ›๋Š”๋‹ค |
52
+ | `semanticLevel` | 1~6 | `level`์˜ ์ˆซ์ž | ํฌ๊ฒŒ ๋ณด์ด์ง€๋งŒ ๊ตฌ์กฐ์ƒ h4์ธ ์นด๋“œ ์ œ๋ชฉ์ฒ˜๋Ÿผ ์‹œ๊ฐ ํฌ๊ธฐ์™€ ๋ฌธ์„œ ๋‹จ๊ณ„๊ฐ€ ๋‹ค๋ฅด๋ฉด ๋‘˜์„ ๋”ฐ๋กœ ์ ๋Š”๋‹ค |
53
+ | `layoutStyle` | ๋ฐฐ์น˜ key(marginยทํญยท์ •๋ ฌ ๋“ฑ) | โ€” | ๋ฐ”๊นฅ ๋ฐฐ์น˜๋งŒ. ํฌ๊ธฐยท์ค„ ๋†’์ดยท๊ตต๊ธฐยท์ƒ‰์€ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค |
54
+
55
+ - ๋ฒ”์œ„ ๋ฐ– ๊ฐ’์€ `TypeError`๋กœ ๊ฑฐ๋ถ€๋œ๋‹ค.
56
+
57
+ ## ๋ฐฐ์น˜
58
+
59
+ | ํ•ญ๋ชฉ | ๊ฐ’ | ๊ทผ๊ฑฐ |
60
+ | --- | --- | --- |
61
+ | ํฌ๊ธฐ | ์ค„ ๋†’์ด: `level1` 48 ยท `level2` 40 ยท `level3` 32 ยท `level4` 28 ยท `level5` 26 | `foundations.ts` `heading`, `typography` |
62
+ | ๊ฐ„๊ฒฉ | Heading์€ margin์ด 0์ด๋‹ค(Web `.hjm-heading`). ์œ„์•„๋ž˜ ๊ฐ„๊ฒฉ์€ ๊ฐ์‹ธ๋Š” StackยทSection์˜ `gap`์ด ์ •ํ•œ๋‹ค | `styles.css` `.hjm-heading` |
63
+ | ์ˆœ์„œยท์ •๋ ฌ | ํ•œ ๋ธ”๋ก์— Heading์€ ํ•˜๋‚˜๋ฅผ ๋งจ ์œ„์— ๋‘”๋‹ค | โ€” |
64
+ | ๊ณ ์ •ยท์Šคํฌ๋กค | โ€” | โ€” |
65
+ | ์ข์€ ํญยทํฐ ๊ธ€์ž | ํญ์€ ๋ถ€๋ชจ๋ฅผ ๋”ฐ๋ฅด๊ณ  ๊ธด ์ œ๋ชฉ์€ ์ค„๋ฐ”๊ฟˆํ•œ๋‹ค. Web์€ `--hjm-text-scale`๋กœ ํฌ๊ธฐยท์ค„ ๋†’์ด๋ฅผ ํ•จ๊ป˜ ํ‚ค์šด๋‹ค | `styles.css` `.hjm-heading` |
66
+
67
+ ## ๊ผญ ์ง€ํ‚ฌ ๊ฒƒ
68
+
69
+ - ์ œ๋ชฉ ๋ฌธ๊ตฌ๋Š” i18n ํ‚ค๋กœ ๋„ฃ๋Š”๋‹ค. Web์€ ์ค„๋ฐ”๊ฟˆ์„ `overflow-wrap: anywhere`๋กœ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์ž๋ฅด์ง€ ์•Š๋Š”๋‹ค.
70
+ - ํฌ๊ธฐ๋ฅผ ๋งž์ถ”๋ ค๊ณ  `semanticLevel`์„ ๋ฐ”๊พธ๊ฑฐ๋‚˜, ๊ตฌ์กฐ๋ฅผ ๋งž์ถ”๋ ค๊ณ  `level`์„ ์ค„์ด์ง€ ์•Š๋Š”๋‹ค.
71
+ - ์ œ๋ชฉ ์•„๋ž˜ ๊ฐ„๊ฒฉ์€ Heading์— ์ฃผ์ง€ ์•Š๊ณ  ๊ฐ์‹ธ๋Š” StackยทSection์˜ gap์œผ๋กœ ์ •ํ•œ๋‹ค.
72
+ - ์ƒ‰ยทํฌ๊ธฐยท๊ตต๊ธฐ๋ฅผ ๋ฎ์ง€ ์•Š๋Š”๋‹ค. ์ƒ‰์€ ํ…Œ๋งˆ์˜ `content.primary`๋ฅผ ๋”ฐ๋ฅธ๋‹ค.
73
+
74
+ ## ํ”Œ๋žซํผ ์ฐจ์ด
75
+
76
+ | ํ•ญ๋ชฉ | Web | Native |
77
+ | --- | --- | --- |
78
+ | ์ถœ๋ ฅ | `h1`~`h6` ์š”์†Œ | `Text` + `accessibilityRole="header"` + `aria-level` |
79
+ | ์ถ”๊ฐ€ prop | `HTMLAttributes`(`id`, `className`, `style` ๋“ฑ), ref | ์—†์Œ |
80
+ | ๋ฐฐ์น˜ | `layoutStyle`. `style`๋„ ํ•ฉ์ณ์ง„๋‹ค(ํฌ๊ธฐ ๋ณ€์ˆ˜๋Š” ๋ฎ์ด์ง€ ์•Š์Œ) | `layoutStyle`. `style`(TextStyle)์€ deprecated(๊ฐœ๋ฐœ ๋ชจ๋“œ 1ํšŒ ๊ฒฝ๊ณ , ๋‹ค์Œ major ์ œ๊ฑฐ) โ€” `layoutStyle` ๋˜๋Š” `level` |
81
+ | ๊ธ€์ž ๋ฐฐ์œจ | `--hjm-text-scale`๋กœ ํ™•๋Œ€ | Native `Text`์˜ ๊ธ€์ž ๋ฐฐ์œจ |
82
+
83
+ ## ํ•จ์ •
84
+
85
+ - Web `style`์€ ์ด์ œ ๋ฒ„๋ ค์ง€์ง€ ์•Š๊ณ  ํ•ฉ์ณ์ง„๋‹ค. ๋‹ค๋งŒ ํฌ๊ธฐยท์ค„ ๋†’์ดยท๊ตต๊ธฐ ๋ณ€์ˆ˜(`--hjm-heading-*`)๋Š” recipe๊ฐ€ ๋งˆ์ง€๋ง‰์— ๋ฎ์œผ๋ฏ€๋กœ
86
+ `style`๋กœ ํฌ๊ธฐ๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์—†๋‹ค. ๋ฐฐ์น˜๋Š” `layoutStyle`๋กœ ์ค€๋‹ค.
87
+ - Native `style`(deprecated)์€ ์•„์ง ๋งˆ์ง€๋ง‰์— ํ•ฉ์ณ์ ธ ์ƒ‰ยทํฌ๊ธฐ๊นŒ์ง€ ๋ฐ”๋€๋‹ค. ์“ฐ์ง€ ๋ง๊ณ  `layoutStyle`ยท`level`๋กœ ์˜ฎ๊ธด๋‹ค.
@@ -0,0 +1,126 @@
1
+ # IconButton
2
+
3
+ - ๋‹จ๊ณ„: ์ปดํฌ๋„ŒํŠธ
4
+ - ์ƒํƒœ: ๋ฐฐํฌ
5
+ - ์ง€์›: Web ยท Native
6
+ - ์ ์šฉ: 1.12.1
7
+ - ๊ฒ€ํ† ์ผ: 2026-10-06
8
+ - ๊ทผ๊ฑฐ: recipe `iconButtonRecipe`(`src/icon-button-recipe.ts`), NotificationBell ๊ณ„์•ฝ: [Compound controls](../../compound-controls.md#notificationbell)
9
+ - ์Šคํ† ๋ฆฌ๋ถ: `๋ฐฐํฌ/์ปดํฌ๋„ŒํŠธ/๋™์ž‘/์•„์ด์ฝ˜ ๋ฒ„ํŠผ` ยท `๋ฐฐํฌ/์ปดํฌ๋„ŒํŠธ/์ƒํƒœ์™€ ์•Œ๋ฆผ/์•Œ๋ฆผ ๋ฒจ`
10
+
11
+ ## ์–ธ์ œ ์“ฐ๋‚˜
12
+
13
+ ๋ณด์ด๋Š” ๊ธ€์ž ์—†์ด ์•„์ด์ฝ˜๋งŒ์œผ๋กœ ํ‘œ์‹œํ•˜๋Š” ํ–‰๋™์— ์“ด๋‹ค. ๋‹ซ๊ธฐ, ๋’ค๋กœ, ๋”๋ณด๊ธฐ, ๊ณต์œ , ์ฆ๊ฒจ์ฐพ๊ธฐ ํ† ๊ธ€์ฒ˜๋Ÿผ
14
+ ๋ชจ์–‘์ด ๋„๋ฆฌ ํ†ตํ•˜๋Š” ํ–‰๋™์ด๋‹ค. ์ด๋ฆ„์€ ํ˜„์ง€ํ™”ํ•œ `label`์ด ์ ‘๊ทผ์„ฑ ์ด๋ฆ„์œผ๋กœ ์ฝํžŒ๋‹ค.
15
+ ๋ฏธํ™•์ธ ์•Œ๋ฆผ ์ˆ˜๊ฐ€ ๋ถ™์€ ์•Œ๋ฆผ ์ง„์ž…์ ์€ `NotificationBell`์„ ์“ด๋‹ค.
16
+
17
+ ## ์“ฐ์ง€ ์•Š์„ ๋•Œ
18
+
19
+ | ์ƒํ™ฉ | ๋Œ€์‹  ์“ธ ๊ฒƒ |
20
+ | --- | --- |
21
+ | ๊ธ€์ž ๋ผ๋ฒจ์ด ์žˆ๋Š” ํ–‰๋™ | [Button](button.md) |
22
+ | ๋‹ค๋ฅธ ํŽ˜์ด์ง€ยทURL๋กœ ์ด๋™ | [Link](link.md) |
23
+ | ํ™”๋ฉด ์œ„์— ๋–  ์žˆ๋Š” ์ฃผ ํ–‰๋™ | [FloatingActionButton](floating-action-button.md) |
24
+ | ์—ฌ๋Ÿฌ ์•„์ด์ฝ˜ ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ฆ„ | [ToggleGroup](toggle-group.md), [SegmentedControl](segmented-control.md) |
25
+ | ๋ˆ„๋ฅผ ์ˆ˜ ์—†๋Š” ๊ทธ๋ฆผ | [Icon](icon.md) |
26
+
27
+ ## ๊ณต๊ฐœ ์ด๋ฆ„๊ณผ import
28
+
29
+ | ์ด๋ฆ„ | ์—ญํ•  | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `IconButton` | ๊ธฐ๋ณธ | `@hjmds/react`, `/actions` | `@hjmds/react-native`, `/actions`, `/bottom-cta` |
32
+ | `NotificationBell` | ํ™•์žฅ โ€” ๋ฏธํ™•์ธ ์ˆ˜ ๋ฐฐ์ง€ + ์ฆ๊ฐ€ ์‹œ ํ•œ ๋ฒˆ ํ”๋“ค๋ฆผ | `/notification-bell` | `/notification-bell` |
33
+
34
+ `NotificationBell`์€ granular subpath๋กœ๋งŒ import ๋œ๋‹ค. ์ถ”๊ฐ€ peer๋Š” ์—†๋‹ค.
35
+
36
+ ## ์ตœ์†Œ ์‚ฌ์šฉ ์˜ˆ
37
+
38
+ ```tsx
39
+ // Web
40
+ import { IconButton } from "@hjmds/react/actions";
41
+ import { Icon } from "@hjmds/react/display";
42
+
43
+ <IconButton label={t("common.close")} onClick={close}>
44
+ <Icon name="close" />
45
+ </IconButton>
46
+ ```
47
+
48
+ ```tsx
49
+ // Native
50
+ import { IconButton } from "@hjmds/react-native/actions";
51
+ import { Icon } from "@hjmds/react-native/primitives";
52
+ import { NotificationBell } from "@hjmds/react-native/notification-bell";
53
+ // renderGlyph๋Š” createLucideGlyph๋กœ ๋งŒ๋“ ๋‹ค(icon.md ์ฐธ๊ณ ).
54
+
55
+ <IconButton label={t("common.more")} onPress={openMenu}>
56
+ <Icon descriptor={{ name: "more" }} renderGlyph={renderGlyph} />
57
+ </IconButton>
58
+
59
+ <NotificationBell
60
+ label={t("inbox.open", { count: unread })}
61
+ count={unread}
62
+ icon={<Icon descriptor={{ name: "notifications" }} renderGlyph={renderGlyph} />}
63
+ onPress={openInbox}
64
+ active={isFocused}
65
+ />
66
+ ```
67
+
68
+ ## ์ถ•๊ณผ ๊ธฐ๋ณธ๊ฐ’
69
+
70
+ | prop | ๊ฐ’ | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… |
71
+ | --- | --- | --- | --- |
72
+ | `tone` | `ghost` ยท `primary` ยท `secondary` ยท `danger` | `ghost` | โ€” |
73
+ | `size` | `small` 36 ยท `medium` 44 ยท `large` 52 | `medium` | `small`์€ ํ„ฐ์น˜ ์—ฌ๋ฐฑ 4 ์ถ”๊ฐ€. ์•„์ด์ฝ˜ ํฌ๊ธฐ๋Š” ๊ฐ๊ฐ `sm`ยท`md`ยท`lg` |
74
+ | `shape` | `rounded` ยท `circle` | `rounded` | โ€” |
75
+ | `selected` | `true` ยท `false` | โ€” | ์ฃผ๋ฉด ํ† ๊ธ€์ด ๋œ๋‹ค(Web `aria-pressed`, Native ์ ‘๊ทผ์„ฑ state) |
76
+ | `loading` | `true` ยท `false` | โ€” | ๋ˆ„๋ฆ„์„ ๋ง‰๊ณ  ์Šคํ”ผ๋„ˆ๋กœ ๋ฐ”๊พผ๋‹ค. Native๋Š” ๊ธฐ๋ณธ์œผ๋กœ ํฌ์ปค์Šค๋ฅผ ์œ ์ง€ํ•œ๋‹ค(`disableWhileLoading`์œผ๋กœ๋งŒ ์˜› ๋™์ž‘) |
77
+ | `loading` ์ค‘ `renderLoadingIndicator`(Native) | `(props: { color: string; size: "small" }) => ReactNode` | ๊ธฐ๋ณธ ์Šคํ”ผ๋„ˆ | ์Šคํ”ผ๋„ˆ๋ฅผ ๋ฐ”๊ฟ€ ๋•Œ๋งŒ |
78
+ | Web `onClick` ยท Native `onPress` | Web `(event: MouseEvent<HTMLButtonElement>) => void` ยท Native `(event: GestureResponderEvent) => void` | โ€” | Native๋Š” `onLongPress`๋„ ๋ฐ›๋Š”๋‹ค |
79
+ | `count`(NotificationBell) | 0 ์ด์ƒ์˜ ์ •์ˆ˜ | โ€” | ์•„๋‹ˆ๋ฉด `RangeError` |
80
+ | `onPress`(NotificationBell) | `() => void` | ํ•„์ˆ˜ | WebยทNative ๋ชจ๋‘ `onPress`๋‹ค(Web๋„ `onClick`์ด ์•„๋‹ˆ๋‹ค) |
81
+ | `active`(NotificationBell) | `true` ยท `false` | `true` | โ€” |
82
+ | `disabled`(NotificationBell) | `true` ยท `false` | `false` | โ€” |
83
+ | Web `layoutStyle`(NotificationBell) | `HjmCompositionStyleProp` | โ€” | ๋ฃจํŠธ ๋ฐฐ์น˜. ๊ธฐ๋ณธ `alignSelf: "flex-start"`๋ณด๋‹ค ์šฐ์„ ํ•˜์ง€๋งŒ `width`๋Š” ๋ฌด์‹œ๋œ๋‹ค(๋ฃจํŠธ๋Š” ํ•ญ์ƒ `max-content`). ๋–  ์žˆ๋Š” ๋ฐฐ์ง€๊ฐ€ ๋ฃจํŠธ์˜ ๋ ๋ชจ์„œ๋ฆฌ์— ๋ถ™์–ด ์žˆ์–ด ๋ฃจํŠธ๊ฐ€ ๋„“์–ด์ง€๋ฉด ์•„์ด์ฝ˜์—์„œ ๋–จ์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. marginยท`alignSelf`๋กœ ๋ฐฐ์น˜ํ•œ๋‹ค. ๋ฏธ๊ฒŒ์‹œ(1.12.1 ์ดํ›„) |
84
+
85
+ ## ๋ฐฐ์น˜
86
+
87
+ | ํ•ญ๋ชฉ | ๊ฐ’ | ๊ทผ๊ฑฐ |
88
+ | --- | --- | --- |
89
+ | ํฌ๊ธฐ | ์ง€๋ฆ„: `small` 36 ยท `medium` 44 ยท `large` 52. `small`์€ hitSlop 4๋ฅผ ๋”ํ•ด ํ„ฐ์น˜ ์˜์—ญ์„ 44(`control.minTouchTarget`)์— ๋งž์ถ˜๋‹ค. ์•„์ด์ฝ˜ ํฌ๊ธฐ๋Š” `glyph.sm` 20 ยท `glyph.md` 24 ยท `glyph.lg` 28์ด๋‹ค | `icon-button-recipe.ts` `iconButtonRecipe` |
90
+ | ๊ฐ„๊ฒฉ | [TopBar](top-bar.md)๊ฐ€ ์Šฌ๋กฏ ์ตœ์†Œ ํญ 44, ์Šฌ๋กฏ ์•ˆ ๊ฐ„๊ฒฉ `spacing.xs`(8), ์ขŒ์šฐ ์—ฌ๋ฐฑ `spacing.md`(16)๋ฅผ ์ค€๋‹ค. NotificationBell ๋ฐฐ์ง€๊ฐ€ ๋ฒ„ํŠผ ๋ฐ–์œผ๋กœ ๋„˜์น  ์ˆ˜ ์žˆ์œผ๋‹ˆ ์˜† ์š”์†Œ์™€ ๊ฐ„๊ฒฉ์„ ๋‘”๋‹ค | `component-recipes.ts` `topBarRecipe`, `styles.css` `.hjm-top-bar` |
91
+ | ์ˆœ์„œยท์ •๋ ฌ | ํ™”๋ฉด ๋งจ ์œ„์—์„œ๋Š” TopBar์˜ `leading`(๋’ค๋กœยท๋‹ซ๊ธฐ)๊ณผ `actions`/`trailing`(๋”๋ณด๊ธฐยท๊ณต์œ ยท์•Œ๋ฆผ)์— ๋‘”๋‹ค. ํ–‰ ๋(๋ชฉ๋กยท์นด๋“œ)์—๋Š” `size="small"` ๋˜๋Š” `medium`, `tone="ghost"`๋ฅผ ์“ด๋‹ค. NotificationBell์€ IconButton ์˜ค๋ฅธ์ชฝ ์œ„ ๋ชจ์„œ๋ฆฌ(`end: 0`, `top: 0`)์— [CounterBadge](counter-badge.md) `floating`์„ ๊ฒน์นœ๋‹ค | `react-native/src/notification-bell.tsx` |
92
+ | ๊ณ ์ •ยท์Šคํฌ๋กค | โ€” | โ€” |
93
+ | ์ข์€ ํญยทํฐ ๊ธ€์ž | โ€” | โ€” |
94
+
95
+ ```text
96
+ TopBar
97
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
98
+ โ”‚ [โ†] ํ™”๋ฉด ์ œ๋ชฉ [๋ฒจยณ][โ‹ฏ] โ”‚ โ† leading 44 / trailing ๊ฐ„๊ฒฉ xs 8, ์ขŒ์šฐ ์—ฌ๋ฐฑ md 16
99
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
100
+ ```
101
+
102
+ ## ๊ผญ ์ง€ํ‚ฌ ๊ฒƒ
103
+
104
+ - `label`์€ ๋ชจ์–‘์ด ์•„๋‹ˆ๋ผ ํ–‰๋™์„ ๋งํ•œ๋‹ค("๋‹๋ณด๊ธฐ"๊ฐ€ ์•„๋‹ˆ๋ผ "๊ฒ€์ƒ‰"). Native๋Š” ๋นˆ `label`ยท๋นˆ children์„ `TypeError`๋กœ ๊ฑฐ๋ถ€ํ•œ๋‹ค.
105
+ - ์•ˆ์˜ Icon์€ ์žฅ์‹์œผ๋กœ ๋‘”๋‹ค. ์ด๋ฆ„์€ ๋ฒ„ํŠผ์ด ์ด๋ฏธ ์ฝ๋Š”๋‹ค.
106
+ - ๋ฐฐ์น˜๋Š” `layoutStyle`๋กœ๋งŒ ํ•œ๋‹ค. ์ง€๋ฆ„ยทradiusยท๋ฐฐ๊ฒฝ์„ `style`/`className`์œผ๋กœ ๋ฎ์ง€ ์•Š๋Š”๋‹ค.
107
+ - NotificationBell์˜ `label`์— ๋ฏธํ™•์ธ ์ˆ˜๋ฅผ ํฌํ•จํ•œ ํ˜„์ง€ํ™” ๋ฌธ๊ตฌ๋ฅผ ๋„ฃ๋Š”๋‹ค. ๋ฐฐ์ง€์™€ ๊ทธ๋ฆผ์€ ์ ‘๊ทผ์„ฑ์—์„œ ์ˆจ๊ฒจ์ง„๋‹ค.
108
+ ๋ˆŒ๋Ÿฌ๋„ ์ฝ์Œ ์ฒ˜๋ฆฌ๋Š” ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ฝ์Œ ์ฒ˜๋ฆฌ๋Š” ์ œํ’ˆ์ด ํ•œ๋‹ค.
109
+ - Native์—์„œ ํ™”๋ฉด์ด ๊ฐ€๋ ค์ง„ ์ฑ„ mounted๋ฉด `active={false}`๋ฅผ ๋„˜๊ฒจ ํ”๋“ค๋ฆผ์„ ๋ฉˆ์ถ˜๋‹ค.
110
+
111
+ ## ํ”Œ๋žซํผ ์ฐจ์ด
112
+
113
+ | ํ•ญ๋ชฉ | Web | Native |
114
+ | --- | --- | --- |
115
+ | ์ด๋ฒคํŠธ | `onClick` | `onPress`, `onLongPress` |
116
+ | ๊ธฐ๋ณธ `type` | `"button"` | ํ•ด๋‹น ์—†์Œ |
117
+ | ์Šคํ”ผ๋„ˆ ๊ต์ฒด | ์—†์Œ | `renderLoadingIndicator` |
118
+ | ๋น„ํ™œ์„ฑ์ด์ง€๋งŒ ํฌ์ปค์Šค ์œ ์ง€ | `aria-disabled` | `loading` ๊ธฐ๋ณธ ๋™์ž‘ |
119
+ | NotificationBell ํ”๋“ค๋ฆผ ์ •์ง€ | ๋ฌธ์„œ ์ˆจ๊น€ยทreduced motion | AppState ๋น„ํ™œ์„ฑยทreduced motion |
120
+ | ๋ฐฐ์น˜ | `layoutStyle`(IconButtonยทNotificationBell, NotificationBell์€ `width` ๋ฌด์‹œ) | IconButton `layoutStyle`. `style`์€ deprecated(๊ฐœ๋ฐœ ๋ชจ๋“œ 1ํšŒ ๊ฒฝ๊ณ , ๋‹ค์Œ major ์ œ๊ฑฐ) โ€” `layoutStyle` ๋˜๋Š” `tone`ยท`size`ยท`shape`. NotificationBell์€ ๋ฐฐ์น˜ prop์ด ์—†์–ด ๊ฐ์‹ธ๋Š” View๋กœ ๋ฐฐ์น˜ํ•œ๋‹ค |
121
+
122
+ ## ํ•จ์ •
123
+
124
+ - children์˜ ์ƒ‰์€ ๋ฒ„ํŠผ์ด ์น ํ•˜์ง€ ์•Š๋Š”๋‹ค. HJM Icon์€ ์ž๊ธฐ `tone` ์ƒ‰์„ ์“ฐ๋ฏ€๋กœ, `primary`ยท`danger` ๋ฒ„ํŠผ ์•ˆ์—์„œ๋Š”
125
+ `tone="inverse"`๋ฅผ ์ค€๋‹ค. ๋‹ค๋ฅธ ์•„์ด์ฝ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ๋„ฃ์œผ๋ฉด(Native) ์ƒ‰์„ ์ œํ’ˆ์ด ๋„˜๊ฒจ์•ผ ํ•œ๋‹ค.
126
+ - Native `IconButton` `style`์€ deprecated์ง€๋งŒ ๋‹ค์Œ major ์ „๊นŒ์ง€ recipe ๋’ค์— ํ•ฉ์ณ์ ธ ์ง€๋ฆ„ยท๋ฐฐ๊ฒฝ๊นŒ์ง€ ๋ฐ”๋€๋‹ค. ์ƒˆ ์ฝ”๋“œ์—์„œ ์“ฐ์ง€ ์•Š๋Š”๋‹ค.