@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,119 @@
1
+ # PhotoSourceSheet
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screens.tsx`·`src/screen-flows.tsx`; 기존 개별 지침을 새 규격으로 통합. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
+ - 스토리북: `배포/구성/선택과 필터/사진 촬영과 앨범 선택`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 사진 버튼 하나에서 **앨범에서 고르기 / 촬영하기**를 고르게 할 때 쓴다. [Sheet](sheet.md)와 Button 두 개의
14
+ 조합이며 출처 선택만 한다. 권한 요청·카메라 실행·파일 선택·업로드는 `onSelect`에서 제품이 한다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 여러 사진 선택·순서·업로드 상태 | [MediaSelectionScreen](media-selection-screen.md) |
21
+ | 파일 일반 선택 | [FilePicker](file-picker.md) |
22
+ | 업로드 진행 한 건 | [UploadItem](upload-item.md) |
23
+ | 사진을 받지 않는 입력(댓글 등) | 추가하지 않는다 |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `PhotoSourceSheet` | supplemental, 루트 barrel에 없음 | `/screen-flows` | `/screen-flows` |
30
+
31
+ `@hjmds/react/screen-flows`, `@hjmds/react-native/screen-flows`로만 import한다. 추가 optional peer는 없다.
32
+ HJM은 Expo·브라우저 카메라 SDK에 의존하지 않는다.
33
+
34
+ ## 최소 사용 예
35
+
36
+ ```tsx
37
+ // Web — 숨긴 file input을 onSelect 콜스택 안에서 클릭한다
38
+ import { useRef, useState } from "react";
39
+ import { PhotoSourceSheet } from "@hjmds/react/screen-flows";
40
+
41
+ export function PhotoPicker() {
42
+ const [open, setOpen] = useState(false);
43
+ const library = useRef<HTMLInputElement>(null);
44
+ const camera = useRef<HTMLInputElement>(null);
45
+ return <>
46
+ <input ref={library} type="file" accept="image/*" multiple hidden onChange={addFiles} />
47
+ <input ref={camera} type="file" accept="image/*" capture="environment" hidden onChange={addFiles} />
48
+ <PhotoSourceSheet
49
+ open={open}
50
+ onOpenChange={setOpen}
51
+ labels={{
52
+ title: t("photo.source.title"),
53
+ library: t("photo.source.library"),
54
+ camera: t("photo.source.camera"),
55
+ cancel: t("common.cancel"),
56
+ }}
57
+ onSelect={(source) => (source === "camera" ? camera : library).current?.click()}
58
+ />
59
+ </>;
60
+ }
61
+ ```
62
+
63
+ ```tsx
64
+ // Native
65
+ import { PhotoSourceSheet } from "@hjmds/react-native/screen-flows";
66
+
67
+ <PhotoSourceSheet
68
+ open={open}
69
+ onOpenChange={setOpen}
70
+ labels={{
71
+ title: t("photo.source.title"),
72
+ library: t("photo.source.library"),
73
+ camera: t("photo.source.camera"),
74
+ cancel: t("common.cancel"),
75
+ }}
76
+ cameraAvailable={hasCamera}
77
+ onSelect={(source) => (source === "camera" ? takePhoto() : pickFromLibrary())}
78
+ />
79
+ ```
80
+
81
+ ## 축과 기본값
82
+
83
+ | prop | 값 | 기본값 | 설명 |
84
+ | --- | --- | --- | --- |
85
+ | `open` | `boolean` | 필수 | 제어형 열림 |
86
+ | `onOpenChange` | `(open: boolean) => void` | 필수 | 닫기·선택 시 `false` |
87
+ | `onSelect` | `(source: "library" \| "camera") => void` | 필수 | 고르면 시트를 닫고(`onOpenChange(false)`) 호출한다. 호출 시점은 플랫폼 차이 참고 |
88
+ | `labels` | `{ title, library, camera, cancel }` | 필수 | `cancel`은 Sheet 닫기 버튼 이름 |
89
+ | `cameraAvailable` | `boolean` | `true` | `false`면 촬영 버튼을 숨긴다. 기기 카메라 유무는 제품이 판단한다 |
90
+ | `disabled` | `boolean` | `false` | 두 버튼을 막고 선택을 무시한다 |
91
+
92
+ ## 배치
93
+
94
+ | 항목 | 값 | 근거 |
95
+ | --- | --- | --- |
96
+ | 크기 | Sheet 기본 `placement="bottom"`·`size="auto"`(내용 높이, 화면 높이 0.9 상한), Web 최대 폭 640; 선택 버튼은 `tone="secondary"` `Button` 두 개 | `sheetRecipe`, `screen-flows.tsx` |
97
+ | 간격 | 시트 안 여백은 Sheet 소유(두 플랫폼 좌우 `spacing.lg` 20 · 위아래 `spacing.sm` 12 + 하단 safe area); 두 버튼 사이 `spacing.sm` 12; 바깥에 padding을 더하지 않는다 | Web `.hjm-sheet__body`, Native `sheetRecipe.content`, `Stack gap="sm"` |
98
+ | 순서·정렬 | Sheet 제목·닫기 → 앨범 → 촬영(`cameraAvailable`일 때만) | Web `screen-flows.tsx` 59–60행, Native 50–51행 |
99
+ | 고정·스크롤 | 화면 하단에 뜨는 오버레이(배경 막) — 선택 뒤 picker 호출 시점은 플랫폼 차이 참고 | `Sheet` |
100
+ | 좁은 폭·큰 글자 | 버튼 문구는 줄바꿈되고 Sheet 높이가 늘어난다(0.9 상한을 넘으면 본문 스크롤); 카메라가 없으면 버튼 하나만 남는다 | `sheetRecipe.content.maxHeightRatio` |
101
+
102
+ ## 꼭 지킬 것
103
+
104
+ - `labels`의 네 문구는 모두 i18n 키로 넣는다. 색은 HJM Provider 테마를 따른다.
105
+ - 취소·권한 거부·기기 없음일 때 기존 초안과 첨부를 지우지 않는다. 권한 거부에는 앨범 대안을 안내한다.
106
+ - EXIF 제거·크기·개수 제한·업로드는 제품 계약을 따른다.
107
+
108
+ ## 플랫폼 차이
109
+
110
+ | 항목 | Web | Native |
111
+ | --- | --- | --- |
112
+ | `onSelect` 호출 시점 | 클릭 콜스택 안에서 즉시(브라우저 사용자 활성화 보존) | 시트가 실제로 닫힌 뒤(`Sheet`의 dismiss 완료) |
113
+ | `onSelect`에서 할 일 | 숨긴 `input[type=file]` 클릭. 촬영은 `capture="environment"` input을 따로 둔다 | 카메라 권한 요청 후 촬영, 또는 앨범 선택기 실행 |
114
+
115
+ ## 함정
116
+
117
+ - Web에서 `onSelect` 안의 file input 클릭을 `setTimeout`·애니메이션 뒤로 미루면 브라우저가 거부할 수 있다. 같은 콜스택에서 클릭한다.
118
+ - Native에서 닫히는 Modal 위에 카메라·선택기를 띄우면 iOS가 표시하지 못한다. 그래서 dismiss 완료 뒤에 호출되며, 제품이 타이머로 다시 앞당기지 않는다.
119
+ - `capture`를 지원하지 않는 브라우저·기기는 OS 파일 선택 화면으로 대체될 수 있어 실제 촬영을 보장하지 않는다. 앨범 input에는 `capture`를 붙이지 않는다.
@@ -0,0 +1,108 @@
1
+ # Popover
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Popover](../../popover.md), [ConfirmPopover 조합](../../confirm-popover.md), `src/popover.ts`(`popoverRecipe`)
9
+ - 스토리북: `배포/컴포넌트/오버레이/팝오버`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 트리거에 붙어 뜨는 비모달 표면 안에 **포커스를 받는 임의 콘텐츠**를 둘 때 쓴다.
14
+ 작은 폼, 링크가 섞인 설명, 여러 control이 섞인 필터 묶음, 되돌릴 수 있는 행동의 짧은 확인이 여기에 속한다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | stable id를 가진 action·선택 항목 목록 | [Menu](menu.md) |
21
+ | 포커스가 들어가지 않는 한 문장 보충 설명 | [Tooltip](tooltip.md) |
22
+ | 되돌릴 수 없는 파괴적 행동의 확인 | [AlertDialog](alert-dialog.md) |
23
+ | 사용자가 반드시 응답해야 하는 모달 작업 | [Dialog](dialog.md) |
24
+ | Native 앱의 같은 자리 | [Sheet](sheet.md) (Popover는 Web 전용) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `Popover` | 기본 | `@hjmds/react`, `/popover` | — |
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { Button } from "@hjmds/react/actions";
37
+ import { Popover } from "@hjmds/react/popover";
38
+
39
+ <Popover
40
+ trigger={<Button tone="secondary">{t("filter.open")}</Button>}
41
+ title={t("filter.title")}
42
+ closeLabel={t("common.close")}
43
+ descriptor={{ placement: "bottom", align: "start" }}
44
+ >
45
+ {({ close }) => <FilterForm onApply={() => { apply(); close(); }} />}
46
+ </Popover>
47
+ ```
48
+
49
+ Native 예는 없다(renderer 없음).
50
+
51
+ ## 축과 기본값
52
+
53
+ | prop | 값 | 기본값 | 설명 |
54
+ | --- | --- | --- | --- |
55
+ | `trigger` | 요소 하나 | 필수 | 여는 버튼. Popover가 ref·aria 속성을 붙인다 |
56
+ | `title` · `closeLabel` | 문자열 | 필수 | 비면 던진다 |
57
+ | `description` | 문자열 | — | 제목 아래 설명 |
58
+ | `children` | 노드 또는 `(actions: { close(): void }) => ReactNode` | — | 콘텐츠 안에서 닫을 때 함수형을 쓴다 |
59
+ | `openOn` | `press` · `hover` | `press` | `hover`는 지연된 포인터 진입(열기 300ms·닫기 150ms)을 **더할** 뿐이고 click/Enter/Space 경로는 그대로다 |
60
+ | `descriptor` | `{ placement?, align?, accessibilityLabel? }` | — | |
61
+ | `descriptor.placement` | `top` · `bottom` · `start` · `end` | `bottom` | |
62
+ | `descriptor.align` | `start` · `center` · `end` | `start` | |
63
+ | `dismissPolicy`(부분 지정) | `{ dismissible?, outsideDismiss?, escapeDismiss?, focusOutDismiss? }`(`boolean`) | 모두 `true` | |
64
+ | `open` · `defaultOpen` | `boolean` | 비제어 `false` | 제어하면 `onOpenChange` 필수 |
65
+ | `onOpenChange` | `(open: boolean, details: { reason }) => void` | — | `reason`: `trigger` · `close-action` · `outside-pointer` · `outside-focus` · `escape` · `programmatic` |
66
+ | `initialFocusRef` | `RefObject<HTMLElement \| null>` | 첫 포커스 가능 요소 | 열릴 때 처음 포커스 |
67
+ | `portalContainer` | `HTMLElement` | `document.body` | 표면을 붙일 곳 |
68
+ | `className` | 문자열 | — | `layoutStyle`은 받지 않는다(아래 함정) |
69
+
70
+ ## 배치
71
+
72
+ | 항목 | 값 | 근거 |
73
+ | --- | --- | --- |
74
+ | 크기 | 폭 기본 360(`popoverRecipe.maxWidth`), 화면이 좁으면 최소 240(`minWidth`)까지 줄어든다. radius `radius.md` 12 | `popoverRecipe`, `src/popover.tsx` |
75
+ | 간격 | 트리거와 8(`sideOffset` = `spacing.xs`), 화면 가장자리 8(`collisionPadding` = `spacing.xs`). 안쪽 `spacing.sm` 12. 제목·닫기 사이 8, 설명 위 8 · 아래 16, 본문 위 8 | `popoverRecipe`, `.hjm-popover*` |
76
+ | 순서·정렬 | 기본 `placement="bottom"` `align="start"`. 안쪽은 제목과 닫기(`ghost`)가 한 줄 양 끝 → 설명 → 본문. 본문 행동은 끝 정렬, 보조 → 주([Button](button.md)) | `popoverDescriptorDefaults`, `.hjm-popover__header` |
77
+ | 고정·스크롤 | 트리거에 붙는 portal(`layer.dropdown` 400; 모달 내부는 소유 모달 + 1). 공간이 모자라면 반대쪽으로 뒤집고 그래도 안 되면 다른 축으로 옮긴다(`fallbackAxis`). 넘치는 내용은 표면 안 스크롤 | `src/popover.tsx`, `useAnchoredPopup` |
78
+ | 좁은 폭·큰 글자 | 제목 줄은 좁으면 감긴다. 폭은 240까지만 줄고 화면 안으로 제한된다 | `.hjm-popover__header`, `src/popover.tsx` |
79
+
80
+ 계약에는 화살표(`arrow` 4) 슬롯이 있지만 현재 Web renderer는 화살표를 그리지 않는다.
81
+
82
+ ```text
83
+ [필터 ▾] ← 트리거
84
+ ↓ 8
85
+ ┌──────────────────────────────┐
86
+ │ 필터 [닫기] │ ← 제목·닫기(ghost)
87
+ │ 설명 문구 │
88
+ │ ─ 본문(스크롤) ───────────── │
89
+ │ ☐ 옵션 A │
90
+ │ ☐ 옵션 B │
91
+ │ [초기화] [적용] │ ← 보조 → 주
92
+ └──────────────────────────────┘
93
+ 240 ~ 360, 화면 가장자리 8
94
+ ```
95
+
96
+ ## 꼭 지킬 것
97
+
98
+ - `title`과 `closeLabel`은 비어 있으면 안 된다(던진다). 둘 다 i18n 키로 넣는다.
99
+ - 행동 목록을 띄우려는 것이면 Popover가 아니라 Menu다. Popover 안에 menuitem 목록을 직접 만들지 않는다.
100
+ - 처음 포커스 대상을 바꿔야 하면 `initialFocusRef`를 쓴다.
101
+ - 콘텐츠 안에서 닫기는 children 함수 인자의 `close()`로 한다.
102
+ - `className`은 배치에만 쓴다. 표면의 색·radius·그림자를 덮지 않는다.
103
+
104
+ ## 함정
105
+
106
+ - Popover는 `layoutStyle`을 받지 않는다(Web `layoutStyle` 제외 15개 중 하나). 렌더하는 것이 제품 trigger와 떠 있는 portal뿐이라
107
+ 배치는 trigger 쪽(또는 감싼 요소)에서 한다.
108
+ - 부모 Popover가 닫히면 안에 중첩된 Popover도 닫히고 `onOpenChange(false, { reason: "programmatic" })`가 온다. 제어형이면 이 reason도 처리한다.
@@ -0,0 +1,89 @@
1
+ # ProfileScreen
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screens.tsx`·`src/screen-flows.tsx`; 기존 개별 지침을 새 규격으로 통합. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
+ - 스토리북: `배포/화면/계정/프로필`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 내 프로필(또는 계정) 화면 틀에 쓴다. 상단 요약, 그 아래 “프로필 수정” 보조 버튼, 이어지는 본문,
14
+ 맨 아래 계정 행동(로그아웃·탈퇴) 슬롯을 [ScreenLayout](screen-layout.md) 위에 이 순서로 쌓는다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 섹션별 설정 항목이 중심 | [SettingsScreen](settings-screen.md) |
21
+ | 프로필 수정 폼 자체 | [EditorScreen](editor-screen.md) |
22
+ | 다른 사용자 프로필의 신고·차단 | [ModerationScreen](moderation-screen.md) |
23
+ | 아바타 한 개 | [Avatar](avatar.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `ProfileScreen` | supplemental, 루트 barrel에 없음 | `/screen-flows` | `/screen-flows` |
30
+
31
+ `@hjmds/react/screen-flows`, `@hjmds/react-native/screen-flows`로만 import한다. 추가 optional peer는 없다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { ProfileScreen } from "@hjmds/react/screen-flows";
38
+
39
+ <ProfileScreen
40
+ title={t("profile.title")}
41
+ summary={<ProfileSummary user={user} />}
42
+ edit={{ label: t("profile.edit"), onAction: openEditor }}
43
+ accountActions={<AccountActions onSignOut={signOut} onDelete={confirmDelete} />}
44
+ state={user ? { kind: "ready" } : { kind: "loading", title: t("profile.loading") }}
45
+ >
46
+ <MyPostsSection />
47
+ </ProfileScreen>
48
+ ```
49
+
50
+ ```tsx
51
+ // Native — props는 Web과 같다
52
+ import { ProfileScreen } from "@hjmds/react-native/screen-flows";
53
+
54
+ <ProfileScreen
55
+ title={t("profile.title")}
56
+ summary={<ProfileSummary user={user} />}
57
+ edit={{ label: t("profile.edit"), onAction: openEditor }}
58
+ accountActions={<AccountActions onSignOut={signOut} onDelete={confirmDelete} />}
59
+ state={user ? { kind: "ready" } : { kind: "loading", title: t("profile.loading") }}
60
+ >
61
+ <MyPostsSection />
62
+ </ProfileScreen>
63
+ ```
64
+
65
+ ### 제품이 공급할 것
66
+
67
+ | 슬롯·prop | 내용 |
68
+ | --- | --- |
69
+ | `summary` | 아바타·닉네임·소개 등 요약(필수). Avatar·Heading·Text를 제품이 조합한다 |
70
+ | `edit` | 수정 진입 행동 `{ label, onAction, disabled?, pending? }`(필수). ghost 버튼으로 요약 아래에 놓인다 |
71
+ | `children` | 활동·통계 등 본문 |
72
+ | `accountActions` | 로그아웃·탈퇴 버튼 등. 본문 맨 아래 |
73
+ | ScreenLayout props | `title`, `description`, `header`, `leading`, `actions`, `notice`, `state`, `stateAction` 등(`footer` 제외) |
74
+
75
+ ## 배치
76
+
77
+ | 항목 | 값 | 근거 |
78
+ | --- | --- | --- |
79
+ | 크기 | ScreenLayout 폭(최대 720); 수정은 ghost `Button` 기본 크기 | `ProfileScreen` |
80
+ | 간격 | 화면 padding `spacing.md` 16; summary–수정 `spacing.md` 16; (summary·수정 묶음)–`children`–`accountActions` `spacing.xl` 24 | Web·Native `ProfileScreen` `Stack gap="xl"`·`gap="md"` |
81
+ | 순서·정렬 | 헤더(제목) → summary → 수정 → `children` → `accountActions` | 렌더 순서 |
82
+ | 고정·스크롤 | 헤더 고정, 본문 전체 화면 스크롤(footer 없음); 계정 행동은 본문 끝 | `ScreenLayout` |
83
+ | 좁은 폭·큰 글자 | 요약 줄바꿈·아바타 크기는 `summary`(제품) 소유; 제목 열 최소 폭 120 × 글자 배율 | `screenPatternRecipe.headerMinWidth` |
84
+
85
+ ## 꼭 지킬 것
86
+
87
+ - 계정 정보 조회, 인증, 로그아웃·탈퇴 mutation과 그 확인([AlertDialog](alert-dialog.md))은 제품 소유다.
88
+ - 프로필 이미지·닉네임 문구 등 표시 데이터는 제품이 넘긴다. HJM은 계정 모델을 모른다.
89
+ - 요약 자리에 브랜드 색을 하드코딩하지 않고 HJM 토큰·제품 테마로 연결한다.
@@ -0,0 +1,122 @@
1
+ # Progress
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Progress](../../progress.md), [Scroll progress](../../scroll-progress.md), `src/progress-recipe.ts`(`progressRecipe`)
9
+ - 스토리북: `배포/컴포넌트/상태와 알림/진행 표시`, `배포/컴포넌트/상태와 알림/읽기 진행 표시`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 작업이 얼마나 진행됐는지 보여 줄 때 쓴다. 업로드·내보내기 진행, 목표 대비 달성률,
14
+ 온보딩 단계 비율이 여기에 속한다. 진행량을 모르면 `value`를 생략해 불확정 진행으로 표시한다.
15
+ 읽기 진행률(스크롤 위치)은 같은 계약을 합성한 `ScrollProgress`를 쓴다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 진행량 없이 “기다리는 중”만 표시 | [Spinner](spinner.md) |
22
+ | 버튼 안의 처리 중 상태 | [Button](button.md)의 `loading` |
23
+ | 파일 하나의 업로드 상태·재시도 | [UploadItem](upload-item.md) |
24
+ | 단계 이름이 있는 흐름 | [Steps](steps.md) |
25
+ | 사용자가 값을 조절 | [Slider](slider.md) |
26
+ | 수치 강조 | [Statistic](statistic.md) |
27
+
28
+ ## 공개 이름과 import
29
+
30
+ | 이름 | 역할 | Web | Native |
31
+ | --- | --- | --- | --- |
32
+ | `Progress` | 기본 | `@hjmds/react`, `/feedback` | `@hjmds/react-native`, `/feedback` |
33
+ | `ScrollProgress` | 확장(스크롤 위치를 Progress로 표시, optional-extension, 추가 peer 없음) | `/scroll-progress` | `/scroll-progress` |
34
+ | `useScrollMetrics` | 보조(Web 스크롤 host 측정 hook) | `/scroll-progress` | — |
35
+
36
+ ## 최소 사용 예
37
+
38
+ ```tsx
39
+ // Web
40
+ import { Progress } from "@hjmds/react/feedback";
41
+
42
+ <Progress label={t("upload.progress")} value={64} />
43
+ ```
44
+
45
+ ```tsx
46
+ // Native
47
+ import { Progress } from "@hjmds/react-native/feedback";
48
+
49
+ <Progress label={t("upload.progress")} value={64} />
50
+ ```
51
+
52
+ ```tsx
53
+ // Web
54
+ // ScrollProgress — 실제 세로 스크롤 요소를 callback ref로 넘긴다
55
+ import { useState } from "react";
56
+ import { ScrollProgress, useScrollMetrics } from "@hjmds/react/scroll-progress";
57
+
58
+ const [host, setHost] = useState<HTMLElement | null>(null);
59
+ const metrics = useScrollMetrics(host);
60
+ <>
61
+ <ScrollProgress label={t("article.readProgress")} metrics={metrics} size="small" />
62
+ <div ref={setHost} style={{ overflowY: "auto" }}>{article}</div>
63
+ </>
64
+ ```
65
+
66
+ Native `ScrollProgress`는 제품 ScrollView의 `onScroll`(contentOffset.y)·`onContentSizeChange`(height)·`onLayout`(height)로
67
+ `metrics={{ offset, contentSize, viewportSize }}`를 만들어 넘긴다. 기존 콜백과 제스처는 그대로 둔다.
68
+
69
+ ## 축과 기본값
70
+
71
+ | prop | 값 | 기본값 | 설명 |
72
+ | --- | --- | --- | --- |
73
+ | `label` | Web `ReactNode`(필수) · Native `string` | — | Native는 `label` 또는 `accessibilityLabel` 중 하나가 필수 |
74
+ | `max` | 양수 | `100` | 두 renderer 동일. 0–1 비율이면 `max={1}`을 함께 준다 |
75
+ | `value` | `0`~`max` | — | 생략하면 불확정 진행 |
76
+ | `size` | `small` · `medium` · `large` | `medium` | 두께 4 · 8 · 12, circular 지름 24 · 40 · 64 |
77
+ | `tone` | `brand` · `success` · `warning` · `danger` | `brand` | — |
78
+ | `shape` | `linear` · `circular` | `linear` | `circular`일 때만 `children`(링 안 내용)을 그린다 |
79
+ | `valueText` | 지역화 문구 | 백분율 문구 | 단위가 다른 문구가 필요하면 지역화해 넘긴다 |
80
+ | `layoutStyle` | 배치 전용 style 객체 | — | 바깥 여백·폭만 |
81
+ | `metrics`(ScrollProgress) | `{ offset: number; contentSize: number; viewportSize: number }` | 필수 | 타입 `ScrollMetrics`(`@hjmds/design-contracts/scroll-progress`). `viewportSize` 0이면 0%, 내용이 화면보다 짧으면 100% |
82
+ | `useScrollMetrics`(Web) | `(host: HTMLElement \| null) => ScrollMetrics` | — | host의 스크롤·크기·자식 변화를 rAF로 모아 잰다 |
83
+
84
+ ## 배치
85
+
86
+ | 항목 | 값 | 근거 |
87
+ | --- | --- | --- |
88
+ | 크기 | 두께(linear) `small` 4 · `medium` 8 · `large` 12, 모서리 `radius.full`, 폭은 부모를 채운다. circular 지름 `small` 24 · `medium` 40 · `large` 64, 획 3 · 4 · 6 | `progressRecipe.sizes`·`circular`, `.hjm-progress__native` |
89
+ | 간격 | 문구 줄(label·값)과 막대 사이 `spacing.xs` 8. Web은 label과 값 문구 사이 `spacing.md` 16. 여러 개를 쌓을 때 간격은 감싸는 [Stack](stack.md)이 준다 | `.hjm-progress`, `.hjm-progress__copy`, Native `gap: spacing.xs` |
90
+ | 순서·정렬 | label은 시작 쪽, 값 문구는 끝 쪽, 둘 다 막대 **위** 한 줄. circular도 문구 줄을 링 위에 두고 링은 시작 쪽에 붙는다(두 플랫폼) | `.hjm-progress[data-shape="circular"]`, Native `Progress` |
91
+ | 고정·스크롤 | 고정되지 않는다. 카드·목록 행 본문 아래에 둔다. 스크롤 위치 표시는 `ScrollProgress`를 스크롤 영역 위에 둔다 | — |
92
+ | 좁은 폭·큰 글자 | 문구 줄은 Native `flexDirection: "row"`로 줄바꿈 없이 양 끝 정렬된다. 긴 label은 짧은 i18n 문구로 둔다 | Native `Progress` |
93
+
94
+ ```text
95
+ linear circular (Web) circular (Native)
96
+ 업로드 중 64% 업로드 중 64% ◯ 업로드 중 64%
97
+ └─ gap spacing.xs 8 ◯
98
+ ████████████░░░░░░░ ← 8 (medium)
99
+ ```
100
+
101
+ ## 꼭 지킬 것
102
+
103
+ - `value`와 `max`는 같은 단위로 넘긴다. `value={0.64}`만 넘기면 0.64%다([단위 표](../../progress.md)).
104
+ `max ≤ 0`, 범위 밖 `value`는 던진다.
105
+ - 이름은 지역화한 `label`로 준다. Native는 보이는 label 없이 `accessibilityLabel`만 줄 수도 있다(`ScrollProgress`는 `label` 필수).
106
+ - 색은 `tone`, 두께는 `size`로만 바꾸고 배치는 `layoutStyle`로 한다. Native의 `style`·`labelStyle`·`valueStyle`·`trackStyle`·`indicatorStyle`은
107
+ deprecated(개발 모드 경고, 다음 major 제거)다.
108
+ - `ScrollProgress`는 window에 자동으로 붙지 않는다. Web은 스크롤 host를, Native는 측정값을 반드시 넘긴다.
109
+ - 작업 완료율에는 `ScrollProgress`가 아니라 `Progress`를 쓴다.
110
+
111
+ ## 플랫폼 차이
112
+
113
+ | 항목 | Web | Native |
114
+ | --- | --- | --- |
115
+ | `label` 타입 | `ReactNode`, 필수 | `string`, 또는 `accessibilityLabel`만 |
116
+ | 요소 | `<progress>` (ref 전달) | `accessibilityRole="progressbar"` View |
117
+ | 보조 문구 | 없음 | `accessibilityHint` |
118
+ | 스크롤 측정 | `useScrollMetrics(host)` | 제품이 ScrollView 이벤트로 계산 |
119
+
120
+ ## 함정
121
+
122
+ - 1.12.0 전 Native `max` 기본값은 1이었다. 분수 값을 넘기던 옛 코드는 `max={1}`을 명시해야 한다([이관표](../../migration-native-legacy-removal.md)).
@@ -0,0 +1,122 @@
1
+ # QRCode
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [QRCode](../../qr-code.md), `src/qr-code-recipe.ts`(`qrCodeRecipe`)
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/큐알 코드`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 문자열(초대 링크, 연결 코드, 결제·체크인 URL)을 다른 기기의 카메라로 스캔하게 할 때 쓴다.
14
+ QR을 찍을 수 없는 사용자를 위한 대체 행동(링크 복사, 코드 직접 입력)을 항상 같이 둔다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 서버가 만든 QR 이미지를 그대로 보여 줌 | [Image](image.md) |
21
+ | 짧은 코드를 사람이 읽고 입력함 | [Text](text.md), 입력 쪽은 [OTPField](otp-field.md) |
22
+ | 링크를 눌러 이동 | [Link](link.md) |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `QRCode` | 기본(root에서는 내보내지 않는다) | `/qr-code` | `/qr-code` |
29
+
30
+ granular subpath로만 가져온다. 이 subpath는 optional peer를 import 한다.
31
+
32
+ ### 필요한 peer
33
+
34
+ 정확한 버전, `peerDependenciesMeta` optional.
35
+
36
+ - Web: `qrcode-generator` 2.0.4
37
+ - Native: `qrcode-generator` 2.0.4, `react-native-svg` 15.15.5(native 모듈, dev client 재빌드 필요)
38
+
39
+ ## 최소 사용 예
40
+
41
+ ```tsx
42
+ // Web
43
+ import { Button } from "@hjmds/react/actions";
44
+ import { QRCode } from "@hjmds/react/qr-code";
45
+ import { Stack } from "@hjmds/react/layout";
46
+ import { spacing } from "@hjmds/design-contracts/foundations";
47
+
48
+ <QRCode
49
+ value={inviteUrl}
50
+ label={t("invite.qrLabel")}
51
+ fallback={<Stack layoutStyle={{ marginTop: spacing.md }}><Button tone="secondary" onClick={copyLink}>{t("invite.copyLink")}</Button></Stack>}
52
+ />
53
+ ```
54
+
55
+ ```tsx
56
+ // Native
57
+ import { Button } from "@hjmds/react-native/actions";
58
+ import { QRCode } from "@hjmds/react-native/qr-code";
59
+ import { Stack } from "@hjmds/react-native/primitives";
60
+ import { spacing } from "@hjmds/design-contracts/foundations";
61
+
62
+ <QRCode
63
+ value={inviteUrl}
64
+ label={t("invite.qrLabel")}
65
+ size={224}
66
+ fallback={<Stack layoutStyle={{ marginTop: spacing.md }}><Button tone="secondary" onPress={copyLink}>{t("invite.copyLink")}</Button></Stack>}
67
+ />
68
+ ```
69
+
70
+ ## 축과 기본값
71
+
72
+ | prop | 값 | 기본값 | 설명 |
73
+ | --- | --- | --- | --- |
74
+ | `value` | 문자열 | 필수 | 비면 `TypeError`, 용량 초과는 `RangeError` |
75
+ | `label` | 문자열 | 필수 | 접근성 이름 |
76
+ | `fallback` | `ReactNode` | 필수 | QR 아래에 그려지는 대체 행동(`null`·`false`면 `TypeError`) |
77
+ | `size` | 숫자(px) | `192` | 실제 크기는 모듈 수의 정수배로 내림된다. 모듈당 2px 미만이면 `TypeError` |
78
+ | `level` | `L` · `M` · `Q` · `H` | `M` | 오류 정정 수준 |
79
+ | `layoutStyle` | 배치 전용 style 객체 | — | 루트 배치만(Native는 미게시(1.12.1 이후)) |
80
+
81
+ 콜백은 없다. `style`·`className`은 두 renderer 모두 없다. 1.12.1 Native는 `layoutStyle`이 없어 감싸는 레이아웃에서 배치한다.
82
+
83
+ ## 배치
84
+
85
+ | 항목 | 값 | 근거 |
86
+ | --- | --- | --- |
87
+ | 크기 | 기본 192×192. 실제 변은 (모듈 수 + 여백 4모듈×2)의 정수배로 내림돼 `size`보다 조금 작을 수 있다. 모듈당 최소 2px. 흰 여백 4모듈이 그림 안에 포함되므로 바깥에 흰 테두리를 또 두르지 않는다 | `qrCodeRecipe.quietZone`·`minModuleSize`, `qr-code.tsx` |
88
+ | 간격 | QR과 `fallback` 사이 간격을 컴포넌트가 주지 않는다(Web CSS 없음, Native는 감싼 `View`만). `fallback` 슬롯 안의 [Stack](stack.md)에 `layoutStyle={{ marginTop: spacing.md }}`로 16을 준다. QRCode 바깥 Stack은 내부 두 요소의 간격을 바꿀 수 없다 | `packages/react/src/qr-code.tsx`, `packages/react-native/src/qr-code.tsx` |
89
+ | 순서·정렬 | QR 위, `fallback` 바로 아래. 가운데 정렬은 감싸는 레이아웃이 한다 | 같은 파일 |
90
+ | 고정·스크롤 | 고정되지 않는다. 카드·시트 본문 안에 둔다 | — |
91
+ | 좁은 폭·큰 글자 | 좁은 폭에서 `size`를 화면 폭보다 크게 두지 않는다. 큰 글자에서도 QR 크기는 그대로이고 `fallback`만 커진다 | — |
92
+
93
+ ```text
94
+ ┌──────────────────────┐
95
+ │ ┌────────┐ │
96
+ │ │ ▓▒▓▒▓▒ │ 192 │ ← 흰 여백 4모듈 포함
97
+ │ └────────┘ │
98
+ │ (fallback marginTop md) │
99
+ │ [ 링크 복사 ] │ ← fallback (필수)
100
+ └──────────────────────┘
101
+ ```
102
+
103
+ ## 꼭 지킬 것
104
+
105
+ - 앱에 peer가 설치돼 있는지 먼저 확인한다. 없으면 tsc·lint·단위 테스트는 통과하지만 기기 Metro 번들에서
106
+ `Unable to resolve module`로 죽는다(2026-10 utilverse, celebration 등 같은 부류 subpath 포함).
107
+ 확인: `grep 'from "' node_modules/@hjmds/react-native/dist/qr-code.js`와 앱 `package.json` 비교.
108
+ - `label`이 비었거나 `fallback`이 없거나(`null`/`false`) `size`가 모듈당 2px 미만이면 렌더 중
109
+ `TypeError`를 던진다. 빈 `value`·잘못된 `level`도 `TypeError`, 용량 초과는 `RangeError`다.
110
+ 사용자 입력을 그대로 넣는 화면이면 error boundary 또는 길이 제한을 둔다.
111
+ - 색은 테마와 무관하게 검정 모듈·흰 배경·4모듈 여백으로 고정된다. 브랜드 색·로고를 얹지 않는다.
112
+ - 만료·인증·목적지 권한은 서버(제품) 소유다. QR 컴포넌트는 인코딩만 보증한다.
113
+
114
+ ## 플랫폼 차이
115
+
116
+ | 항목 | Web | Native |
117
+ | --- | --- | --- |
118
+ | 그리기 | 인라인 `<svg role="img">` | `react-native-svg`, 감싼 `View`가 `accessibilityRole="image"` |
119
+ | 필요한 peer | `qrcode-generator` | `qrcode-generator`, `react-native-svg` |
120
+ | 배치 | `layoutStyle` | `layoutStyle`(미게시(1.12.1 이후), 1.12.1은 감싸는 View) |
121
+
122
+ - 2026-10-06 독립 재구현에서 QRCode 바깥 Stack의 gap으로 내부 QR–fallback 간격을 바꿀 수 없음을 확인해 슬롯 안 여백 예제로 수정했다.