@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,103 @@
1
+ # 권한 안내
2
+
3
+ - 단계: 화면
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/기본 흐름/권한 안내`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
+ - 스토리북: `배포/화면/소개/권한 안내`
10
+
11
+ ## 목적
12
+
13
+ PermissionScreen을 사용해 권한 안내 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
14
+
15
+ ## 영역 구조
16
+
17
+ ```text
18
+ host: 남은 높이·safe area·키보드
19
+ └─ 선택적 그림 → 권한 이유 → 상태별 행동
20
+ ```
21
+
22
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
23
+ | --- | --- | --- | --- |
24
+ | 바깥 틀 | PermissionScreen | route 본문 | [API 배치 규칙](../components/permission-screen.md#배치), host 남은 높이 |
25
+ | 내용 | 공개 슬롯 | 선택적 그림 → 권한 이유 → 상태별 행동 | 화면 recipe의 sectionGap·itemGap; 슬롯 안은 각 지침 토큰 |
26
+ | 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
27
+
28
+ ## 버튼과 행동 위치
29
+
30
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
31
+ | --- | --- | --- | --- |
32
+ | 작업 | PermissionScreen 공개 행동 슬롯 | prompt 요청, denied 설정, granted 계속, unavailable 대안 | 같은 표면에 경쟁하는 primary 하나만 |
33
+ | 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
34
+
35
+ ## 상태
36
+
37
+ | 상태 | 화면 모습 | 행동 |
38
+ | --- | --- | --- |
39
+ | 기본 | 선택적 그림 → 권한 이유 → 상태별 행동 | 각 공개 콜백을 제품 상태에 연결 |
40
+ | 로딩 | 최초 조회는 본문 상태, 저장은 해당 행동 pending | 중복 제출 차단; 성공을 먼저 표시하지 않음 |
41
+ | 빈 | 실제 조회 0건 또는 아직 작성하지 않은 상태 안내 | 시작·조건 해제 등 맥락에 맞는 대안 |
42
+ | 오류 | 렌더 시 OS 요청 금지; 설정 복귀 시 제품이 상태 재조회 | 실패 원인과 재시도 경로 제공 |
43
+
44
+ ## 사용하는 지침
45
+
46
+ | 지침 | 쓰는 곳 |
47
+ | --- | --- |
48
+ | [PermissionScreen](../components/permission-screen.md) | 필수 props·슬롯·플랫폼 차이 |
49
+ | [Button](../components/button.md) | 동작·로딩·보조 행동 |
50
+ | [ScreenLayout](../components/screen-layout.md) | 화면 높이·본문 교체·스크롤 소유 |
51
+
52
+ ## 코드 골격
53
+
54
+ ```tsx
55
+ // Web
56
+ import { Text } from "@hjmds/react/layout";
57
+ import { PermissionScreen } from "@hjmds/react/screen-flows";
58
+
59
+ <PermissionScreen
60
+ title={t("permission.camera.title")}
61
+ status={cameraStatus /* navigator.permissions 등으로 제품이 조회한 값 */}
62
+ illustration={cameraArt}
63
+ explanation={<Text>{t("permission.camera.why")}</Text>}
64
+ request={{ label: t("permission.allow"), onAction: requestCamera, pending: requesting }}
65
+ settings={{ label: t("permission.browserSettings"), onAction: showSettingsHelp }}
66
+ continueAction={{ label: t("common.continue"), onAction: goNext }}
67
+ skip={{ label: t("common.later"), onAction: goNext }}
68
+ />
69
+ ```
70
+
71
+ ```tsx
72
+ // Native
73
+ import { Text } from "@hjmds/react-native/primitives";
74
+ import { PermissionScreen } from "@hjmds/react-native/screen-flows";
75
+ import { Linking } from "react-native";
76
+
77
+ <PermissionScreen
78
+ title={t("permission.camera.title")}
79
+ status={cameraStatus /* 제품이 OS에서 조회한 값 */}
80
+ illustration={cameraArt}
81
+ explanation={<Text>{t("permission.camera.why")}</Text>}
82
+ request={{ label: t("permission.allow"), onAction: requestCamera, pending: requesting }}
83
+ settings={{ label: t("permission.openSettings"), onAction: () => Linking.openSettings() }}
84
+ continueAction={{ label: t("common.continue"), onAction: goNext }}
85
+ skip={{ label: t("common.later"), onAction: goNext }}
86
+ />
87
+ ```
88
+
89
+ 콜백·데이터·지역화 함수는 제품에서 공급한다. Web·Native import와 필수 props는 위 API 지침에서 확인한다. Web은 브라우저 설정을 코드로 열 수 없으므로 `settings`는 설정 방법 안내로 연결한다. Native는 `Linking.openSettings()`를 쓴다.
90
+
91
+ ## 큰 글자·다크·좁은 폭
92
+
93
+ | 조건 | 바뀌는 것 |
94
+ | --- | --- |
95
+ | 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
96
+ | 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
97
+ | 좁은 폭 | 320px부터 한 열로 읽기 순서 유지. 가상화 본문은 scroll=content, 중첩 스크롤 금지 |
98
+ | 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
99
+
100
+ ## 함정
101
+
102
+ - 렌더 시 OS 요청 금지; 설정 복귀 시 제품이 상태 재조회.
103
+ - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
@@ -0,0 +1,347 @@
1
+ # 서비스 소개
2
+
3
+ - 단계: 화면
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: `showcase/web/src/patterns/Landing.stories.tsx`·`Landing.previews.tsx`, `showcase/native/src/Landing.stories.tsx`·`introduction-preview.tsx`, 변형 `showcase/*/reference-flow-previews.tsx`(`ServiceIntroduction`), `showcase/shared/landing-pattern.ts`, `showcase/native/src/pattern-status.tsx`, `packages/react/src/effect-surface.tsx`, `packages/react-native/src/effect-surface.tsx`. 2026-10-06 사용자 승인으로 실험 `서비스 소개/제품 체험 중심`(설명과 사례 중심 포함)을 배포하면서 같은 소개 목적의 배포 `화면/랜딩 화면`과 한 항목으로 합쳤다(Web id `patterns-landing` 보존, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
+ - 스토리북: `배포/화면/소개/서비스 소개`
10
+
11
+ ## 목적
12
+
13
+ 제품을 처음 보는 사람에게 한 문장 가치 제안을 보여 주고 같은 화면에서 첫 행동(짧은 입력)을 체험하게 하는 소개 화면이다.
14
+ 히어로 → 결과 미리보기 → 기능 소개 → 자주 묻는 질문 → 마지막 행동 순서의 세로 한 열이다. 가입·결제·후기·실적 수치는
15
+ 이 화면에 없고 제품이 실제 근거와 함께 더한다.
16
+ 스토리 `기본`이 이 랜딩이고, 같은 목적의 두 변형은 첫 구획 배치만 다르다: `제품 체험 중심`(히어로·행동과 제품 미리보기를 두 열로),
17
+ `설명과 사례 중심`(히어로를 먼저 한 열로 두고 설명과 미리보기를 두 열로). 변형은 아래 [변형](#변형-제품-체험-중심설명과-사례-중심) 절을 본다.
18
+
19
+ ## 영역 구조
20
+
21
+ ```text
22
+ 좁은 폭(Native·모바일 Web) — 전체가 하나의 세로 스크롤, 하단 고정 영역 없음
23
+ ┌ 상단 안전 영역(헤더 소유) ───────────────┐
24
+ │ 바깥 틀: 좌우 Container gutter │
25
+ │ (폭 < 600 compact 16 · 이상 regular 20) │
26
+ │ Native 위아래 spacing.lg 20 │
27
+ │ ┌ ① 히어로 EffectSurface(mesh+grain) ──┐ │
28
+ │ │ (안쪽 여백 spacing.xl 24 — 제품이 줌) │ │
29
+ │ │ eyebrow Text tone=muted │ │ Stack gap lg 20
30
+ │ │ 제목 Heading level1 (h1) │ │
31
+ │ │ 소개 Text as=p │ │
32
+ │ │ [ 첫 기록 써보기 ] ← 주 행동(primary) │ │
33
+ │ └──────────────────────────────────────┘ │
34
+ │ ↕ spacing.xl 24 │
35
+ │ ┌ ② 미리보기 Surface padding lg 20 ────┐ │
36
+ │ │ Heading level2 │ │ Stack gap md 16
37
+ │ │ (저장 직후) "추가했어요" status │ │
38
+ │ │ List > ListRow 한 줄씩(최신이 위) │ │
39
+ │ └──────────────────────────────────────┘ │
40
+ │ ③ 기능 Heading level2 │
41
+ │ ┌ Surface padding lg ─┐ Heading level3 + Text (Stack gap sm 12)
42
+ │ └─────────────────────┘ × 3, 사이 24 │
43
+ │ ④ FAQ Heading level2 │
44
+ │ ▸ 질문 1 Collapsible (트리거 최소 44) │
45
+ │ ▸ 질문 2 │
46
+ │ ⑤ [ 첫 기록 써보기 ] ← 같은 행동 반복 │ secondary
47
+ │ 범위 안내 Text tone=muted │
48
+ └ 하단: 고정 영역 없음, 내용이 스크롤 ─────┘
49
+
50
+ ⑥ 입력 Sheet(주 행동을 누르면)
51
+ ┌ 첫 기록을 남겨요 ─────────────── (닫기) ┐
52
+ │ (저장 실패면 Notice danger) │ Stack gap lg 20
53
+ │ 오늘의 한 줄 [ TextField ] │
54
+ │ (비면 오류 문구) │
55
+ │────────────────────────────────────────│
56
+ │ footer: [ 미리보기에 추가 ] (고정) │ ← 시트 주 행동
57
+ └ 하단 안전 영역(시트 소유) / Native는 키보드 위로 ┘
58
+ ```
59
+
60
+ 넓은 폭 Web도 한 열이다. 바깥은 [Container](../components/container.md) `size="content"`(`layout.contentMaxWidth` 1200)로
61
+ 묶는다. 기능 카드 3개를 가로로 놓는 배치는 스토리에서 확인되지 않았다.
62
+
63
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
64
+ | --- | --- | --- | --- |
65
+ | 바깥 틀 | Web `main` > `Container size="content" gutter={gutter}` > `Stack gap="xl"`(문서 스크롤) · Native `ScrollView`(`contentContainerStyle={{ paddingVertical: spacing.lg }}`) > `Container size="content" gutter={gutter}` > `Stack gap="xl"` | 화면 전체, 스크롤. 위 안전 영역은 내비게이션 헤더(또는 [TopBar](../components/top-bar.md) `safeAreaTop`)가 맡고 헤더 없이 띄울 때만 제품이 감싼다. 아래 고정 영역은 없다. 바깥에는 입력이 없어 키보드 처리는 ⑥ 시트가 맡는다 | 좌우 gutter: 폭 600 미만 `compact` 16(`layout.pagePadding.compact`), 이상 `regular` 20([화면 여백](../tokens/layout.md)). Native 위아래 `spacing.lg` 20. 직계 요소 사이 `spacing.xl` 24(`layout.sectionGap`) |
66
+ | ① 히어로 | EffectSurface `descriptor={{ layers: ["mesh","grain"], active: false }}` > Stack `gap="lg"` > Text·Heading·Text·Button | 맨 위 | 요소 사이 `spacing.lg` 20. EffectSurface는 안쪽 여백이 없어 제품이 컨테이너에 `spacing.xl` 24를 준다(Web `className`, Native `style`) |
67
+ | ② 미리보기 | Surface `padding="lg"` > Stack `gap="md"` > Heading·상태 Text·List | 히어로 아래 | 안쪽 20, 요소 사이 `spacing.md` 16, 한 줄 행 최소 56(`layout.rowHeight.singleLine`). 새 기록이 맨 위 |
68
+ | ③ 기능 | Heading level2 + Surface `padding="lg"` ×3 > Stack `gap="sm"` | 미리보기 아래 | 카드 안 `spacing.sm` 12, 카드 사이 24 |
69
+ | ④ FAQ | Heading level2 + Collapsible × n | 기능 아래 | 트리거 최소 44(`control.minTouchTarget`), 트리거 위아래 `spacing.xs` 8(Web) |
70
+ | ⑤ 마지막 행동 | Button `secondary` + Text `tone="muted"` | 맨 아래 | 사이 24 |
71
+ | ⑥ 입력 | Sheet(Native `keyboardAvoidance` `scrollable`) > Stack `gap="lg"` > Notice(실패 때)·TextField, 제출은 `footer` | 오버레이, 화면 아래 | 시트 좌우 `spacing.lg` 20, TextField 높이 44(`control.fieldHeight`). 아래 안전 영역·키보드는 시트가 맡는다 |
72
+
73
+ ### 변형: 제품 체험 중심·설명과 사례 중심
74
+
75
+ ```text
76
+ ScreenLayout(제목 = 소개 문장, 최대 720, 바깥 padding spacing.md 16) — 본문 스크롤
77
+ ├─ 제품 체험 중심: [히어로·행동 | 제품 미리보기] → 특징 3열 → FAQ → 끝 행동 카드
78
+ └─ 설명과 사례 중심: 히어로·행동 → [설명 | 제품 미리보기] → 특징 3열 → FAQ → 끝 행동 카드
79
+ [a | b] = Grid 두 열(열 최소 240), 좁은 폭·큰 글자에서 한 열로 접힌다
80
+ 입력: Sheet(아래 고정 footer에 저장)
81
+ ```
82
+
83
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
84
+ | --- | --- | --- | --- |
85
+ | 바깥 틀 | ScreenLayout `title`(소개 문장) | route 본문, host가 남은 높이·safe area | 폭 최대 720, 바깥 padding `spacing.md` 16([ScreenLayout 배치](../components/screen-layout.md#배치)) |
86
+ | 내용 | Stack `gap="xl"` > Grid(`columns={{ compact: 2 }}`·`{{ compact: 3 }}`, `gap={{ compact: "xl" }}`, `minColumnWidth={{ compact: 240 }}`) | 위 순서 | 구획 사이 `spacing.xl` 24, 열 최소 240. 구획 제목은 Heading `level4`·`level5` |
87
+ | 끝 행동 | Surface `padding="lg"` > Stack `gap="md"` > Heading `level5` + Button primary | FAQ 아래 | 안쪽 20, 사이 16 |
88
+ | 입력 | Sheet > TextField, 저장은 `footer` | 오버레이 | 빈 입력이면 저장 비활성 |
89
+
90
+ ## 버튼과 행동 위치
91
+
92
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
93
+ | --- | --- | --- | --- |
94
+ | 주 행동(체험 시작) | Button 기본 primary, `medium` | ① 히어로 소개문 바로 아래 | 1 |
95
+ | 같은 행동 반복 | Button `secondary`, 같은 라벨·같은 동작 | ⑤ FAQ 다음 | 1. 큰 글자에서 히어로 행동이 첫 화면 밖으로 밀리므로 끝에 한 번 더 둔다 |
96
+ | FAQ 펼치기 | Collapsible 트리거 | ④ 각 질문 행 전체 | 질문마다 1 |
97
+ | 입력 제출 | Button 기본 primary | ⑥ Sheet `footer`(Web 끝 정렬, Native 꽉 찬 폭) | 1. 시트 안에서 따로 센다([Button](../components/button.md#꼭-지킬-것)) |
98
+ | 시트 닫기 | Sheet `closeLabel` | 시트 머리 끝 | 1. 저장 중에는 Sheet `busy`로 막는다 |
99
+ | 파괴 행동 | — | — | 없음 |
100
+
101
+ 화면의 primary는 히어로 행동 하나다. 시트 `footer`의 제출은 시트 안에서 따로 센다.
102
+ 두 변형은 히어로 행동과 끝 행동 카드의 버튼이 같은 Sheet를 연다(같은 행동 반복). FAQ는 Collapsible 대신 `selected` 토글 Button 하나로 답을 연다.
103
+
104
+ 키보드: Native 시트는 `keyboardAvoidance`로 키보드 위에 붙고 `footer`가 키보드 위에 고정돼 제출 버튼이 가려지지 않는다.
105
+ 바깥 ScrollView에는 입력이 없다.
106
+
107
+ ## 상태
108
+
109
+ | 상태 | 화면 모습 | 행동 |
110
+ | --- | --- | --- |
111
+ | 기본 | ①~⑤, ②에 예시 한 줄 | 주 행동 |
112
+ | 로딩 | 스토리에 없음. 저장이 비동기면 `footer` 제출 Button `loading`, Sheet `busy`로 닫기를 막는다. 입력 값은 그대로 둔다 | 기다림 |
113
+ | 빈 | ②에 기록이 0개인 경우는 스토리에 없다. 예시 한 줄을 처음부터 넣어 미리보기가 비지 않게 한다 | — |
114
+ | 오류 | 입력 오류: ⑥ TextField `error`(`landing.error.empty`). 입력을 바꾸면 지워지고 시트는 닫히지 않는다. 저장 실패는 스토리에 없음. 시트 본문 맨 위에 [Notice](../components/notice.md) `tone="danger"`(Native `announcement="assertive"` — 기본 `none`), 문구는 원인별 키: 네트워크 `landing.error.offline`, 서버 `landing.error.server`. 입력 값과 ②의 이전 기록은 유지하고 시트를 닫지 않는다. 다시 시도는 같은 `footer` 제출 버튼이며, 또 실패하면 Notice를 같은 자리에 두고 `loading`·`busy`만 푼다 | 다시 입력 · 다시 제출 |
115
+ | 성공 | 시트가 닫히고 ② 맨 위에 새 행, "추가했어요" 상태 문구가 읽힌다 | — |
116
+ | FAQ 펼침 | 질문 아래에 답이 열린다(같은 자리 enter/exit 모션) | 다시 눌러 접기 |
117
+ | 제품 체험 중심 | `제품 체험 중심` 스토리: 히어로·행동과 제품 미리보기 두 열 → 특징 3열 → FAQ → 끝 행동 카드. 저장하면 미리보기가 방금 쓴 장면으로 바뀐다 | 히어로·끝 행동 = 같은 Sheet |
118
+ | 설명과 사례 중심 | `설명과 사례 중심` 스토리: 히어로·행동 한 열 → 설명과 미리보기 두 열 → 이하 같음 | 같은 Sheet |
119
+
120
+ ## 사용하는 지침
121
+
122
+ | 지침 | 쓰는 곳 |
123
+ | --- | --- |
124
+ | [Container](../components/container.md) | 바깥 틀 폭·좌우 여백 |
125
+ | [화면 여백과 너비](../tokens/layout.md) | gutter 선택(`resolveWindowClass`) |
126
+ | [EffectSurface](../components/effect-surface.md) | ① 히어로 배경 장식 |
127
+ | [Heading](../components/heading.md) | 히어로 level1, 영역 제목 level2, 기능 카드 level3 |
128
+ | [Text](../components/text.md) | eyebrow·소개·범위 안내·상태 문구 |
129
+ | [Button](../components/button.md) | 주 행동, 반복 행동, 시트 제출 |
130
+ | [Surface](../components/surface.md) | ② 미리보기, ③ 기능 카드 |
131
+ | [List](../components/list.md) · [ListRow](../components/list-row.md) | ② 기록 목록 |
132
+ | [Collapsible](../components/collapsible.md) | ④ FAQ |
133
+ | [ScreenLayout](../components/screen-layout.md) · [Grid](../components/grid.md) | 변형의 바깥 틀 · 두 열 ↔ 한 열 |
134
+ | [Sheet](../components/sheet.md) | ⑥ 입력 |
135
+ | [Field](../components/field.md) | ⑥ `TextField` |
136
+ | [Notice](../components/notice.md) | ⑥ 저장 실패 |
137
+ | [Stack](../components/stack.md) | 모든 세로 리듬 |
138
+ | [간격](../tokens/spacing.md) | `spacing.sm`·`md`·`lg`·`xl` |
139
+
140
+ ## 코드 골격
141
+
142
+ 카피·기능 목록·FAQ·장식 seed는 제품 소유다. 오류 문구는 상태→키 상수 표로 고른다.
143
+
144
+ ```tsx
145
+ // Web
146
+ import { Container, Stack, Surface, Text } from "@hjmds/react/layout";
147
+ import { Heading } from "@hjmds/react/heading";
148
+ import { Button } from "@hjmds/react/actions";
149
+ import { TextField } from "@hjmds/react/forms";
150
+ import { Notice } from "@hjmds/react/feedback";
151
+ import { Sheet } from "@hjmds/react/overlays";
152
+ import { Collapsible } from "@hjmds/react/collapsible";
153
+ import { EffectSurface } from "@hjmds/react/effect-surface";
154
+ import { List, ListRow } from "@hjmds/react/display";
155
+ import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
156
+
157
+ const failureKey = { offline: "landing.error.offline", server: "landing.error.server" } as const;
158
+ const gutter = resolveWindowClass(window.innerWidth) === "compact" ? "compact" : "regular";
159
+
160
+ <main><Container size="content" gutter={gutter}><Stack gap="xl">
161
+ {/* landing-hero: padding: var(--hjm-space-xl) — EffectSurface는 안쪽 여백이 없다 */}
162
+ <EffectSurface className="landing-hero" descriptor={{ layers: ["mesh", "grain"], seed: "landing", active: false }}>
163
+ <Stack gap="lg">
164
+ <Text tone="muted">{t("landing.eyebrow")}</Text>
165
+ <Heading level="level1">{t("landing.title")}</Heading>
166
+ <Text as="p">{t("landing.intro")}</Text>
167
+ <Button onClick={start}>{t("landing.cta")}</Button>
168
+ </Stack>
169
+ </EffectSurface>
170
+ <Surface padding="lg"><Stack gap="md">
171
+ <Heading level="level2">{t("landing.preview")}</Heading>
172
+ {saved ? <Text role="status">{t("landing.saved")}</Text> : null}
173
+ <List label={t("landing.preview")}>{notes.map(n => <ListRow key={n.id} title={n.text} />)}</List>
174
+ </Stack></Surface>
175
+ <Heading level="level2">{t("landing.features")}</Heading>
176
+ {features.map(f => <Surface key={f.id} padding="lg"><Stack gap="sm">
177
+ <Heading level="level3">{t(f.titleKey)}</Heading><Text>{t(f.bodyKey)}</Text></Stack></Surface>)}
178
+ <Heading level="level2">{t("landing.faq")}</Heading>
179
+ {faqs.map(q => <Collapsible key={q.id} trigger={t(q.questionKey)}><Text>{t(q.answerKey)}</Text></Collapsible>)}
180
+ <Button tone="secondary" onClick={start}>{t("landing.cta")}</Button>
181
+ <Text tone="muted">{t("landing.scope")}</Text>
182
+ </Stack></Container>
183
+ <Sheet title={t("landing.sheetTitle")} closeLabel={t("common.close")} open={open} onOpenChange={setOpen} busy={saving}
184
+ footer={<Button loading={saving} onClick={submit}>{t("landing.submit")}</Button>}>
185
+ <Stack gap="lg">
186
+ {failure ? <Notice tone="danger" title={t(failureKey[failure])} /> : null}
187
+ <TextField label={t("landing.field")} value={draft} onValueChange={v => { setDraft(v); setEmpty(false); }}
188
+ {...(empty ? { error: t("landing.error.empty") } : {})} />
189
+ </Stack>
190
+ </Sheet>
191
+ </main>
192
+ ```
193
+
194
+ ```tsx
195
+ // Native
196
+ import { ScrollView, useWindowDimensions } from "react-native";
197
+ import { spacing } from "@hjmds/design-contracts/foundations";
198
+ import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
199
+ import { Container, Stack, Text } from "@hjmds/react-native/primitives";
200
+ import { Heading } from "@hjmds/react-native/heading";
201
+ import { Button } from "@hjmds/react-native/actions";
202
+ import { TextField } from "@hjmds/react-native/inputs";
203
+ import { Notice } from "@hjmds/react-native/feedback";
204
+ import { Sheet } from "@hjmds/react-native/overlays";
205
+ import { EffectSurface } from "@hjmds/react-native/effect-surface";
206
+
207
+ const failureKey = { offline: "landing.error.offline", server: "landing.error.server" } as const;
208
+ const { width } = useWindowDimensions();
209
+ const gutter = resolveWindowClass(width) === "compact" ? "compact" : "regular";
210
+
211
+ <ScrollView contentContainerStyle={{ paddingVertical: spacing.lg }}>
212
+ <Container size="content" gutter={gutter}><Stack gap="xl">
213
+ <EffectSurface style={{ padding: spacing.xl }} descriptor={{ layers: ["mesh", "grain"], seed: "landing", active: false }}>
214
+ <Stack gap="lg">
215
+ <Heading level="level1">{t("landing.title")}</Heading>
216
+ <Text>{t("landing.intro")}</Text>
217
+ <Button onPress={start}>{t("landing.cta")}</Button>
218
+ </Stack>
219
+ </EffectSurface>
220
+ {/* 미리보기·기능·FAQ·반복 행동(secondary)·범위 안내: Web과 같은 순서, onPress */}
221
+ </Stack></Container>
222
+ <Sheet keyboardAvoidance scrollable busy={saving} title={t("landing.sheetTitle")} closeLabel={t("common.close")}
223
+ open={open} onOpenChange={setOpen}
224
+ footer={<Button fullWidth loading={saving} onPress={submit}>{t("landing.submit")}</Button>}>
225
+ <Stack gap="lg">
226
+ {failure ? <Notice tone="danger" announcement="assertive" title={t(failureKey[failure])} /> : null}
227
+ <TextField label={t("landing.field")} value={draft} onValueChange={v => { setDraft(v); setEmpty(false); }}
228
+ {...(empty ? { error: t("landing.error.empty") } : {})} />
229
+ </Stack>
230
+ </Sheet>
231
+ </ScrollView>
232
+ ```
233
+
234
+ ### 변형 코드
235
+
236
+ `제품 체험 중심` 골격이다. `설명과 사례 중심`은 히어로(`intro`)를 Grid 밖 맨 위에 두고 Grid 첫 칸에 설명(Heading `level4` + 본문)을 넣는다.
237
+
238
+ ```tsx
239
+ // Web
240
+ import { Button } from "@hjmds/react/actions";
241
+ import { Heading } from "@hjmds/react/heading";
242
+ import { TextField } from "@hjmds/react/forms";
243
+ import { Grid, Stack, Surface, Text } from "@hjmds/react/layout";
244
+ import { Sheet } from "@hjmds/react/overlays";
245
+ import { ScreenLayout } from "@hjmds/react/screens";
246
+
247
+ <>
248
+ <ScreenLayout title={t("intro.title")}>
249
+ <Stack gap="xl">
250
+ <Grid columns={{ compact: 2 }} gap={{ compact: "xl" }} minColumnWidth={{ compact: 240 }}>
251
+ <Stack gap="lg">
252
+ <Text tone="brand" variant="label">{t("intro.eyebrow")}</Text>
253
+ <Text>{t("intro.lead")}</Text>
254
+ <Button onClick={() => setOpen(true)}>{t("intro.start")}</Button>
255
+ </Stack>
256
+ {preview}
257
+ </Grid>
258
+ <Grid columns={{ compact: 3 }} gap={{ compact: "xl" }} minColumnWidth={{ compact: 240 }}>
259
+ {features.map((item) => <Stack key={item.id} gap="sm"><Heading level="level5" semanticLevel={2}>{item.title}</Heading><Text>{item.body}</Text></Stack>)}
260
+ </Grid>
261
+ <Button tone="secondary" selected={faqOpen} onClick={toggleFaq}>{t("intro.faq.question")}</Button>
262
+ {faqOpen ? <Text>{t("intro.faq.answer")}</Text> : null}
263
+ <Surface padding="lg">
264
+ <Stack gap="md">
265
+ <Heading level="level5" semanticLevel={2}>{t("intro.closing")}</Heading>
266
+ <Button onClick={() => setOpen(true)}>{t("intro.startAgain")}</Button>
267
+ </Stack>
268
+ </Surface>
269
+ </Stack>
270
+ </ScreenLayout>
271
+ <Sheet open={open} onOpenChange={setOpen} title={t("intro.sheet.title")} closeLabel={t("common.close")}
272
+ footer={<Button disabled={!draft.trim()} onClick={saveDraft}>{t("intro.sheet.save")}</Button>}>
273
+ <TextField label={t("intro.sheet.field")} value={draft} onValueChange={setDraft} />
274
+ </Sheet>
275
+ </>
276
+ ```
277
+
278
+ ```tsx
279
+ // Native
280
+ import { Button } from "@hjmds/react-native/actions";
281
+ import { Heading } from "@hjmds/react-native/heading";
282
+ import { TextField } from "@hjmds/react-native/inputs";
283
+ import { Grid, Stack, Surface, Text } from "@hjmds/react-native/primitives";
284
+ import { Sheet } from "@hjmds/react-native/overlays";
285
+ import { ScreenLayout } from "@hjmds/react-native/screens";
286
+
287
+ <>
288
+ <ScreenLayout title={t("intro.title")}>
289
+ <Stack gap="xl">
290
+ <Grid columns={{ compact: 2 }} gap={{ compact: "xl" }} minColumnWidth={{ compact: 240 }}>
291
+ <Stack gap="lg">
292
+ <Text tone="brand" variant="label">{t("intro.eyebrow")}</Text>
293
+ <Text>{t("intro.lead")}</Text>
294
+ <Button onPress={() => setOpen(true)}>{t("intro.start")}</Button>
295
+ </Stack>
296
+ {preview}
297
+ </Grid>
298
+ <Grid columns={{ compact: 3 }} gap={{ compact: "xl" }} minColumnWidth={{ compact: 240 }}>
299
+ {features.map((item) => <Stack key={item.id} gap="sm"><Heading level="level5">{item.title}</Heading><Text>{item.body}</Text></Stack>)}
300
+ </Grid>
301
+ <Button tone="secondary" selected={faqOpen} onPress={toggleFaq}>{t("intro.faq.question")}</Button>
302
+ {faqOpen ? <Text>{t("intro.faq.answer")}</Text> : null}
303
+ <Surface padding="lg">
304
+ <Stack gap="md">
305
+ <Heading level="level5">{t("intro.closing")}</Heading>
306
+ <Button onPress={() => setOpen(true)}>{t("intro.startAgain")}</Button>
307
+ </Stack>
308
+ </Surface>
309
+ </Stack>
310
+ </ScreenLayout>
311
+ <Sheet open={open} onOpenChange={setOpen} title={t("intro.sheet.title")} closeLabel={t("common.close")}
312
+ footer={<Button disabled={!draft.trim()} onPress={saveDraft}>{t("intro.sheet.save")}</Button>}>
313
+ <TextField label={t("intro.sheet.field")} value={draft} onValueChange={setDraft} />
314
+ </Sheet>
315
+ </>
316
+ ```
317
+
318
+ ## 큰 글자·다크·좁은 폭
319
+
320
+ | 조건 | 바뀌는 것 |
321
+ | --- | --- |
322
+ | 큰 글자(`textScale` 2) | 제목·카드가 세로로 길어질 뿐 순서는 같다. 히어로 주 행동이 첫 화면 밖으로 밀릴 수 있어 ⑤의 반복 행동이 필요하다. 시트 본문이 길어져도 `footer` 제출은 고정이다 |
323
+ | 다크 | EffectSurface 배경은 테마 팔레트의 `bg`를 쓰고 장식 층이 그 위에 그려진다. 나머지는 테마 토큰 |
324
+ | 좁은 폭 | 한 열 그대로, 폭 600 미만은 gutter `compact` 16. 시트는 아래에 붙고 Native는 키보드 위로 올라간다 |
325
+ | 넓은 폭 Web | 한 열 유지, Container `content` 1200으로 폭을 묶고 gutter `regular` 20 |
326
+ | 모션 줄이기 | 스토리는 `active: false`로 장식 움직임을 끈다. Native는 `visible={false}`로 화면 밖 장식을 멈춘다 |
327
+ | 변형의 두 열 | 열 최소 240을 못 채우면(좁은 폭·큰 글자) Grid가 한 열로 접어 히어로 → 미리보기 순서로 읽힌다. Sheet `footer` 저장은 고정이다 |
328
+
329
+ ## 플랫폼 차이
330
+
331
+ | 항목 | Web | Native |
332
+ | --- | --- | --- |
333
+ | 바깥 스크롤 | 문서 스크롤 | `ScrollView`(위아래 `spacing.lg` 20), 좌우는 Container gutter |
334
+ | 시트 키보드 처리 | 브라우저가 처리 | `keyboardAvoidance` 명시 |
335
+ | 시트 본문 스크롤 | 자동 | `scrollable` 명시 |
336
+ | 시트 `footer` 버튼 | 끝 정렬 가로 줄 | 꽉 찬 폭 세로 열(`fullWidth`) |
337
+ | 저장 알림 | `role="status"` | 라이브 영역(Android) + iOS는 제품이 직접 알림. 문구가 새로 나타날 때 한 번 읽히게 한다(스토리 `PatternStatus announceOnMount`) |
338
+ | 실패 알림 | Notice가 자기 역할로 읽힌다 | Notice `announcement="assertive"` 명시(기본 `none`) |
339
+ | 히어로 안쪽 여백 | `className`으로 `var(--hjm-space-xl)` | `style`의 `padding: spacing.xl` |
340
+
341
+ ## 함정
342
+
343
+ - 히어로는 Surface padding="xl"로 내부 여백을 갖고, 바깥 폭·gutter는 Container가 소유한다. 시트 저장은 footer에 둔다(2026-10-06 지침과 예제 정합성 수정).
344
+ - 위·아래 CTA는 동일 행동이다. 긴 스크롤 구간의 접근성을 위한 반복 primary 예외이며 서로 다른 주 행동을 허용하지 않는다.
345
+ - 미리보기 행은 번역 문구나 배열 위치 대신 생성 시 부여한 id로 유지한다.
346
+ - 변형 예제의 저장은 메모리에만 남고 Sheet를 닫아도 입력 초안이 남는다. 운영 저장·실패 처리는 위 오류 행 규칙을 따른다.
347
+ - 현재 변형 스토리는 FAQ를 Collapsible이 아니라 `selected` 토글 Button 하나로 그린다. 질문이 여럿이면 기본처럼 Collapsible을 쓴다.
@@ -0,0 +1,190 @@
1
+ # 목업 편집
2
+
3
+ - 단계: 화면
4
+ - 상태: 배포
5
+ - 지원: Web
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: `showcase/web/src/foundations/MockupStudio.stories.tsx`, `src/responsive.ts`(`resolveWindowClass`), `showcase/web/src/studio/MockupStudio.tsx`, `showcase/web/src/studio/SceneTimeline.tsx`, `showcase/shared/mockup-scene.ts`, `showcase/shared/scene-timeline.ts`
9
+ - 스토리북: `배포/화면/화면 틀과 도구/목업 편집`
10
+
11
+ ## 목적
12
+
13
+ 제품 화면 캡처를 휴대폰·브라우저 프레임에 넣어 스토어·소개용 이미지(PNG)와 짧은 장면 영상을 만드는 Web 전용 작업 도구 화면이다.
14
+ `MockupStudio`는 showcase 안의 도구이며 `@hjmds/react`의 공개 API가 아니다 — 제품은 이 컴포넌트를 가져다 쓰지 않는다.
15
+ 소비자가 이 지침에서 가져갈 것은 두 가지다. (1) 도구 자체: Storybook에서 열어 제품 캡처로 목업을 만든다.
16
+ (2) 배치: "큰 미리보기 + 긴 설정 목록 + 마지막 내보내기 줄"로 된 편집기 화면을 HJM 컴포넌트로 짜는 예.
17
+ Native 스토리는 없다(장면 작성은 Web showcase에 둔다는 통합 계획, `component-stories.test.ts`).
18
+
19
+ ## 영역 구조
20
+
21
+ ```text
22
+ 한 열, 문서 스크롤(넓은 폭도 같음) — main > Container content(좌우 gutter: 폭<600 compact 16 · 이상 20) > Stack gap xl 24
23
+ 고정 영역 없음 · 안전 영역은 브라우저 · 키보드는 브라우저가 입력으로 스크롤
24
+ ┌──────────────────────────────────────────┐
25
+ │ ① 제목 Heading level3 · semanticLevel 1 │ Stack gap sm 12
26
+ │ + 설명 Text p │
27
+ │ ┌ ② 미리보기 Surface padding md 16 ─────┐ │
28
+ │ │ <canvas role="img"> 폭 100%, │ │ 최대 높이 70vh, contain
29
+ │ │ 출력 1080×1440 / 1080×1080 / 1440×900│ │
30
+ │ └──────────────────────────────────────┘ │
31
+ │ ③ "출력 1080 × 1440 · …" (muted) │
32
+ │ ④ 파일 입력 2개: 화면 캡처 / 장면 설정 불러오기│
33
+ │ ⑤ 상태 문구 (role=status) · 실패면 Notice danger │
34
+ │ ⑥ 장면 타임라인 (Stack gap md 16) │
35
+ │ 제목 Heading level3 · semanticLevel 2 │
36
+ │ Slider 재생 위치 / "0.00 / 3.00초 · 30fps"│
37
+ │ [재생] [처음으로] │ secondary · ghost
38
+ │ 움직임 [천천히 떠오르기][화면 등장] │ secondary + selected
39
+ │ 길이 [3초][6초][9초] [24fps][30fps] │ ghost + selected
40
+ │ [현재 장면을 포스터로] [포스터 PNG 저장] │ secondary
41
+ │ [영상 내보내기] ([출력 취소]) │ secondary · ghost(진행 중만)
42
+ │ 상태 문구 / 안내 │
43
+ │ ⑦ TextField 제목(≤80) · 설명(≤120) │
44
+ │ ⑧ 선택 묶음: 각 "라벨(strong) + 버튼 줄" │
45
+ │ 출력 비율 [세로][정사각형][가로] secondary│
46
+ │ 프레임 [휴대폰][브라우저] secondary│
47
+ │ 배경 [새벽][잉크][민트] secondary│
48
+ │ 각도 [-12°][-6°][0°][6°][12°] ghost │
49
+ │ 여백 [좁게][보통][넓게] ghost │
50
+ │ [그림자] ← secondary 토글 │
51
+ │ ⑨ TextField 화면 출처 · 허용 사용 범위 │
52
+ │ ⑩ [PNG 내보내기] [장면 설정 저장] [초기화] │ ← 주 행동 줄(맨 아래)
53
+ │ primary secondary ghost │
54
+ │ ⑪ 개인정보·출처 안내 (muted) │
55
+ └──────────────────────────────────────────┘
56
+ ```
57
+
58
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
59
+ | --- | --- | --- | --- |
60
+ | 바깥 틀 | `main` > `Container size="content" gutter={gutter}` > `Stack gap="xl"` | 화면 전체, 문서 스크롤. 고정 영역 없음 | 최대 폭 1200(`layout.contentMaxWidth`), 좌우 gutter 폭 600 미만 `compact` 16 · 이상 `regular` 20(`resolveWindowClass`), 직계 요소 사이 `spacing.xl` 24. 안전 영역·키보드는 브라우저가 맡는다(문서 스크롤) |
61
+ | ① 제목 | Stack `gap="sm"` > Heading `level="level3"` `semanticLevel={1}` · Text `as="p"` | 맨 위 | 사이 `spacing.sm` 12 |
62
+ | ② 미리보기 | Surface `padding="md"` > `canvas`(`role="img"`, `aria-label`=제목 + 프레임) | 제목 아래, 스크롤과 함께 움직임 | 안쪽 `spacing.md` 16, 폭 100%, 최대 높이 70vh |
63
+ | ③~⑤ 출력·파일·상태 | Text muted · `<input type="file">` ×2(제품은 FilePicker) · Text `role="status"`, 실패면 그 자리에 Notice `tone="danger"` | 미리보기 아래 | 사이 24 |
64
+ | ⑥ 타임라인 | Stack `gap="md"` > Heading `level="level3"` `semanticLevel={2}` · Slider · Button 줄들 | 상태 아래 | 사이 `spacing.md` 16, 버튼 줄 `Stack axis="inline" wrap gap="sm"` 12 |
65
+ | ⑦⑨ 텍스트 입력 | TextField ×4 | 타임라인 아래·선택 묶음 아래 | 높이 44(`control.fieldHeight`) |
66
+ | ⑧ 선택 묶음 | Text `emphasis="strong"` + Stack `axis="inline" wrap gap="sm"` > Button `selected` | 텍스트 입력 사이 | 라벨과 줄이 바깥 Stack 직계라 사이 24 |
67
+ | ⑩ 내보내기 줄 | Stack `axis="inline" wrap gap="sm"` > Button ×3 | 설정 목록 맨 아래 | 사이 12 |
68
+
69
+ ## 버튼과 행동 위치
70
+
71
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
72
+ | --- | --- | --- | --- |
73
+ | PNG 내보내기(주 행동) | Button 기본 primary | ⑩ 줄 첫째 | 1. 이미지 없음·불러오는 중이면 `disabled`, 내보내는 중이면 `loading` |
74
+ | 장면 설정 저장 | Button `secondary` | ⑩ 둘째 | 1. JSON 파일 내려받기 |
75
+ | 초기화 | Button `ghost` | ⑩ 셋째(끝) | 1. 확인 없이 모든 설정·이미지를 버린다 |
76
+ | 재생·일시 정지 | Button `secondary` | ⑥ 슬라이더 아래 첫째 | 1. 이미지 없음·모션 줄이기·영상 출력 중이면 `disabled` |
77
+ | 영상 내보내기 | Button `secondary` + 진행 중 `출력 취소` ghost | ⑥ 아래쪽 | 1(+1). 진행률을 라벨에 % 로 표시(`t("editor.videoProgress", { percent })`) |
78
+ | 실패 후 다시 시도 | Notice `action` > Button `tone="secondary" size="small"` | ⑤ 실패 Notice 안 | 내보내기 실패일 때만 1. 파일 읽기 실패는 다른 파일을 고르는 것이 복구라 버튼을 두지 않는다 |
79
+ | 설정 고르기 | Button `secondary`(형태·프레임·배경·움직임) · `ghost`(각도·여백·길이·fps) + `selected` | ⑥⑧ 각 줄 | 줄마다 2~5 |
80
+ | 파일 고르기 | `<input type="file">`(PNG·JPEG·WebP / JSON) | ④ | 2 |
81
+
82
+ 주 행동은 PNG 내보내기 하나다. 재생·영상 내보내기는 `secondary`로 둔다. 설정 버튼의 `selected`는 선택 표시라 primary 수에
83
+ 세지 않는다([Button](../components/button.md) "꼭 지킬 것"의 예외). 내보내기 줄은 맨 아래 흐름 배치라 긴 설정을 지나야 닿는다.
84
+ 제품 편집기에서 주 행동을 늘 보이게 하려면 [BottomCTA](../components/bottom-cta.md) `position="sticky"`로 옮긴다.
85
+
86
+ ## 상태
87
+
88
+ | 상태 | 화면 모습 | 행동 |
89
+ | --- | --- | --- |
90
+ | 기본 | 빈 프레임 미리보기, 상태 "스크린샷을 선택해 시작하세요.", PNG·재생·영상 비활성 | 화면 캡처 선택 |
91
+ | 로딩 | 상태 "스크린샷을 여는 중이에요.", PNG 비활성 | 기다림 |
92
+ | 빈 | 기본과 같다(이미지 없음). 장면 JSON만 불러오면 이미지가 비워지고 "파일을 다시 선택해 주세요" | 같은 캡처 다시 선택 |
93
+ | 오류 | 네트워크·서버 요청이 없다(브라우저 안에서만 처리). ⑤ 자리에 [Notice](../components/notice.md) `tone="danger"`. 이미지 읽기 실패(형식 PNG·JPEG·WebP 밖·20MB·16384px 초과): 이전 이미지·설정 유지. 장면 JSON 실패(64KB 초과·형식 오류): 현재 장면 유지. PNG·포스터 내보내기 실패: `action` 다시 시도, `loading` 해제. 영상: 인코더 미지원·실패면 PNG 대안 안내, 사용자가 취소하면 오류가 아니라 상태 문구. 다시 시도가 또 실패하면 같은 Notice가 같은 자리에 남는다 | 다른 파일 선택 / 다시 시도 / PNG 저장 |
94
+ | 불러옴 | 상태 "파일명 · 가로 × 세로 화면을 불러왔어요.", 미리보기에 그려짐 | 편집·내보내기 |
95
+ | 영상 출력 중 | 버튼 라벨 "영상 출력 N%"(보간 키), [출력 취소] 등장. 다른 탭으로 가면 취소 | 취소 |
96
+ | 모션 줄이기 | 재생 비활성, 안내 문구. 슬라이더로 정지 장면 확인 | 슬라이더 |
97
+
98
+ ## 사용하는 지침
99
+
100
+ | 지침 | 쓰는 곳 |
101
+ | --- | --- |
102
+ | [Stack](../components/stack.md) | 바깥 리듬, 버튼 줄 |
103
+ | [Surface](../components/surface.md) | ② 미리보기 틀 |
104
+ | [Heading](../components/heading.md) | ① 화면 제목, ⑥ 타임라인 제목 |
105
+ | [Text](../components/text.md) | 설명·라벨·상태·안내 |
106
+ | [Notice](../components/notice.md) | 읽기·내보내기 실패 |
107
+ | [Container](../components/container.md) | 바깥 폭·좌우 여백 |
108
+ | [Button](../components/button.md) | 모든 선택·실행 |
109
+ | [Field](../components/field.md) | 제목·설명·출처·사용 범위 `TextField` |
110
+ | [Slider](../components/slider.md) | 재생 위치 |
111
+ | [FilePicker](../components/file-picker.md) | 제품에서 `<input type="file">` 대신 |
112
+ | [BottomCTA](../components/bottom-cta.md) | 제품 편집기의 고정 내보내기 줄 |
113
+ | [화면 여백과 너비](../tokens/layout.md) | gutter·`layout.contentMaxWidth` |
114
+ | [간격](../tokens/spacing.md) | `spacing.sm`·`md`·`xl` |
115
+
116
+ ## 코드 골격
117
+
118
+ 도구를 다시 만들 필요는 없다. 아래는 같은 편집기 배치를 제품에서 짤 때의 뼈대다. 상태 문구는 상태→키 표로 고르고 파일 이름·크기는 보간 값으로 넘긴다.
119
+
120
+ ```tsx
121
+ // Web
122
+ import { Container, Stack, Surface, Text } from "@hjmds/react/layout";
123
+ import { Heading } from "@hjmds/react/heading";
124
+ import { Button } from "@hjmds/react/actions";
125
+ import { TextField } from "@hjmds/react/forms";
126
+ import { Notice } from "@hjmds/react/feedback";
127
+ import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
128
+
129
+ // 상태→키 표. 보간이 필요한 문구는 값으로 넘긴다.
130
+ const statusKey = {
131
+ idle: "editor.status.idle",
132
+ loading: "editor.status.loading",
133
+ loaded: "editor.status.loaded",
134
+ imageFailed: "editor.error.image",
135
+ sceneFailed: "editor.error.scene",
136
+ exported: "editor.status.exported",
137
+ exportFailed: "editor.error.export",
138
+ } as const;
139
+ const failed = status === "imageFailed" || status === "sceneFailed" || status === "exportFailed";
140
+ const gutter = resolveWindowClass(window.innerWidth) === "compact" ? "compact" : "regular";
141
+
142
+ <main><Container size="content" gutter={gutter}><Stack gap="xl">
143
+ <Stack gap="sm">
144
+ <Heading level="level3" semanticLevel={1}>{t("editor.title")}</Heading>
145
+ <Text as="p">{t("editor.intro")}</Text>
146
+ </Stack>
147
+ <Surface padding="md">{/* 미리보기: canvas/img, role="img", aria-label */}</Surface>
148
+ {failed ? <Notice tone="danger" title={t(statusKey[status])}
149
+ {...(status === "exportFailed" ? { action: <Button tone="secondary" size="small" onClick={exportPng}>{t("common.retry")}</Button> } : {})} />
150
+ : <Text role="status">{t(statusKey[status], file ? { name: file.name, width: file.width, height: file.height } : {})}</Text>}
151
+ <Stack gap="md">
152
+ <Heading level="level3" semanticLevel={2}>{t("editor.timeline")}</Heading>
153
+ <Stack axis="inline" wrap gap="sm">
154
+ <Button tone="secondary" disabled={!ready} onClick={togglePlay}>{playing ? t("editor.pause") : t("editor.play")}</Button>
155
+ </Stack>
156
+ </Stack>
157
+ <TextField label={t("editor.caption")} value={caption} maxLength={80} onValueChange={setCaption} />
158
+ <Text emphasis="strong">{t("editor.format")}</Text>
159
+ <Stack axis="inline" wrap gap="sm">
160
+ {formats.map(f => <Button key={f.id} tone="secondary" selected={format === f.id} onClick={() => setFormat(f.id)}>{t(f.labelKey)}</Button>)}
161
+ </Stack>
162
+ <Stack axis="inline" wrap gap="sm">
163
+ <Button loading={exporting} disabled={!ready} onClick={exportPng}>{t("editor.export")}</Button>
164
+ <Button tone="secondary" onClick={saveScene}>{t("editor.saveScene")}</Button>
165
+ <Button tone="ghost" onClick={reset}>{t("editor.reset")}</Button>
166
+ </Stack>
167
+ </Stack></Container></main>
168
+ ```
169
+
170
+ ```tsx
171
+ // Native
172
+ // 없음. 목업 편집은 Web showcase 전용이다.
173
+ ```
174
+
175
+ ## 큰 글자·다크·좁은 폭
176
+
177
+ | 조건 | 바뀌는 것 |
178
+ | --- | --- |
179
+ | 큰 글자(`textScale` 2) | 모든 버튼 줄이 감긴다. canvas 안 글자는 출력 이미지의 일부라 글자 배율을 따르지 않는다 |
180
+ | 다크 | 화면 UI는 테마 토큰. 출력 이미지 배경은 장면 설정(새벽·잉크·민트)이 정하고 테마와 무관하다 |
181
+ | 좁은 폭 | 한 열 그대로. 폭 600 미만이면 Container gutter `compact` 16. 미리보기는 폭 100%·최대 70vh로 줄고 파일 입력도 폭 100% |
182
+ | 넓은 폭 | 한 열 그대로, Container `content`로 최대 1200. 미리보기 옆에 설정을 두는 두 열 배치는 확인되지 않았다 |
183
+
184
+ ## 함정
185
+
186
+ - 화면 캡처는 브라우저 밖으로 나가지 않는다. 장면 JSON에는 파일 이름·출처·사용 범위만 저장되므로 다른 사람에게 JSON만 주면 이미지를 다시 골라야 한다.
187
+ - PNG는 화면에 보이는 canvas가 아니라 내보낼 때 새로 그린 canvas에서 만든다. 인코딩 중 설정을 바꿔도 결과가 바뀌지 않는다.
188
+ - 초기화는 확인 없이 지운다. 제품 편집기에서 되돌리기 어려운 초기화에는 [InlineConfirm](../components/button.md)이나 [AlertDialog](../components/alert-dialog.md)를 둔다.
189
+ - 구획 제목은 Heading으로 표시한다. 이전 Text heading 예제는 2026-10-06 제목 의미 구조를 맞추면서 수정했다.
190
+ - 현재 스토리는 읽기·내보내기 실패를 상태 Text 문구로만 알리고(Notice·다시 시도 없음), 상태 문구를 템플릿 문자열로 조립한다. PNG 내보내기 중에는 `loading` 대신 `disabled` + 라벨 교체를 쓴다.