@hjmds/design-contracts 1.12.1 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +26 -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 +31 -0
  16. package/dist/component-recipes.d.ts.map +1 -1
  17. package/dist/component-recipes.js +20 -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 +378 -0
  67. package/docs/sheet.md +21 -0
  68. package/docs/splitter.md +8 -2
  69. package/docs/theming.md +36 -29
  70. package/docs/toggle-group.md +13 -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 +107 -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 +221 -0
  169. package/docs/usage/components/section.md +111 -0
  170. package/docs/usage/components/segmented-control.md +146 -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 +157 -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 +279 -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,105 @@
1
+ # Icon
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Icon](../../icon.md), recipe `iconRecipe`(`src/component-recipes.ts`)
9
+ - 스토리북: `배포/컴포넌트/글자와 아이콘/아이콘`, `배포/컴포넌트/글자와 아이콘/루시드 아이콘`
10
+
11
+ ## 언제 쓰나
12
+
13
+ HJM semantic 이름(`search`, `back`, `chevronEnd`, `notifications` 등 43개)으로 고르는 그림 기호에 쓴다.
14
+ 크기·색·선 굵기·RTL 반전은 HJM이 정하고, 그림 자체는 내장 경로(Web)나 제품이 넘긴 glyph가 그린다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 아이콘만 있는 누를 수 있는 행동 | [IconButton](icon-button.md) (이름은 버튼이 소유) |
21
+ | 사진·일러스트 | [Image](image.md), [Asset](asset.md) |
22
+ | 사람·계정 얼굴 | [Avatar](avatar.md) |
23
+ | 소셜 로그인 제공자 로고 | [AuthProviderButton](auth-provider-button.md) (로고는 제품 자산) |
24
+ | 미확인 개수 표시 | [CounterBadge](counter-badge.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `Icon` | 기본 | `@hjmds/react`, `/display` | `@hjmds/react-native`, `/primitives` |
31
+ | `createLucideGlyph` | 보조 — Lucide glyph 연결 helper(컴포넌트 아님) | `/icon-lucide` | `/icon-lucide` |
32
+
33
+ `/icon-lucide`는 optional peer가 필요하다. Web `lucide-react` 1.49.0,
34
+ Native `lucide-react-native` 1.49.0. 루트 entry에서는 나오지 않는다.
35
+
36
+ ## 최소 사용 예
37
+
38
+ ```tsx
39
+ // Web — 내장 glyph
40
+ import { Icon } from "@hjmds/react/display";
41
+
42
+ <Icon name="search" />
43
+ <Icon name="warning" tone="warning" decorative={false} accessibilityLabel={t("sync.failed")} />
44
+ ```
45
+
46
+ ```tsx
47
+ // Native — glyph는 제품이 공급한다(renderGlyph 필수)
48
+ import { Icon } from "@hjmds/react-native/primitives";
49
+ import { createLucideGlyph } from "@hjmds/react-native/icon-lucide";
50
+ import { Search, ArrowLeft } from "lucide-react-native";
51
+
52
+ const renderGlyph = createLucideGlyph({ search: Search, back: ArrowLeft });
53
+
54
+ <Icon descriptor={{ name: "search" }} renderGlyph={renderGlyph} />
55
+ ```
56
+
57
+ ## 축과 기본값
58
+
59
+ | prop | 값 | 기본값 | 설명 |
60
+ | --- | --- | --- | --- |
61
+ | `size` | `xs` 14 · `sm` 20 · `md` 24 · `lg` 28 · `xl` 32 · `xxl` 44 · `xxxl` 48 | `md` | — |
62
+ | `tone` | `primary` · `secondary` · `decorative` · `brand` · `info` · `success` · `warning` · `danger` · `inverse` | `secondary` | — |
63
+ | `weight` | `regular` · `strong` | `regular` | — |
64
+ | `directionality` | — | — | 생략하면 `back`·`forward`·`chevronStart`·`chevronEnd`만 RTL에서 반전 |
65
+ | `decorative` | `true` · `false` | `true` | 기본은 장식(`decorative` 생략 = true)이다. 정보 아이콘은 `decorative: false`와 현지화된 `accessibilityLabel`을 함께 준다 |
66
+ | `renderGlyph` | `(props: { name; size: number; color: string; strokeWidth: number }) => ReactNode` | Web 내장 SVG · Native 필수 | HJM이 정한 크기·색·선 굵기를 받아 그린다. Native `name`은 `Icon<Name>`의 제품 이름 타입 |
67
+ | `layoutStyle` | 배치 key | — | 바깥 배치만. 크기·색은 `size`·`tone`으로 |
68
+
69
+ - 묘사 객체 모양(Native `descriptor`, Web 평평한 prop 같은 이름): 장식 `{ name, size?, tone?, weight?, directionality?, decorative?: true }`,
70
+ 정보 `{ name, …, tone?: decorative 제외, decorative: false, accessibilityLabel: string }`. 두 모양은 타입으로 갈린다.
71
+
72
+ ## 배치
73
+
74
+ | 항목 | 값 | 근거 |
75
+ | --- | --- | --- |
76
+ | 크기 | `size`로만 정한다. 정사각형 프레임 한 변이 `glyph` 값이다(`md` 24 기본, `sm` 20, `xs` 14). Icon은 누를 수 없고 터치 영역이 없다. 누를 수 있게 하려면 최소 44(`control.minTouchTarget`)를 갖는 [IconButton](icon-button.md)으로 감싼다 | `design-contracts/src/foundations.ts`(`glyph`, `control`) |
77
+ | 간격 | 글자와의 간격은 감싸는 컴포넌트가 정한다(Link `spacing.xxs` 4, ListRow `spacing.sm` 12) | `design-contracts/src/component-recipes.ts` |
78
+ | 순서·정렬 | 텍스트 옆 아이콘은 글자와 세로 가운데로 맞춘다(Web `.hjm-icon`은 `vertical-align: middle`, `flex: 0 0 auto`로 줄어들지 않는다) | `react/src/styles.css`(`.hjm-icon`) |
79
+ | 고정·스크롤 | — | — |
80
+ | 좁은 폭·큰 글자 | 큰 글자 설정에서도 크기가 늘지 않는다. 글자와 함께 커져야 하면 더 큰 `size`를 제품이 고른다 | `react-native/src/primitives.tsx`(Icon) |
81
+
82
+ ## 꼭 지킬 것
83
+
84
+ - 장식 아이콘에 `accessibilityLabel`을 주거나, 정보 아이콘에 라벨을 빼면 `TypeError`가 난다.
85
+ 정보 아이콘에는 `decorative` tone도 쓸 수 없다.
86
+ - 이름은 모양이 아니라 목적으로 읽힌다. icon-only 행동의 이름은 바깥 IconButton·Link에 둔다.
87
+ - `inverse`는 brand·danger처럼 채워진 면 위에서만 쓴다.
88
+ - 크기·색·stroke를 `style`·`className`·glyph 쪽에서 바꾸지 않는다. 그림 라이브러리는 제품이 고르고
89
+ (`createLucideGlyph`에 named import만 넘김), 의미 이름과 외형은 HJM이 소유한다.
90
+
91
+ ## 플랫폼 차이
92
+
93
+ | 항목 | Web | Native |
94
+ | --- | --- | --- |
95
+ | API 모양 | 평평한 prop(`name`, `size`, `tone` …) | `descriptor` 객체 하나 |
96
+ | glyph | 내장 SVG 경로, `renderGlyph`는 선택 | `renderGlyph` 필수(내장 그림 없음) |
97
+ | 사용자 정의 이름 | `SemanticIconName`만 | `Icon<Name>` 제네릭으로 제품 이름 허용 |
98
+ | 출력 | `<svg>`(장식이면 `aria-hidden`) | `View` 프레임(장식이면 `accessible={false}`) |
99
+ | 배치 | `layoutStyle` | `layoutStyle`. `style`은 deprecated(개발 모드 1회 경고, 다음 major 제거) — `layoutStyle` 또는 descriptor `size`·`tone` |
100
+
101
+ ## 함정
102
+
103
+ - 아이콘은 자기 `tone` 색을 직접 칠한다. Web CSS가 `data-tone`으로 색을 지정하므로 부모 색을 상속하지 않는다.
104
+ primary·danger IconButton 안에서 기본 tone(`secondary`)을 쓰면 채워진 면 위에 회색이 된다. 이때는 `tone="inverse"`.
105
+ - `createLucideGlyph` 맵에 없는 이름을 그리면 실행 중 `TypeError`가 난다.
@@ -0,0 +1,122 @@
1
+ # Image
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Image](../../image.md), [GridReveal](../../grid-reveal.md), [ImageViewer](../../optional-adapters.md#behavior-boundaries), recipe `imageRecipe`(`src/image.ts`)
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/이미지` · `배포/컴포넌트/시각 효과/격자 등장 효과`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 원본 크기를 아는 사진·차트 이미지를 로드 전에 자리를 잡아 두고, 실패해도 의미를 잃지 않게 보여 줄 때 쓴다.
14
+ 로드 완료 순간 격자 마스크로 드러내려면 `GridReveal`로 감싸고, Native에서 사진을 크게 넘겨 보며
15
+ 확대하려면 `ImageViewer`를 연다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 의미 이름으로 고르는 그림 기호 | [Icon](icon.md) |
22
+ | 사람·계정 얼굴, 이니셜 대체 | [Avatar](avatar.md) |
23
+ | 제품 일러스트·자산 묶음 | [Asset](asset.md) |
24
+ | 크기를 모르는 임의 콘텐츠의 비율 고정 | [AspectRatio](aspect-ratio.md) |
25
+ | 여러 장을 넘겨 보는 띠 | [Carousel](carousel.md) |
26
+ | 높이가 다른 사진 카드 격자 | [Masonry](masonry.md) |
27
+
28
+ ## 공개 이름과 import
29
+
30
+ | 이름 | 역할 | Web | Native |
31
+ | --- | --- | --- | --- |
32
+ | `Image` | 기본 | `@hjmds/react`, `/display` | `@hjmds/react-native`, `/data-display` |
33
+ | `GridReveal` | 확장 — 로드 완료 시 4×4 마스크 연출(optional) | `/grid-reveal` | `/grid-reveal` |
34
+ | `ImageViewer` | 확장 — 전체 화면 넘겨 보기·확대(optional) | — | `/image-viewer` |
35
+
36
+ `GridReveal`은 추가 peer가 없다. `ImageViewer`는 granular subpath로만 import 되며 optional peer
37
+ `react-native-zoom-toolkit` 5.1.1, `react-native-gesture-handler` 2.32.0과 Reanimated·Worklets 설치가 필요하다.
38
+ Expo Go로는 검증할 수 없고 개발 클라이언트가 필요하다. tsc·테스트 통과는 peer 설치의 근거가 아니다.
39
+
40
+ ## 최소 사용 예
41
+
42
+ ```tsx
43
+ // Web — next/image 같은 adapter는 renderImage로 연결
44
+ import { Image } from "@hjmds/react/display";
45
+ import { GridReveal } from "@hjmds/react/grid-reveal";
46
+
47
+ <GridReveal ready={loaded}>
48
+ <Image src={photo.url} width={photo.width} height={photo.height}
49
+ decorative={false} accessibilityLabel={t("gallery.photoAlt", { title: photo.title })}
50
+ onLoadStatusChange={(status) => setLoaded(status === "loaded")} />
51
+ </GridReveal>
52
+ ```
53
+
54
+ ```tsx
55
+ // Native
56
+ import { Image } from "@hjmds/react-native/data-display";
57
+ import { ImageViewer } from "@hjmds/react-native/image-viewer";
58
+
59
+ <Image src={chart.url} width={800} height={450}
60
+ decorative={false} accessibilityLabel={t("stats.chartAlt")} layoutStyle={{ width: "100%" }} />
61
+
62
+ <ImageViewer open={viewerOpen} onClose={() => setViewerOpen(false)} items={photos}
63
+ safeAreaInsets={insets} closeLabel={t("common.close")} previousLabel={t("viewer.prev")}
64
+ nextLabel={t("viewer.next")} loadingLabel={t("common.loading")}
65
+ errorLabel={t("viewer.loadFailed")} retryLabel={t("common.retry")} />
66
+ ```
67
+
68
+ ## 축과 기본값
69
+
70
+ | prop | 값 | 기본값 | 설명 |
71
+ | --- | --- | --- | --- |
72
+ | `src`, `width`, `height` | 필수, 양의 유한수 | — | 비율로 자리를 예약하고 루트 폭 기본값은 `width`다 |
73
+ | `fit` | `cover` · `contain` · `fill` | `cover` | `fill`은 Native `stretch`로 번역 |
74
+ | `decorative` | `true` · `false` | `true` | 기본은 장식이다. 이미지만으로 정보를 전하면 `decorative: false`와 현지화된 `accessibilityLabel`을 함께 준다 |
75
+ | `accessibilityLabel` | 현지화 문구 | — | `decorative={false}`일 때만, 필수. Web은 `<img alt>`(실패 대체는 `aria-label`)로 간다. Web도 `alt` prop은 받지 않는다 |
76
+ | `fallback` | `ReactNode` | 중립 배경 + 오류 기호 | 시각만 바꾼다 |
77
+ | `onLoadStatusChange` | `(status: "loaded" \| "error") => void` | — | 로드 완료·실패를 한 번씩 알린다. GridReveal `ready`를 이 값으로 정한다 |
78
+ | `renderImage` | Web `(props: ImageAdapterProps) => ReactElement` · Native `(props: CanonicalImageRenderProps) => ReactNode` | `<img>` · RN `Image` | adapter는 받은 `onLoad`·`onError`를 실제 요소에 넘긴다 |
79
+ | Native `sourceAdapter` | `(descriptor: ResolvedImageDescriptor) => ImageSourcePropType` | — | 헤더·캐시가 필요한 원격 이미지 |
80
+ | GridReveal `ready` | `true` · `false` | — (필수) | — |
81
+ | GridReveal `active` | `true` · `false` | `true` | — |
82
+ | ImageViewer `items` | `id`·`uri`·`label` | — | 라벨 6종, `safeAreaInsets`가 필수 |
83
+ | ImageViewer `initialIndex` | 0 이상 정수 | `0` | — |
84
+ | ImageViewer `onClose` · `onIndexChange` | `() => void` · `(index: number) => void` | `onClose` 필수 | — |
85
+ | ImageViewer `safeAreaInsets` | `{ top: number; bottom: number }` | 필수 | — |
86
+
87
+ - 실패하면 중립 배경 위에 오류 기호를 그리고, 정보 이미지의 이름은 그대로 유지한다. `fallback`은 시각만 바꾼다.
88
+
89
+ ## 배치
90
+
91
+ | 항목 | 값 | 근거 |
92
+ | --- | --- | --- |
93
+ | 크기 | 자리는 `width`·`height` 비율로 미리 잡는다(Web `aspect-ratio`, Native `aspectRatio`). 로드 전후로 높이가 바뀌지 않는다. 모서리는 `radius.md` 12로 잘린다(`imageRecipe.radius`) | `design-contracts/src/image.ts`(`imageRecipe`), `design-contracts/src/foundations.ts`(`radius`), `react/src/supplemental-display.tsx`(Image) |
94
+ | 간격 | 카드 안에 넣을 때 카드 padding 안쪽에 둔다 | — |
95
+ | 순서·정렬 | 여러 장은 직접 줄 세우지 말고 [Grid](grid.md)·[Masonry](masonry.md)·[Carousel](carousel.md)로 배치한다 | — |
96
+ | 고정·스크롤 | ImageViewer는 전체 화면을 덮는다. 상·하단 버튼은 `safeAreaInsets` 안쪽에 그려지므로 호스트가 inset을 넘긴다 | `react-native/src/image-viewer.tsx` |
97
+ | 좁은 폭·큰 글자 | Web 루트는 `inline-size: width`, `max-inline-size: 100%`라 부모보다 넓어지지 않는다. Native 루트 폭은 `width` 숫자 그대로이므로 화면 폭 사진은 `layoutStyle={{ width: "100%" }}`로 준다. Grid·Masonry 칸에 채울 때 Web은 `layoutStyle={{ inlineSize: "100%" }}`(루트 `style`도 기본 `inline-size`를 덮는다), Native는 `layoutStyle={{ width: "100%" }}`로 칸 폭에 맞춘다. 원본이 칸보다 작으면 기본값으로는 좁게 남는다 | `react/src/styles.css`(`.hjm-image`), `react-native/src/data-display.tsx`(Image) |
98
+
99
+ ## 꼭 지킬 것
100
+
101
+ - 장식인데 라벨을 주거나 정보인데 라벨을 빼면 `TypeError`가 난다.
102
+ - GridReveal의 `ready`는 `onLoadStatusChange`가 `loaded`일 때만 true, 오류·소스 변경·재시도 전에는 false로 돌린다.
103
+ Native에서 화면이 가려진 채 mounted면 `active={false}`.
104
+ - ImageViewer는 `open`으로 제어하고 닫히면 상태를 버린다. 라벨·`id`가 비거나 중복이면 `TypeError`.
105
+ - ImageViewer는 불러오는 중(`loadingLabel`)과 실패(`errorLabel`)를 모두 알린다. 실패는 Android assertive live region, iOS는 `announceForAccessibility`다(미게시(1.12.1 이후). 1.12.1은 실패를 알리지 않았다).
106
+ - 원격 이미지 권한·URL 수명·캐시는 제품 소유다. HJM은 메타데이터를 가져오지 않는다.
107
+
108
+ ## 플랫폼 차이
109
+
110
+ | 항목 | Web | Native |
111
+ | --- | --- | --- |
112
+ | 배치 | `layoutStyle`, 루트 `style`/`className`(기본 `inline-size: width`, `max-inline-size: 100%`) | `layoutStyle`(루트 프레임) |
113
+ | 정보 이미지 이름 | `accessibilityLabel` → `<img alt>`(`alt` prop은 받지 않는다) | `accessibilityLabel` |
114
+ | 이미지 host 스타일 | `imageProps.style`/`className` | `style`(ImageStyle) |
115
+ | 다른 이미지 엔진 | `renderImage`(받은 props를 `<img>`까지 전달) | `renderImage`(예: expo-image), `sourceAdapter`(헤더·캐시) |
116
+ | 확대 보기 | — | `ImageViewer` |
117
+
118
+ ## 함정
119
+
120
+ - `renderImage` adapter가 받은 `onLoad`·`onError`를 실제 이미지 요소에 넘기지 않으면 HJM이 실패를 보지 못해
121
+ 대체 화면과 GridReveal의 `ready`가 동작하지 않는다.
122
+ - Native 루트 폭은 `width` 숫자 그대로다. 화면 폭에 맞추려면 `layoutStyle`로 폭을 준다.
@@ -0,0 +1,93 @@
1
+ # KeyboardAvoiding
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Native platform](../../native-platform.md#키보드-회피), 판정 `resolveKeyboardInset`(`src/native-platform.ts`)
9
+ - 스토리북: 없음
10
+
11
+ ## 언제 쓰나
12
+
13
+ 추가 native peer 없이 하단 행동(BottomCTA, 채팅 입력창)이 소프트웨어 키보드에 가려지지 않게 할 때 쓴다.
14
+ 키보드가 뜨면 측정한 높이만큼 아래 여백을 늘리고, 내려가면 safe area 여백만 남긴다.
15
+ `react-native-keyboard-controller`를 설치하지 않은 앱의 기본 선택이다(Native 전용, 별도 보조 기능).
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | keyboard-controller를 설치한 앱에서 하단 행동을 키보드에 붙여 움직임 | [KeyboardDock](keyboard-dock.md) |
22
+ | 입력이 여러 개인 스크롤 폼에서 포커스된 필드를 보이게 | [KeyboardFormScrollView](keyboard-form-scroll-view.md) |
23
+ | keyboard-controller 어댑터를 쓰기 위한 앱 루트 설정 | [KeyboardMotionProvider](keyboard-motion-provider.md) |
24
+ | Sheet 안의 입력 | [Sheet](sheet.md)의 `keyboardAvoidance` (시트가 여백을 소유) |
25
+ | Web | 해당 없음. Web에는 이 문제와 계약이 없다 |
26
+
27
+ ## 공개 이름과 import
28
+
29
+ | 이름 | 역할 | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `KeyboardAvoiding` | 기본 | — | `@hjmds/react-native`, `/keyboard` |
32
+ | `resolveKeyboardAvoidanceBehavior` | 보조 — 플랫폼별 `KeyboardAvoidingView` behavior 판정 함수 | — | 같은 entry |
33
+
34
+ 추가 peer가 필요 없다.
35
+
36
+ ## 최소 사용 예
37
+
38
+ Web: 없음. Web 구현은 없다.
39
+
40
+ ```tsx
41
+ // Native
42
+ import { KeyboardAvoiding } from "@hjmds/react-native/keyboard";
43
+ import { BottomCTA } from "@hjmds/react-native/bottom-cta";
44
+
45
+ <KeyboardAvoiding safeAreaBottom={insets.bottom}>
46
+ <BottomCTA primaryAction={{ label: t("signup.next"), onPress: next }} />
47
+ </KeyboardAvoiding>
48
+ ```
49
+
50
+ ## 축과 기본값
51
+
52
+ | prop | 값 | 기본값 | 설명 |
53
+ | --- | --- | --- | --- |
54
+ | `offset` | 숫자 | `spacing.sm`(12) | 키보드 위에 남길 여백 |
55
+ | `safeAreaBottom` | 숫자 | `0` | 하단 safe area 값. 키보드가 닫혀 있으면 이 값만큼, 열려 있으면 키보드 높이 + `offset` 만큼 아래 여백을 준다. safe area는 한 번만 센다 |
56
+ | `style` | `StyleProp<ViewStyle>` | — | 감싸는 `View`의 스타일. renderer가 주는 `paddingBottom` 뒤에 합쳐진다. 플랫폼 host라 1.13 deprecated 대상이 아니고 `layoutStyle`은 없다 |
57
+ | `resolveKeyboardAvoidanceBehavior` | `(platform: "ios" \| "android") => KeyboardAvoidanceBehavior` | — | RN `KeyboardAvoidingView`를 직접 쓸 때의 `behavior` 판정. 이 컴포넌트는 쓰지 않는다 |
58
+
59
+ - 콜백·상태 prop은 없다. 키보드 높이는 컴포넌트 안에서 키보드 이벤트로 잰다.
60
+
61
+ ## 배치
62
+
63
+ | 항목 | 값 | 근거 |
64
+ | --- | --- | --- |
65
+ | 크기 | 아래 여백: 키보드 닫힘 = `safeAreaBottom`, 열림 = 키보드 높이 + `offset`(기본 `spacing.sm` 12) | `design-contracts/src/native-platform.ts`(`resolveKeyboardInset`, `keyboardAvoidanceDefaults`) |
66
+ | 간격 | safe area는 키보드 높이 안에 포함돼 한 번만 센다 | `design-contracts/src/native-platform.ts`(`resolveKeyboardInset`) |
67
+ | 순서·정렬 | 화면 맨 아래, 스크롤 영역 **바깥**에서 하단 행동(BottomCTA, 입력창)만 감싼다. 본문 스크롤은 위에 따로 둔다 | `react-native/src/keyboard.tsx` |
68
+ | 고정·스크롤 | 높이 변화는 키보드 이벤트에 맞춰 `LayoutAnimation`(easeInEaseOut)으로 움직인다 | `react-native/src/keyboard.tsx` |
69
+ | 좁은 폭·큰 글자 | — | — |
70
+
71
+ ```text
72
+ 키보드 닫힘 키보드 열림
73
+ ┌──────────────────────┐ ┌──────────────────────┐
74
+ │ 본문(스크롤) │ │ 본문(스크롤, 줄어듦) │
75
+ │ │ ├──────────────────────┤
76
+ ├──────────────────────┤ │ [ 다음(primary) ]│ ← KeyboardAvoiding 안
77
+ │ [ 다음(primary) ]│ ← 고정 ├────── offset 12 ──────┤
78
+ ├── safeAreaBottom ────┤ │ ░░░░░ 키보드 ░░░░░░░ │
79
+ └──────────────────────┘ └──────────────────────┘
80
+ ```
81
+
82
+ ## 꼭 지킬 것
83
+
84
+ - 같은 내용을 다른 키보드 회피 래퍼(`KeyboardAvoidingView`, KeyboardDock, 호스트 어댑터)와 겹쳐 감싸지 않는다.
85
+ - safe area 여백을 안쪽 컴포넌트(BottomCTA `safeAreaBottom` 등)와 이 래퍼에 동시에 주지 않는다. 한 곳에서만 센다.
86
+ - `style`로 `paddingBottom`을 덮으면 키보드 여백이 사라진다. 배치 외 값을 넣지 않는다.
87
+
88
+ ## 함정
89
+
90
+ - `KeyboardAvoidingView`가 아니다. 플랫폼과 무관하게 키보드 이벤트로 잰 높이를 `paddingBottom`으로 준다
91
+ (iOS는 `keyboardWillChangeFrame`, Android는 `keyboardDidShow`). `resolveKeyboardAvoidanceBehavior`는
92
+ 제품이 RN `KeyboardAvoidingView`를 직접 쓸 때를 위한 판정이며 이 컴포넌트는 그 값을 쓰지 않는다.
93
+ - 이 래퍼는 아래 여백만 늘린다. 스크롤 안의 포커스된 필드를 보이는 곳으로 옮겨 주지 않는다.
@@ -0,0 +1,110 @@
1
+ # KeyboardDock
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Optional adapters](../../optional-adapters.md#behavior-boundaries). 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
+ - 스토리북: `배포/구성/직접 조작과 모션/이미지·시트·키보드 조작`
10
+
11
+ ## 언제 쓰나
12
+
13
+ `react-native-keyboard-controller`를 설치한 앱에서 화면 하단에 고정된 행동(BottomCTA, 채팅 입력창)이
14
+ 키보드와 함께 위아래로 움직이게 할 때 쓴다(Native 전용, 별도 보조 기능. API 성숙도는 실험적 어댑터). 내부는 `KeyboardStickyView`이며,
15
+ 여백을 바꾸는 대신 키보드 움직임을 따라 translate 한다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | keyboard-controller 없이 하단 행동을 키보드 위로 | [KeyboardAvoiding](keyboard-avoiding.md) |
22
+ | 입력이 여러 개인 스크롤 폼 본문 | [KeyboardFormScrollView](keyboard-form-scroll-view.md) (하단 버튼은 이 Dock과 함께) |
23
+ | 앱 루트 설정 | [KeyboardMotionProvider](keyboard-motion-provider.md) (먼저 필요) |
24
+ | Sheet 안의 입력 | [Sheet](sheet.md)의 `keyboardAvoidance` |
25
+ | Web | 해당 없음 |
26
+
27
+ ## 공개 이름과 import
28
+
29
+ | 이름 | 역할 | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `KeyboardDock` | 기본 — 하단 고정 행동을 키보드에 붙임 | — | `/keyboard-controller` |
32
+
33
+ granular subpath로만 import 된다. 설치 조건(peer `react-native-keyboard-controller` 1.22.5, Reanimated·Worklets,
34
+ 개발 클라이언트)과 루트 Provider는 [KeyboardMotionProvider](keyboard-motion-provider.md)를 따른다.
35
+
36
+ ## 최소 사용 예
37
+
38
+ Web: 없음. Web 구현은 없다.
39
+
40
+ ```tsx
41
+ // Native — KeyboardMotionProvider 안, KeyboardFormScrollView의 다음 형제
42
+ import { View } from "react-native";
43
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
44
+ import { useKeyboardState } from "react-native-keyboard-controller";
45
+ import { KeyboardDock } from "@hjmds/react-native/keyboard-controller";
46
+ import { Button } from "@hjmds/react-native/actions";
47
+ import { Container } from "@hjmds/react-native/primitives";
48
+
49
+ const insets = useSafeAreaInsets();
50
+ const keyboardOpen = useKeyboardState((state) => state.isVisible);
51
+
52
+ <KeyboardDock clearance={8}>
53
+ <View style={{ paddingBottom: keyboardOpen ? 0 : insets.bottom }}>
54
+ <Container gutter="compact">
55
+ <Button onPress={submit} fullWidth>{t("form.done")}</Button>
56
+ </Container>
57
+ </View>
58
+ </KeyboardDock>
59
+ ```
60
+
61
+ ## 축과 기본값
62
+
63
+ | prop | 값 | 기본값 | 설명 |
64
+ | --- | --- | --- | --- |
65
+ | `enabled` | `true` · `false` | `true` | `false`면 따라 움직이지 않는다 |
66
+ | `clearance` | 0 이상 숫자(layout point) | `0` | 키보드 위 추가 간격. 음수나 유한하지 않은 값은 `TypeError` |
67
+ | `children` | ReactNode(필수) | — | 하단 행동. 안쪽 여백·안전 영역은 호스트가 준다 |
68
+ | `style` | `StyleProp<ViewStyle>` | — | 감싸는 sticky view의 host 스타일. 플랫폼 host라 1.13 deprecated 대상이 아니다([이관 문서](../../migration-native-legacy-removal.md#113-deprecated-시각-style-제거는-다음-major)). 배치 값만 넣는다 |
69
+
70
+ - 이벤트·콜백 prop은 없다. 키보드 열림 여부가 필요하면 peer의 `useKeyboardState((state) => state.isVisible)`로 읽는다.
71
+ - 내부에서 `KeyboardStickyView`에 `offset={{ closed: 0, opened: -clearance }}`를 넘긴다.
72
+
73
+ ## 배치
74
+
75
+ | 항목 | 값 | 근거 |
76
+ | --- | --- | --- |
77
+ | 크기 | — | — |
78
+ | 간격 | 안쪽 여백은 호스트가 준다. 좌우는 화면 본문과 같은 [Container](container.md) `gutter`(폭 600 미만 `compact` 16, 이상 `regular` 20 — [화면 여백과 너비](../tokens/layout.md)), 아래는 키보드 닫힘이면 `insets.bottom`, 열림이면 0이다. 키보드와의 추가 간격은 `clearance`(기본 0)로만 준다 | `react-native/src/keyboard-controller.tsx`, `tokens/layout.md` |
79
+ | 순서·정렬 | 화면 맨 아래, 스크롤 영역([KeyboardFormScrollView](keyboard-form-scroll-view.md)) **다음 형제**로 둔다 | `showcase/native/src/OptionalAdapters.stories.tsx` |
80
+ | 고정·스크롤 | 키보드가 열리면 그만큼 위로 translate 된다 | `react-native/src/keyboard-controller.tsx` |
81
+ | 좁은 폭·큰 글자 | — | — |
82
+
83
+ ```text
84
+ ┌──────────────────────────┐
85
+ │ 상단 고정 영역(insets.top) │
86
+ ├──────────────────────────┤
87
+ │ KeyboardFormScrollView │ ← 스크롤 영역, 안에 Container(gutter)
88
+ │ 필드 / 필드 / ... │
89
+ ├──────────────────────────┤
90
+ │ [ 완료 ] │ ← KeyboardDock 안 Container(gutter) — 주 행동
91
+ ├── insets.bottom(닫힘) / 0(열림) ──┤
92
+ └──────────────────────────┘
93
+ ```
94
+
95
+ ## 꼭 지킬 것
96
+
97
+ - 하단 safe area는 호스트가 소유한다. 키보드가 열렸을 때 safe area 여백을 빼는 것도 호스트 몫이다
98
+ (키보드 상태에 따라 `paddingBottom`을 `insets.bottom`과 0 사이에서 바꾼다).
99
+ - 좌우 여백은 숫자로 쓰지 않고 [Container](container.md) `gutter`로 준다.
100
+ - 같은 내용을 KeyboardAvoiding이나 다른 키보드 회피 래퍼로 또 감싸지 않는다.
101
+ - KeyboardMotionProvider 없이 쓰지 않는다.
102
+
103
+ ## 함정
104
+
105
+ - 현재 쇼케이스(`showcase/native/src/OptionalAdapters.stories.tsx`)는 Dock 안 좌우 여백을 `paddingHorizontal: spacing.md`로
106
+ 직접 주고, 키보드 상태를 `Keyboard.addListener`로 따로 추적하며, 버튼 문구가 i18n 키가 아니다. 규칙은 위 예처럼 Container `gutter`와 i18n 키다.
107
+
108
+ - sticky 좌표는 창 기준이다. Storybook 캔버스처럼 아래에 다른 막대가 있는 host에서는 위치가 어긋나므로
109
+ 쇼케이스도 앱 크기 host에서 확인한다.
110
+ - API 성숙도가 실험적 어댑터다(2026-10-06 Storybook 배포와 별개, [근거](../../optional-adapters.md#evidence-and-promotion)). 실제 키보드 애니메이션은 mock 테스트로 검증되지 않는다. 개발 클라이언트로 확인한다.
@@ -0,0 +1,95 @@
1
+ # KeyboardFormScrollView
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Optional adapters](../../optional-adapters.md#behavior-boundaries). 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
+ - 스토리북: `배포/구성/직접 조작과 모션/이미지·시트·키보드 조작`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 입력 필드가 여러 개인 세로 스크롤 폼(가입, 프로필 수정, 주소 입력)에서 포커스된 필드가 키보드에
14
+ 가려지지 않게 스크롤해 줄 때 쓴다(Native 전용, 별도 보조 기능. API 성숙도는 실험적 어댑터). 내부는 `react-native-keyboard-controller`의
15
+ `KeyboardAwareScrollView`이고, `keyboardShouldPersistTaps="handled"`로 고정돼 키보드가 열린 채 버튼을 눌러도 탭이 전달된다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 화면 하단에 고정된 버튼·입력창을 키보드에 붙임 | [KeyboardDock](keyboard-dock.md) (이 폼과 함께 쓸 수 있다) |
22
+ | peer 없이 하단 행동만 올림 | [KeyboardAvoiding](keyboard-avoiding.md) |
23
+ | 앱 루트 설정 | [KeyboardMotionProvider](keyboard-motion-provider.md) (먼저 필요) |
24
+ | 입력 묶음·검증 구조 | [Form](form.md) (스크롤 컨테이너가 아니다) |
25
+ | Sheet 안의 입력 | [Sheet](sheet.md)의 `keyboardAvoidance` |
26
+ | Web | 해당 없음 |
27
+
28
+ ## 공개 이름과 import
29
+
30
+ | 이름 | 역할 | Web | Native |
31
+ | --- | --- | --- | --- |
32
+ | `KeyboardFormScrollView` | 기본 — 키보드 인지 폼 스크롤 | — | `/keyboard-controller` |
33
+
34
+ granular subpath로만 import 된다. 설치 조건과 루트 Provider는 [KeyboardMotionProvider](keyboard-motion-provider.md)를 따른다.
35
+
36
+ ## 최소 사용 예
37
+
38
+ Web: 없음. Web 구현은 없다.
39
+
40
+ ```tsx
41
+ // Native — KeyboardMotionProvider 안
42
+ import { KeyboardFormScrollView } from "@hjmds/react-native/keyboard-controller";
43
+ import { Container, Stack } from "@hjmds/react-native/primitives";
44
+ import { TextField } from "@hjmds/react-native/inputs";
45
+ import { spacing } from "@hjmds/design-contracts/foundations";
46
+
47
+ <KeyboardFormScrollView contentContainerStyle={{ paddingVertical: spacing.md }} bottomOffset={spacing.md}>
48
+ <Container gutter="compact">
49
+ <Stack gap="md">
50
+ <TextField label={t("profile.name")} value={name} onValueChange={setName} />
51
+ <TextField label={t("profile.email")} value={email} onValueChange={setEmail} />
52
+ </Stack>
53
+ </Container>
54
+ </KeyboardFormScrollView>
55
+ ```
56
+
57
+ 스크롤 컨테이너는 세로 여백만 갖고, 좌우 여백은 안쪽 [Container](container.md) `gutter`(폭 600 미만 `compact` 16, 이상
58
+ `regular` 20), 필드 사이는 [Stack](stack.md) `gap`이 준다.
59
+
60
+ ## 축과 기본값
61
+
62
+ | prop | 값 | 기본값 | 설명 |
63
+ | --- | --- | --- | --- |
64
+ | `bottomOffset` | 숫자(layout point) | `0`(peer 기본값) | 포커스된 입력의 caret과 키보드 사이 거리 |
65
+ | `enabled` | `true` · `false` | `true`(peer 기본값) | `false`면 키보드에 맞춰 스크롤하지 않는다 |
66
+ | `contentContainerStyle` | `StyleProp<ViewStyle>` | — | 세로 여백만 준다. 좌우 여백·필드 간격은 Container·Stack |
67
+ | `style` | `StyleProp<ViewStyle>` | — | 스크롤 view 자체의 배치(`flex: 1` 등) |
68
+ | `keyboardShouldPersistTaps` | `"handled"` | `"handled"`(고정) | 바꿀 수 없다 |
69
+
70
+ - 받는 prop은 `children`, `style`, `contentContainerStyle`, `bottomOffset`, `enabled`, `testID`뿐이다. 이벤트·콜백 prop은 없다.
71
+
72
+ ## 배치
73
+
74
+ | 항목 | 값 | 근거 |
75
+ | --- | --- | --- |
76
+ | 크기 | 화면 본문 전체를 차지하는 스크롤 영역으로 둔다 | `showcase/native/src/OptionalAdapters.stories.tsx` |
77
+ | 간격 | 세로 여백은 `contentContainerStyle`의 `paddingVertical`(`spacing.md` 16), 좌우 여백은 안쪽 [Container](container.md) `gutter`(폭 600 미만 16, 이상 20), 필드 사이는 [Stack](stack.md) `gap="md"` 16(`layout.contentGap`). 필드와 키보드 사이 여백은 `bottomOffset`으로 준다 | `tokens/layout.md`, `react-native/src/keyboard-controller.tsx` |
78
+ | 순서·정렬 | 위에는 고정 헤더, 아래에는 [KeyboardDock](keyboard-dock.md)으로 붙인 하단 행동을 형제로 둔다 | `showcase/native/src/OptionalAdapters.stories.tsx` |
79
+ | 고정·스크롤 | 포커스된 필드가 키보드 위로 보일 때까지 스크롤된다 | `react-native/src/keyboard-controller.tsx` |
80
+ | 좁은 폭·큰 글자 | 큰 글자에서 필드가 길어져도 스크롤로 받는다. 높이를 고정하지 않는다 | — |
81
+
82
+ ## 꼭 지킬 것
83
+
84
+ - KeyboardMotionProvider 없이 쓰지 않는다.
85
+ - 다른 ScrollView 안에 넣지 않는다. 스크롤 컨테이너는 하나만 둔다.
86
+ - 같은 내용을 KeyboardAvoiding이나 `KeyboardAvoidingView`로 또 감싸지 않는다.
87
+ - `contentContainerStyle`에는 세로 여백만 HJM spacing 토큰으로 준다. 좌우 여백을 숫자로 넣지 않는다.
88
+
89
+ ## 함정
90
+
91
+ - 현재 쇼케이스(`showcase/native/src/OptionalAdapters.stories.tsx`)는 `contentContainerStyle={{ gap: spacing.md, padding: spacing.md }}`로
92
+ 좌우 여백까지 스크롤 컨테이너에 직접 준다. 규칙은 세로 여백만 스크롤에, 좌우는 Container `gutter`다.
93
+
94
+ - `refreshControl`, `onScroll` 같은 다른 ScrollView prop은 타입에서 빠져 있어 넘길 수 없다.
95
+ - API 성숙도가 실험적 어댑터다(2026-10-06 Storybook 배포와 별개, [근거](../../optional-adapters.md#evidence-and-promotion)). 포커스 이동·키보드 애니메이션은 개발 클라이언트로 기기에서 확인한다.
@@ -0,0 +1,86 @@
1
+ # KeyboardMotionProvider
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Optional adapters](../../optional-adapters.md#installation). 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
+ - 스토리북: `배포/구성/직접 조작과 모션/이미지·시트·키보드 조작`
10
+
11
+ ## 언제 쓰나
12
+
13
+ `@hjmds/react-native/keyboard-controller` 어댑터(KeyboardDock, KeyboardFormScrollView)를 쓰는 앱의
14
+ 루트에 **한 번** 설치한다(Native 전용, 별도 보조 기능. API 성숙도는 실험적 어댑터). 내부는 `react-native-keyboard-controller`의
15
+ `KeyboardProvider`이며, 어댑터를 준비하려고 OS 키보드를 미리 띄우지 않도록 `preload={false}`로 고정돼 있다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | peer 없이 하단 행동만 키보드 위로 올림 | [KeyboardAvoiding](keyboard-avoiding.md) (Provider 불필요) |
22
+ | 하단 행동을 키보드에 붙여 움직임 | [KeyboardDock](keyboard-dock.md) (이 Provider 안에서) |
23
+ | 스크롤 폼의 포커스 필드 보이기 | [KeyboardFormScrollView](keyboard-form-scroll-view.md) (이 Provider 안에서) |
24
+ | HJM 테마·환경 설정 | [DesignSystemProvider](design-system-provider.md) (역할이 다르다) |
25
+ | Web | 해당 없음 |
26
+
27
+ ## 공개 이름과 import
28
+
29
+ | 이름 | 역할 | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `KeyboardMotionProvider` | 기본 — 앱 루트 provider | — | `/keyboard-controller` |
32
+
33
+ granular subpath로만 import 된다(루트 entry에 없음). optional peer `react-native-keyboard-controller` 1.22.5와
34
+ `react-native-reanimated`(^4.5.1)·`react-native-worklets`(^0.10.1)를 앱에 설치해야 한다.
35
+ native 모듈이 연결된 개발 클라이언트가 필요하며 Expo Go로는 확인할 수 없다.
36
+
37
+ ## 최소 사용 예
38
+
39
+ Web: 없음. Web 구현은 없다.
40
+
41
+ ```tsx
42
+ // Native — 앱 루트
43
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
44
+ import { KeyboardMotionProvider } from "@hjmds/react-native/keyboard-controller";
45
+
46
+ <GestureHandlerRootView style={{ flex: 1 }}>
47
+ <KeyboardMotionProvider>
48
+ <AppNavigator />
49
+ </KeyboardMotionProvider>
50
+ </GestureHandlerRootView>
51
+ ```
52
+
53
+ `GestureHandlerRootView`는 쇼케이스 host 구성이며, 앱이 gesture 어댑터를 함께 쓸 때 필요하다.
54
+
55
+ ## 축과 기본값
56
+
57
+ | prop | 값 | 기본값 | 설명 |
58
+ | --- | --- | --- | --- |
59
+ | `children` | ReactNode | — | prop은 `children` 하나뿐이다 |
60
+ | `preload` | `false` | `false`(고정) | 바꿀 수 없다 |
61
+
62
+ - 이벤트·콜백 prop은 없다. 키보드 상태가 필요하면 peer의 `useKeyboardState`·`useKeyboardHandler`를 이 Provider 아래에서 쓴다.
63
+ - 렌더하는 상자가 없어 `layoutStyle`·`style`을 받지 않는다.
64
+
65
+ ## 배치
66
+
67
+ | 항목 | 값 | 근거 |
68
+ | --- | --- | --- |
69
+ | 크기 | 화면에 그려지는 것이 없다 | `react-native/src/keyboard-controller.tsx` |
70
+ | 간격 | — | — |
71
+ | 순서·정렬 | 앱 루트(`GestureHandlerRootView` 안, 내비게이터 바깥)에 한 번 둔다. KeyboardDock·KeyboardFormScrollView는 이 Provider 아래 어느 화면에나 놓을 수 있다 | `showcase/native/src/OptionalAdapters.stories.tsx` |
72
+ | 고정·스크롤 | — | — |
73
+ | 좁은 폭·큰 글자 | — | — |
74
+
75
+ ## 꼭 지킬 것
76
+
77
+ - 앱 루트에 한 번만 둔다. 입력 필드·CTA·화면마다 중첩하지 않는다.
78
+ - KeyboardDock·KeyboardFormScrollView는 이 Provider 바깥에 두지 않는다.
79
+ - 같은 앱에서 이 어댑터와 KeyboardAvoiding을 섞을 수는 있지만 같은 내용에 겹쳐 감싸지 않는다.
80
+
81
+ ## 함정
82
+
83
+ - tsc·mock 테스트 통과는 peer 설치와 native 연결의 근거가 아니다. 다른 optional subpath
84
+ (celebration·effect-surface·qr-code·thinking-orb·toast-liquid)에서 앱에 없는 peer 때문에
85
+ 기기 Metro에서만 크래시가 난 사례가 있다(2026-10). 개발 클라이언트로 실제 화면을 띄워 확인한다.
86
+ - 이 어댑터의 API 성숙도는 실험이다(2026-10-06 Storybook 배포와 별개). 기기·보조기술 검증은 아직 없다([근거](../../optional-adapters.md#evidence-and-promotion)).