@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,96 @@
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
+ ListDetailScreen을 사용해 목록과 상세 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
14
+
15
+ ## 영역 구조
16
+
17
+ ```text
18
+ host: 남은 높이·safe area·키보드
19
+ └─ 목록 헤더 → 목록 → 더 보기 / 상세 헤더 → 상세
20
+ ```
21
+
22
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
23
+ | --- | --- | --- | --- |
24
+ | 바깥 틀 | ListDetailScreen | route 본문 | [API 배치 규칙](../components/list-detail-screen.md#배치), host 남은 높이 |
25
+ | 내용 | 공개 슬롯 | 목록 헤더 → 목록 → 더 보기 / 상세 헤더 → 상세 | 화면 recipe의 sectionGap·itemGap; 슬롯 안은 각 지침 토큰 |
26
+ | 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
27
+
28
+ ## 버튼과 행동 위치
29
+
30
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
31
+ | --- | --- | --- | --- |
32
+ | 작업 | ListDetailScreen 공개 행동 슬롯 | 새로고침은 actions, 상세 뒤로는 leading | 같은 표면에 경쟁하는 primary 하나만 |
33
+ | 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
34
+
35
+ ## 상태
36
+
37
+ | 상태 | 화면 모습 | 행동 |
38
+ | --- | --- | --- |
39
+ | 기본 | 목록 헤더 → 목록 → 더 보기 / 상세 헤더 → 상세 | 각 공개 콜백을 제품 상태에 연결 |
40
+ | 로딩 | 최초 조회는 본문 상태, 저장은 해당 행동 pending | 중복 제출 차단; 성공을 먼저 표시하지 않음 |
41
+ | 빈 | 실제 조회 0건 또는 아직 작성하지 않은 상태 안내 | 시작·조건 해제 등 맥락에 맞는 대안 |
42
+ | 오류 | 목록 pane 유지; 상세 실패는 detail.content 안에서 복구 | 실패 원인과 재시도 경로 제공 |
43
+
44
+ ## 사용하는 지침
45
+
46
+ | 지침 | 쓰는 곳 |
47
+ | --- | --- |
48
+ | [ListDetailScreen](../components/list-detail-screen.md) | 필수 props·슬롯·플랫폼 차이 |
49
+ | [Button](../components/button.md) | 동작·로딩·보조 행동 |
50
+ | [ScreenLayout](../components/screen-layout.md) | 화면 높이·본문 교체·스크롤 소유 |
51
+
52
+ ## 코드 골격
53
+
54
+ ```tsx
55
+ // Web
56
+ import { List } from "@hjmds/react/display";
57
+ import { ListDetailScreen } from "@hjmds/react/screen-flows";
58
+
59
+ <ListDetailScreen
60
+ title={t("orders.title")}
61
+ list={<List label={t("orders.list")}>{rows}</List>}
62
+ {...(selected ? { detail: { title: selected.name, content: <OrderDetail order={selected} /> } } : {})}
63
+ back={{ label: t("common.back"), onAction: () => setSelected(null) }}
64
+ refresh={{ label: t("common.refresh"), onAction: refetch, pending: isRefetching }}
65
+ />
66
+ ```
67
+
68
+ ```tsx
69
+ // Native
70
+ import { List } from "@hjmds/react-native/data-display";
71
+ import { ListDetailScreen } from "@hjmds/react-native/screen-flows";
72
+
73
+ <ListDetailScreen
74
+ title={t("orders.title")}
75
+ list={<List label={t("orders.list")}>{rows}</List>}
76
+ {...(selected ? { detail: { title: selected.name, content: <OrderDetail order={selected} /> } } : {})}
77
+ back={{ label: t("common.back"), onAction: () => setSelected(null) }}
78
+ refresh={{ label: t("common.refresh"), onAction: refetch, pending: isRefetching }}
79
+ />
80
+ ```
81
+
82
+ 콜백·데이터·지역화 함수는 제품에서 공급한다. Web·Native import와 필수 props는 위 API 지침에서 확인한다.
83
+
84
+ ## 큰 글자·다크·좁은 폭
85
+
86
+ | 조건 | 바뀌는 것 |
87
+ | --- | --- |
88
+ | 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
89
+ | 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
90
+ | 좁은 폭 | 320px부터 한 열로 읽기 순서 유지. 가상화 본문은 scroll=content, 중첩 스크롤 금지 |
91
+ | 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
92
+
93
+ ## 함정
94
+
95
+ - 목록 pane 유지; 상세 실패는 detail.content 안에서 복구.
96
+ - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
@@ -0,0 +1,120 @@
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
+ EditorScreen을 사용해 작성과 수정 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
14
+
15
+ ## 영역 구조
16
+
17
+ ```text
18
+ host: 남은 높이·safe area·키보드
19
+ └─ 제목·취소 → 입력 → 저장
20
+ ```
21
+
22
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
23
+ | --- | --- | --- | --- |
24
+ | 바깥 틀 | EditorScreen | route 본문 | [API 배치 규칙](../components/editor-screen.md#배치), host 남은 높이 |
25
+ | 내용 | 공개 슬롯 | 제목·취소 → 입력 → 저장 | 화면 recipe의 sectionGap·itemGap; 슬롯 안은 각 지침 토큰 |
26
+ | 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
27
+
28
+ ## 버튼과 행동 위치
29
+
30
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
31
+ | --- | --- | --- | --- |
32
+ | 작업 | EditorScreen 공개 행동 슬롯 | 취소는 헤더; 저장은 footer; 수정한 채 이탈하면 확인 | 같은 표면에 경쟁하는 primary 하나만 |
33
+ | 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
34
+
35
+ ## 상태
36
+
37
+ | 상태 | 화면 모습 | 행동 |
38
+ | --- | --- | --- |
39
+ | 기본 | 제목·취소 → 입력 → 저장 | 각 공개 콜백을 제품 상태에 연결 |
40
+ | 로딩 | 최초 조회는 본문 상태, 저장은 해당 행동 pending | 중복 제출 차단; 성공을 먼저 표시하지 않음 |
41
+ | 빈 | 실제 조회 0건 또는 아직 작성하지 않은 상태 안내 | 시작·조건 해제 등 맥락에 맞는 대안 |
42
+ | 오류 | 저장 실패는 입력 유지; 저장 성공 확인 후에만 dirty 해제 | 실패 원인과 재시도 경로 제공 |
43
+
44
+ ## 사용하는 지침
45
+
46
+ | 지침 | 쓰는 곳 |
47
+ | --- | --- |
48
+ | [EditorScreen](../components/editor-screen.md) | 필수 props·슬롯·플랫폼 차이 |
49
+ | [Button](../components/button.md) | 동작·로딩·보조 행동 |
50
+ | [ScreenLayout](../components/screen-layout.md) | 화면 높이·본문 교체·스크롤 소유 |
51
+
52
+ ## 코드 골격
53
+
54
+ ```tsx
55
+ // Web
56
+ import { EditorScreen } from "@hjmds/react/screen-flows";
57
+ import { Text } from "@hjmds/react/layout";
58
+ import { TextField } from "@hjmds/react/forms";
59
+
60
+ <EditorScreen
61
+ title={t("post.edit.title")}
62
+ dirty={draft !== saved}
63
+ submit={{ label: t("post.edit.save"), pending: saving, disabled: !draft.trim(), onAction: save }}
64
+ cancel={{ label: t("post.edit.close"), onAction: close }}
65
+ discard={{
66
+ mode: "confirm",
67
+ title: t("post.discard.title"),
68
+ description: t("post.discard.description"),
69
+ confirmLabel: t("post.discard.confirm"),
70
+ cancelLabel: t("post.discard.keep"),
71
+ tone: "danger",
72
+ fallbackErrorMessage: t("common.error"),
73
+ }}
74
+ draftStatus={<Text variant="caption" tone="muted">{t("post.edit.draftSaved")}</Text>}
75
+ >
76
+ <TextField label={t("post.edit.body")} value={draft} onValueChange={setDraft} />
77
+ </EditorScreen>
78
+ ```
79
+
80
+ ```tsx
81
+ // Native
82
+ import { EditorScreen } from "@hjmds/react-native/screen-flows";
83
+ import { Text } from "@hjmds/react-native/primitives";
84
+ import { TextField } from "@hjmds/react-native/inputs";
85
+
86
+ <EditorScreen
87
+ title={t("post.edit.title")}
88
+ dirty={draft !== saved}
89
+ submit={{ label: t("post.edit.save"), pending: saving, disabled: !draft.trim(), onAction: save }}
90
+ cancel={{ label: t("post.edit.close"), onAction: close }}
91
+ discard={{
92
+ mode: "confirm",
93
+ title: t("post.discard.title"),
94
+ description: t("post.discard.description"),
95
+ confirmLabel: t("post.discard.confirm"),
96
+ cancelLabel: t("post.discard.keep"),
97
+ tone: "danger",
98
+ fallbackErrorMessage: t("common.error"),
99
+ }}
100
+ draftStatus={<Text variant="caption" tone="muted">{t("post.edit.draftSaved")}</Text>}
101
+ >
102
+ <TextField label={t("post.edit.body")} value={draft} onValueChange={setDraft} />
103
+ </EditorScreen>
104
+ ```
105
+
106
+ 콜백·데이터·지역화 함수는 제품에서 공급한다. Web·Native import와 필수 props는 위 API 지침에서 확인한다.
107
+
108
+ ## 큰 글자·다크·좁은 폭
109
+
110
+ | 조건 | 바뀌는 것 |
111
+ | --- | --- |
112
+ | 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
113
+ | 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
114
+ | 좁은 폭 | 320px부터 한 열로 읽기 순서 유지. 가상화 본문은 scroll=content, 중첩 스크롤 금지 |
115
+ | 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
116
+
117
+ ## 함정
118
+
119
+ - 저장 실패는 입력 유지; 저장 성공 확인 후에만 dirty 해제.
120
+ - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
@@ -0,0 +1,111 @@
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
+ MediaSelectionScreen을 사용해 사진 선택과 업로드 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
14
+
15
+ ## 영역 구조
16
+
17
+ ```text
18
+ host: 남은 높이·safe area·키보드
19
+ └─ 추가 → 썸네일 격자 → 선택 요약 → 완료
20
+ ```
21
+
22
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
23
+ | --- | --- | --- | --- |
24
+ | 바깥 틀 | MediaSelectionScreen | route 본문 | [API 배치 규칙](../components/media-selection-screen.md#배치), host 남은 높이 |
25
+ | 내용 | 공개 슬롯(`library`를 주면 기본 격자 대신 그것을 그린다) | 추가(헤더) → 썸네일 격자 → 선택 요약 → 완료(footer) | 기본 격자는 `Grid` 2열, `breakpoint.expanded` 960 이상 3열, 칸 사이 `spacing.md` 16. 칸 안은 미리보기 → UploadItem → 이동·제거 행(`spacing.xs` 8 세로, 버튼 사이 `spacing.xxs` 4) |
26
+ | 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
27
+
28
+ ## 버튼과 행동 위치
29
+
30
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
31
+ | --- | --- | --- | --- |
32
+ | 작업 | `add` Button·secondary / `done` Button·primary / 항목 행동 Button·ghost·small | 추가는 헤더 actions, 완료는 footer(선택 요약 아래), 위로·아래로·제거는 각 칸 미리보기 아래 | primary는 완료 하나. 칸마다 위로 → 아래로 → 제거, 첫 칸의 위로·마지막 칸의 아래로는 비활성 |
33
+ | 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
34
+
35
+ ## 상태
36
+
37
+ | 상태 | 화면 모습 | 행동 |
38
+ | --- | --- | --- |
39
+ | 기본 | 추가 → 썸네일 격자 → 선택 요약 → 완료 | 각 공개 콜백을 제품 상태에 연결 |
40
+ | 로딩 | 최초 조회는 본문 상태, 저장은 해당 행동 pending | 중복 제출 차단; 성공을 먼저 표시하지 않음 |
41
+ | 빈 | 실제 조회 0건 또는 아직 작성하지 않은 상태 안내 | 시작·조건 해제 등 맥락에 맞는 대안 |
42
+ | 오류 | 업로드 실패 항목만 재시도, 완료 버튼은 제품 정책으로 제한 | 실패 원인과 재시도 경로 제공 |
43
+
44
+ ## 사용하는 지침
45
+
46
+ | 지침 | 쓰는 곳 |
47
+ | --- | --- |
48
+ | [MediaSelectionScreen](../components/media-selection-screen.md) | 필수 props·슬롯·플랫폼 차이 |
49
+ | [Button](../components/button.md) | 동작·로딩·보조 행동 |
50
+ | [ScreenLayout](../components/screen-layout.md) | 화면 높이·본문 교체·스크롤 소유 |
51
+
52
+ ## 코드 골격
53
+
54
+ ```tsx
55
+ // Web
56
+ import { MediaSelectionScreen } from "@hjmds/react/screen-flows";
57
+
58
+ <MediaSelectionScreen
59
+ title={t("media.title")}
60
+ items={picked.map((p) => ({ descriptor: { id: p.id, name: p.fileName, state: p.upload },
61
+ preview: <img src={p.uri} alt="" width={p.width} height={p.height} /> }))}
62
+ add={{ label: t("media.add"), onAction: openPicker }}
63
+ done={{ label: t("media.done"), onAction: submit, disabled: uploading }}
64
+ labels={{ pending: t("upload.pending"), uploading: t("upload.uploading"),
65
+ success: t("upload.success"), cancel: t("upload.cancel"), retry: t("upload.retry") }}
66
+ actionLabels={{ remove: t("media.remove"), moveUp: t("media.moveUp"), moveDown: t("media.moveDown") }}
67
+ removeLabel={(item) => t("media.removeNamed", { name: item.descriptor.name })}
68
+ moveUpLabel={(item) => t("media.moveUpNamed", { name: item.descriptor.name })}
69
+ moveDownLabel={(item) => t("media.moveDownNamed", { name: item.descriptor.name })}
70
+ onRemove={remove} onMove={(id, direction) => move(id, direction)}
71
+ onRetry={retryUpload} onCancel={cancelUpload}
72
+ />
73
+ ```
74
+
75
+ ```tsx
76
+ // Native
77
+ import { MediaSelectionScreen } from "@hjmds/react-native/screen-flows";
78
+ import { Image } from "react-native";
79
+
80
+ <MediaSelectionScreen
81
+ title={t("media.title")}
82
+ items={picked.map((p) => ({ descriptor: { id: p.id, name: p.fileName, state: p.upload },
83
+ preview: <Image src={p.uri} width={p.width} height={p.height} /> }))}
84
+ add={{ label: t("media.add"), onAction: openPicker }}
85
+ done={{ label: t("media.done"), onAction: submit, disabled: uploading }}
86
+ labels={{ pending: t("upload.pending"), uploading: t("upload.uploading"),
87
+ success: t("upload.success"), cancel: t("upload.cancel"), retry: t("upload.retry") }}
88
+ actionLabels={{ remove: t("media.remove"), moveUp: t("media.moveUp"), moveDown: t("media.moveDown") }}
89
+ removeLabel={(item) => t("media.removeNamed", { name: item.descriptor.name })}
90
+ moveUpLabel={(item) => t("media.moveUpNamed", { name: item.descriptor.name })}
91
+ moveDownLabel={(item) => t("media.moveDownNamed", { name: item.descriptor.name })}
92
+ onRemove={remove} onMove={(id, direction) => move(id, direction)}
93
+ onRetry={retryUpload} onCancel={cancelUpload}
94
+ />
95
+ ```
96
+
97
+ 콜백·데이터·지역화 함수는 제품에서 공급한다. Web·Native import와 필수 props는 위 API 지침에서 확인한다. 미리보기 이미지는 제품이 공급한다(Web `img`, Native `Image`).
98
+
99
+ ## 큰 글자·다크·좁은 폭
100
+
101
+ | 조건 | 바뀌는 것 |
102
+ | --- | --- |
103
+ | 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
104
+ | 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
105
+ | 좁은 폭 | 기본 격자는 `minColumnWidth` 없이 `columns={{ compact: 2, expanded: 3 }}`라 320px에서도 2열이다(한 열로 접지 않는다). 960 미만 2열, 이상 3열. 이동·제거 버튼 행은 줄바꿈된다. 한 열이 필요하면 `library` 슬롯으로 제품 격자를 넘긴다 |
106
+ | 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
107
+
108
+ ## 함정
109
+
110
+ - 업로드 실패 항목만 재시도, 완료 버튼은 제품 정책으로 제한.
111
+ - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
@@ -0,0 +1,120 @@
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
+ ModerationScreen을 사용해 신고와 차단 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
14
+
15
+ ## 영역 구조
16
+
17
+ ```text
18
+ host: 남은 높이·safe area·키보드
19
+ └─ 사유 선택 → 추가 설명 → 신고·차단
20
+ ```
21
+
22
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
23
+ | --- | --- | --- | --- |
24
+ | 바깥 틀 | ModerationScreen | route 본문 | [API 배치 규칙](../components/moderation-screen.md#배치), host 남은 높이 |
25
+ | 내용 | 공개 슬롯 | 사유 선택 → 추가 설명 → 신고·차단 | 화면 recipe의 sectionGap·itemGap; 슬롯 안은 각 지침 토큰 |
26
+ | 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
27
+
28
+ ## 버튼과 행동 위치
29
+
30
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
31
+ | --- | --- | --- | --- |
32
+ | 작업 | ModerationScreen 공개 행동 슬롯 | 신고는 footer; 차단은 별도 확인 후 실행 | 같은 표면에 경쟁하는 primary 하나만 |
33
+ | 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
34
+
35
+ ## 상태
36
+
37
+ | 상태 | 화면 모습 | 행동 |
38
+ | --- | --- | --- |
39
+ | 기본 | 사유 선택 → 추가 설명 → 신고·차단 | 각 공개 콜백을 제품 상태에 연결 |
40
+ | 로딩 | 최초 조회는 본문 상태, 저장은 해당 행동 pending | 중복 제출 차단; 성공을 먼저 표시하지 않음 |
41
+ | 빈 | 실제 조회 0건 또는 아직 작성하지 않은 상태 안내 | 시작·조건 해제 등 맥락에 맞는 대안 |
42
+ | 오류 | 신고 실패에 선택 사유·설명 유지; 차단은 성공 후 확정 | 실패 원인과 재시도 경로 제공 |
43
+
44
+ ## 사용하는 지침
45
+
46
+ | 지침 | 쓰는 곳 |
47
+ | --- | --- |
48
+ | [ModerationScreen](../components/moderation-screen.md) | 필수 props·슬롯·플랫폼 차이 |
49
+ | [Button](../components/button.md) | 동작·로딩·보조 행동 |
50
+ | [ScreenLayout](../components/screen-layout.md) | 화면 높이·본문 교체·스크롤 소유 |
51
+
52
+ ## 코드 골격
53
+
54
+ ```tsx
55
+ // Web
56
+ import { ModerationScreen } from "@hjmds/react/screen-flows";
57
+
58
+ <ModerationScreen
59
+ title={t("report.title")}
60
+ reasonLabel={t("report.reason")}
61
+ reasons={[
62
+ { value: "spam", label: t("report.reason.spam") },
63
+ { value: "abuse", label: t("report.reason.abuse") },
64
+ ]}
65
+ reason={reason}
66
+ onReasonChange={setReason}
67
+ submit={{ label: t("report.submit"), onAction: submitReport, pending: reporting }}
68
+ block={{
69
+ action: { label: t("report.block"), onAction: () => {} },
70
+ confirmation: {
71
+ mode: "confirm", tone: "danger",
72
+ title: t("block.confirm.title"), description: t("block.confirm.body"),
73
+ confirmLabel: t("block.confirm"), cancelLabel: t("common.cancel"),
74
+ onConfirm: blockUser, fallbackErrorMessage: t("block.error"),
75
+ },
76
+ }}
77
+ />
78
+ ```
79
+
80
+ ```tsx
81
+ // Native
82
+ import { ModerationScreen } from "@hjmds/react-native/screen-flows";
83
+
84
+ <ModerationScreen
85
+ title={t("report.title")}
86
+ reasonLabel={t("report.reason")}
87
+ reasons={[
88
+ { value: "spam", label: t("report.reason.spam") },
89
+ { value: "abuse", label: t("report.reason.abuse") },
90
+ ]}
91
+ reason={reason}
92
+ onReasonChange={setReason}
93
+ submit={{ label: t("report.submit"), onAction: submitReport, pending: reporting }}
94
+ block={{
95
+ action: { label: t("report.block"), onAction: () => {} },
96
+ confirmation: {
97
+ mode: "confirm", tone: "danger",
98
+ title: t("block.confirm.title"), description: t("block.confirm.body"),
99
+ confirmLabel: t("block.confirm"), cancelLabel: t("common.cancel"),
100
+ onConfirm: blockUser, fallbackErrorMessage: t("block.error"),
101
+ },
102
+ }}
103
+ />
104
+ ```
105
+
106
+ 콜백·데이터·지역화 함수는 제품에서 공급한다. Web·Native import와 필수 props는 위 API 지침에서 확인한다.
107
+
108
+ ## 큰 글자·다크·좁은 폭
109
+
110
+ | 조건 | 바뀌는 것 |
111
+ | --- | --- |
112
+ | 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
113
+ | 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
114
+ | 좁은 폭 | 320px부터 한 열로 읽기 순서 유지. 가상화 본문은 scroll=content, 중첩 스크롤 금지 |
115
+ | 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
116
+
117
+ ## 함정
118
+
119
+ - 신고 실패에 선택 사유·설명 유지; 차단은 성공 후 확정.
120
+ - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
@@ -0,0 +1,193 @@
1
+ # 온보딩
2
+
3
+ - 단계: 화면
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screen-flows.tsx`(`OnboardingScreen`), 예제 `showcase/*/screen-flow-previews.tsx`(`OnboardingFlowPreview`)·`showcase/shared/onboarding-pattern.ts`(주제·요약). 2026-10-06 사용자 승인으로 실험 `기본 흐름/온보딩`(OnboardingScreen)을 배포하면서 같은 일을 직접 조립하던 배포 `화면/온보딩`을 대체했다(Web id `patterns-onboarding` 보존, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
+ - 스토리북: `배포/화면/소개/온보딩`
10
+
11
+ ## 목적
12
+
13
+ 첫 실행 사용자를 몇 단계(소개 → 관심 주제 → 시작)로 안내하고 마지막 단계에서 완료를 저장하는 화면을 OnboardingScreen 하나로 구성한다.
14
+ 단계 제목·설명은 화면 머리, 진행 문구는 머리 아래, 이동 버튼은 footer에 고정하고 단계 본문만 바꾼다. 권한 요청·로그인·가입은
15
+ 이 화면에 없다([권한 안내](flow-permission.md), [로그인](common-login.md)).
16
+ 스토리는 `기본`(1단계부터 직접 넘기기), `관심 주제 고르기`(2단계를 바로 연 상태), `실패와 복구`(다음 완료 저장 실패 → 다시 시작하기)다.
17
+ 2026-10-06 배포 직접 조립 온보딩(Steps·ContentTransition·여러 개 고르는 주제 버튼)을 이 항목으로 합쳤고, 그 고유 상태인
18
+ 관심 주제 여러 개 고르기는 OnboardingScreen 2단계의 Chip 다중 선택으로 옮겼다(두 플랫폼 `관심 주제 고르기` 스토리가 같은 화면을 연다).
19
+ 단계·주제·문구·저장은 제품이 공급한다.
20
+
21
+ ## 영역 구조
22
+
23
+ ```text
24
+ 좁은 폭(Native·모바일 Web) — host가 남은 높이·safe area·키보드를 준다
25
+ ┌ OnboardingScreen = ScreenLayout(최대 720, 바깥 padding spacing.md 16) ┐
26
+ │ 머리(고정) │
27
+ │ 단계 제목 (title) [건너뛰기] ← skip, ghost │
28
+ │ 단계 설명 (description) │
29
+ │ 진행 문구(고정 notice): "2 / 3" Text caption muted │
30
+ ├──────────────────────────── 본문 스크롤 ──────────────────────────────┤
31
+ │ 단계 content(제품) │
32
+ │ 1단계: 소개 그림 + 짧은 설명 │
33
+ │ 2단계: 관심 주제 [일상] [여행] [독서] [아이디어] ← Chip multiple, wrap │
34
+ │ 3단계: 고른 주제 요약 │
35
+ │ (완료 저장 실패면) Notice danger + 다시 시도 │
36
+ ├──────────────────────────── footer(고정, 위 테두리 1) ─────────────────┤
37
+ │ [ 다음 / 시작하기 ] ← primary, 마지막 단계는 complete │
38
+ │ [ 이전 ] ← ghost, 첫 단계에는 없음 │ 사이 spacing.sm 12
39
+ └ 아래 safe area(Web은 footer padding이 늘어난다) ──────────────────────┘
40
+ ```
41
+
42
+ 넓은 폭 Web도 한 열이다. ScreenLayout이 `layout.readingMaxWidth` 720으로 폭을 묶고 가운데 둔다.
43
+
44
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
45
+ | --- | --- | --- | --- |
46
+ | 바깥 틀 | OnboardingScreen(내부 ScreenLayout `scroll="screen"`) | route 본문. host가 남은 높이·safe area·키보드를 준다 | 폭 최대 720, 바깥 padding `spacing.md` 16([ScreenLayout 배치](../components/screen-layout.md#배치)). 화면 props는 `layoutStyle`만 받는다 |
47
+ | 머리 | `steps[index].title`·`description` + `skip`(ghost Button) | 맨 위, 고정 | 제목 열 최소 120 × 글자 배율, 모자라면 건너뛰기가 다음 줄로 내려간다 |
48
+ | 진행 문구 | `progressLabel(current, total)` → Text `variant="caption" tone="muted"` | 머리 아래 notice 자리, 고정 | 좌우 16 |
49
+ | 단계 본문 | `steps[index].content` | 진행 문구 아래, 본문 스크롤 | 본문 안 간격은 제품 소유. 예제는 Stack `gap="lg"` 20 |
50
+ | 관심 주제 | Stack `axis="inline" wrap gap="xs"`(Web `role="group"` + 이름) > [Chip](../components/chip.md) `selectionMode="multiple"` | 2단계 content 안 | 칩 높이 `small` 36(Native hitSlop으로 터치 44), 사이 `spacing.xs` 8 |
51
+ | 저장 실패 | [Notice](../components/notice.md) `tone="danger"` + `action` | 마지막 단계 content 맨 아래 | 본문 Stack 간격을 따른다 |
52
+ | footer | Stack `gap="sm"` > Button primary(다음·완료) → Button ghost(이전) | 맨 아래, 고정 | 사이 `spacing.sm` 12, 위 테두리 1 |
53
+
54
+ ## 버튼과 행동 위치
55
+
56
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
57
+ | --- | --- | --- | --- |
58
+ | 다음 | `nextLabel` → Button primary | footer 첫째 | 마지막 단계 전까지 1 |
59
+ | 완료(시작하기) | `complete` → Button primary, `pending`이면 `loading`·비활성 | footer 첫째(마지막 단계) | 1. 저장 중 중복 실행을 막는다 |
60
+ | 이전 | `backLabel` → Button ghost | footer 둘째 | 첫 단계에는 없다(숨김) |
61
+ | 건너뛰기 | `skip` → Button ghost | 머리 끝(actions) | 선택. 1 |
62
+ | 관심 주제 고르기 | Chip `selectionMode="multiple"`(checkbox 역할) | 2단계 본문 | 주제 수만큼, 여러 개 선택. 선택 상태는 제품이 든다 |
63
+ | 다시 시도 | 실패 Notice `action` > Button `tone="secondary" size="small"` | 마지막 단계 본문 | 실패일 때만 1. footer의 완료를 다시 눌러도 같다 |
64
+ | 파괴 행동 | — | — | 없음 |
65
+
66
+ 한 단계에 primary는 footer의 다음·완료 하나다. 진행·이동이 footer에 고정돼 단계 본문이 바뀌어도 버튼 자리가 흔들리지 않는다.
67
+
68
+ 2026-10-06 관심 주제를 ghost+`selected` Button에서 Chip `multiple`로 바꿨다. 여러 개 고르는 선택은 checkbox 역할로 읽혀야 하는데
69
+ Button `selected`는 눌림 토글(`aria-pressed`)로 읽히고, 한 줄짜리 주제 선택에 CheckboxGroup은 세로 목록이라 화면을 많이 차지한다.
70
+
71
+ ## 상태
72
+
73
+ | 상태 | 화면 모습 | 행동 |
74
+ | --- | --- | --- |
75
+ | 기본 | 1단계: 소개 그림·설명, 진행 "1 / 3", footer에 다음만(이전 없음), 머리 끝 건너뛰기 | 다음 · 건너뛰기 |
76
+ | 관심 주제 고르기 | `관심 주제 고르기` 스토리: 2단계. 주제 칩을 눌러 켜고 끈다(여러 개, 처음엔 아무것도 고르지 않음). 고른 것이 없어도 다음으로 갈 수 있다 | 주제 토글 · 다음 · 이전 · 건너뛰기 |
77
+ | 로딩 | 주제 목록을 서버에서 받으면 받는 동안 주제 줄 자리에 [Skeleton](../components/skeleton.md). 완료 저장 중에는 `complete.pending`으로 완료 버튼 `loading` | 기다림 |
78
+ | 빈 | 주제를 하나도 고르지 않고 마지막 단계에 오면 요약 자리에 "관심 주제를 아직 선택하지 않았어요" 안내 | 이전 또는 시작하기 |
79
+ | 오류 | 완료 저장 실패: 마지막 단계에 머물고 고른 주제는 그대로 둔다. 마지막 단계 본문에 Notice `danger`(Native `announcement="assertive"` — 기본 `none`). 다시 실패하면 같은 자리 Notice를 유지한다(쌓지 않는다) | 다시 시도 1 |
80
+ | 실패와 복구 | `실패와 복구` 스토리: 다음 저장 실패를 예약한 뒤 시작하기 → 실패 문구 → 다시 시작하기로 완료 | 같은 완료 행동 |
81
+ | 완료 | 제품이 온보딩을 닫고 홈으로 보낸다. 예제는 "시작할 준비가 됐어요" 화면과 다시 열기 | — |
82
+
83
+ ## 사용하는 지침
84
+
85
+ | 지침 | 쓰는 곳 |
86
+ | --- | --- |
87
+ | [OnboardingScreen](../components/onboarding-screen.md) | 필수 props·단계 범위·플랫폼 차이 |
88
+ | [ScreenLayout](../components/screen-layout.md) | 머리·notice·footer 고정, 본문 스크롤, 최대 폭 |
89
+ | [Button](../components/button.md) | 다음·완료·이전·건너뛰기, 다시 시도 |
90
+ | [Chip](../components/chip.md) | 관심 주제 여러 개 고르기 |
91
+ | [Notice](../components/notice.md) | 완료 저장 실패 |
92
+ | [Skeleton](../components/skeleton.md) | 주제 목록 로딩 |
93
+ | [Stack](../components/stack.md) | 단계 본문 세로 리듬, 주제 줄 |
94
+ | [Steps](../components/steps.md) | 단계 표시를 본문에 따로 그려야 할 때(OnboardingScreen은 진행 문구만 준다) |
95
+ | [권한 안내](flow-permission.md) | 온보딩 뒤 권한을 묻는 화면 |
96
+
97
+ ## 코드 골격
98
+
99
+ 단계·주제·문구는 제품 소유다. 진행 문구는 보간 키 하나로 만들고 키 문자열을 조립하지 않는다.
100
+
101
+ ```tsx
102
+ // Web
103
+ import { OnboardingScreen } from "@hjmds/react/screen-flows";
104
+ import { Stack } from "@hjmds/react/layout";
105
+ import { Button } from "@hjmds/react/actions";
106
+ import { Chip } from "@hjmds/react/selection";
107
+ import { Notice } from "@hjmds/react/feedback";
108
+
109
+ const topicPicker = <Stack role="group" aria-label={t("onboarding.topics.label")} axis="inline" wrap gap="xs">
110
+ {topics.map((topic) => <Chip key={topic.id} label={t(topic.labelKey)} selectionMode="multiple"
111
+ selected={chosen.includes(topic.id)} onSelectedChange={() => toggleTopic(topic.id)} />)}
112
+ </Stack>;
113
+
114
+ <OnboardingScreen
115
+ steps={[
116
+ { id: "welcome", title: t("onboarding.welcome.title"), description: t("onboarding.welcome.body"), content: welcomeArt },
117
+ { id: "topics", title: t("onboarding.topics.title"), description: t("onboarding.topics.body"), content: topicPicker },
118
+ { id: "ready", title: t("onboarding.ready.title"), description: t("onboarding.ready.body"), content: <Stack gap="lg">
119
+ {summary}
120
+ {failed ? <Notice tone="danger" title={t("onboarding.error")}
121
+ action={<Button tone="secondary" size="small" onClick={finish}>{t("common.retry")}</Button>} /> : null}
122
+ </Stack> },
123
+ ]}
124
+ index={index}
125
+ onIndexChange={setIndex}
126
+ nextLabel={t("common.next")}
127
+ backLabel={t("common.back")}
128
+ complete={{ label: t("onboarding.start"), onAction: finish, pending: saving }}
129
+ skip={{ label: t("common.skip"), onAction: finish }}
130
+ progressLabel={(current, total) => t("onboarding.progress", { current, total })}
131
+ />
132
+ ```
133
+
134
+ ```tsx
135
+ // Native
136
+ import { OnboardingScreen } from "@hjmds/react-native/screen-flows";
137
+ import { Stack } from "@hjmds/react-native/primitives";
138
+ import { Button } from "@hjmds/react-native/actions";
139
+ import { Chip } from "@hjmds/react-native/inputs";
140
+ import { Notice } from "@hjmds/react-native/feedback";
141
+
142
+ const topicPicker = <Stack axis="inline" wrap gap="xs">
143
+ {topics.map((topic) => <Chip key={topic.id} label={t(topic.labelKey)} selectionMode="multiple"
144
+ selected={chosen.includes(topic.id)} onPress={() => toggleTopic(topic.id)} />)}
145
+ </Stack>;
146
+
147
+ <OnboardingScreen
148
+ steps={[
149
+ { id: "welcome", title: t("onboarding.welcome.title"), description: t("onboarding.welcome.body"), content: welcomeArt },
150
+ { id: "topics", title: t("onboarding.topics.title"), description: t("onboarding.topics.body"), content: topicPicker },
151
+ { id: "ready", title: t("onboarding.ready.title"), description: t("onboarding.ready.body"), content: <Stack gap="lg">
152
+ {summary}
153
+ {failed ? <Notice tone="danger" announcement="assertive" title={t("onboarding.error")}
154
+ action={<Button tone="secondary" size="small" onPress={finish}>{t("common.retry")}</Button>} /> : null}
155
+ </Stack> },
156
+ ]}
157
+ index={index}
158
+ onIndexChange={setIndex}
159
+ nextLabel={t("common.next")}
160
+ backLabel={t("common.back")}
161
+ complete={{ label: t("onboarding.start"), onAction: finish, pending: saving }}
162
+ skip={{ label: t("common.skip"), onAction: finish }}
163
+ progressLabel={(current, total) => t("onboarding.progress", { current, total })}
164
+ />
165
+ ```
166
+
167
+ `finish`는 고른 주제와 완료 여부를 저장하고 성공하면 온보딩을 닫는다. 실패하면 `failed`를 켜고 `index`는 그대로 둔다.
168
+
169
+ ## 큰 글자·다크·좁은 폭
170
+
171
+ | 조건 | 바뀌는 것 |
172
+ | --- | --- |
173
+ | 큰 글자(`textScale` 2) | 단계 제목이 줄바꿈되고 건너뛰기가 제목 아래 줄로 내려간다. 주제 줄은 여러 줄로 감긴다. footer는 고정이라 본문만 스크롤된다 |
174
+ | 다크 | semantic token만 쓰므로 따로 처리하지 않는다. 소개 그림·브랜드 이미지는 제품이 다크용을 준비한다 |
175
+ | 좁은 폭 | 320부터 한 열, 바깥 padding 16. footer 버튼은 세로로 쌓인다 |
176
+ | 넓은 폭 Web | 한 열 유지, 최대 720 가운데 |
177
+ | 키보드 | 단계 본문에 입력을 두면 Native host가 safe area와 키보드를 한 번 처리한다. Web은 포커스된 입력이 footer에 가리지 않는지 확인 |
178
+
179
+ ## 플랫폼 차이
180
+
181
+ | 항목 | Web | Native |
182
+ | --- | --- | --- |
183
+ | 이동 | `onClick` | `onPress` |
184
+ | 저장 실패 발표 | Notice `danger`가 `role="alert"`로 읽힌다 | Notice `announcement="assertive"`를 줘야 읽힌다 |
185
+ | 주제 칩 선택 콜백 | `onSelectedChange(next)` | `onPress(next, event)` |
186
+ | 주제 묶음 이름 | Stack `role="group"` + `aria-label` | 칩마다 `label`로 읽힌다(묶음 역할 없음) |
187
+
188
+ ## 함정
189
+
190
+ - `notice` 자리는 진행 문구가 차지한다. 저장 실패를 띄우려고 OnboardingScreen에 notice를 넘길 수 없으니 마지막 단계 `content`에 둔다.
191
+ - 완료 `pending` 동안에도 이전 버튼은 막히지 않는다. 저장 중 단계 이동이 문제가 되면 `onIndexChange`에서 무시한다.
192
+ - 현재 스토리의 진행 문구는 `` `${index} / ${total}` `` 고정 문자열이다. 제품은 보간 키 하나(`onboarding.progress`)를 쓴다.
193
+ - Storybook은 실제 서버·라우터 연동 증거가 아니다. 기본·어두운 테마·큰 글자와 실패와 복구를 각각 확인한다.