@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,185 @@
1
+ # 시각 효과 모음
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [EffectSurface](../../effect-surface.md), [Avatar fallback](../../avatar-fallback.md), [Icon](../../icon.md), `showcase/shared/visual-foundations.ts`, `showcase/{web/src/patterns,native/src}/visual-previews.tsx`, `src/container.ts`, `packages/react/src/composition-style.ts`
9
+ - 스토리북: `배포/구성/비교와 검증/시각 효과 모음`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 배경 질감, 의미 이름 아이콘, 사진 없는 프로필 얼굴, 문장 전환처럼 화면의 분위기를 더하는 선택 표현을 고를 때 이 모음을 본다.
14
+ 네 스토리(배경 효과·의미별 루시드 아이콘·블로바타 표정·내용 전환 효과)가 각 표현의 선택지를 나란히 비교한다.
15
+
16
+ ## 구성 요소
17
+
18
+ | 컴포넌트 | 역할 | 지침 |
19
+ | --- | --- | --- |
20
+ | EffectSurface | `mesh`·`glow`·`grain` 배경 층 위에 내용을 둔다 | [EffectSurface](../components/effect-surface.md) |
21
+ | Icon + `createLucideGlyph` | 의미 이름(`search`·`settings`·`notifications`·`back`)을 Lucide 그림에 연결 | [Icon](../components/icon.md) |
22
+ | Avatar + `createBlobatarFallback` | 사진이 없을 때 seed로 정해지는 얼굴, `expression: "happy"` | [Avatar](../components/avatar.md) |
23
+ | TextTransition | 문장이 바뀔 때 `fade`·`rise`·`slide`·`scale` 전환 | [TextTransition](../components/text-transition.md) |
24
+ | Heading | 효과 표면 안 제목, 얼굴 묶음 제목(`level2`) | [Heading](../components/heading.md) |
25
+ | Button | 효과 켜기(`selected`)·다음 장면 | [Button](../components/button.md) |
26
+ | `ScrollView` + `Container`(Native) | 바깥 틀. 세로 스크롤·위아래 여백, 좌우 gutter | [Container](../components/container.md), [화면 여백과 너비](../tokens/layout.md) |
27
+
28
+ 비교 결과로 고르는 기준이다.
29
+
30
+ | 비교 | 선택지 | 고르는 기준 |
31
+ | --- | --- | --- |
32
+ | 배경 효과 | `mesh` / `glow` / `grain` / 셋 모두, `active` 켬·끔 | 히어로·빈 화면 배경 하나에만. 위 글자는 일반 대비 tone으로 둔다 |
33
+ | 의미별 아이콘 | 의미 이름 → 제품 아이콘 세트 | 컴포넌트에는 이름만 넘기고 그림은 제품이 `renderGlyph`로 연결 |
34
+ | 블로바타 표정 | 기본 / `happy` | 사진이 없을 때만. seed는 공개 제품 식별자 |
35
+ | 내용 전환 | `fade`(기본) / `rise` 12 / `slide` 16 / `scale` 0.96 | 같은 자리의 문장 교체는 `fade`, 단계가 넘어가면 `slide` |
36
+
37
+ ## 배치
38
+
39
+ ```text
40
+ Native 화면(ScrollView, 위아래 spacing.lg 20) > Container gutter 16(폭 < 600) · 20(폭 ≥ 600)
41
+ 배경 효과 (Stack gap spacing.xl 24)
42
+ [ 배경 움직임 ] ← selected 토글
43
+ ┌──── EffectSurface (mesh) ────────────────────┐
44
+ │ 안쪽 여백 spacing.xl 24(두 플랫폼 같음) │
45
+ │ mesh (label) │
46
+ │ 작은 시작, 새로운 장면. (Heading level2) ↕ lg 20 │
47
+ │ 설명 (body) │
48
+ │ [ 이야기 시작하기 ] secondary │
49
+ └──────────────────────────────────────────────┘
50
+ ┌──── EffectSurface (glow) ─── ... ────────────┐
51
+ ┌──── EffectSurface (grain) ── ... ────────────┐
52
+ ┌──── EffectSurface (mesh+glow+grain) ─────────┐
53
+
54
+ 아이콘 / 얼굴 (행마다 가로, gap spacing.md 16)
55
+ (🔍) search (☺)(☻) 이름
56
+ 고정 영역 없음. 안전 영역은 화면 골격이 맡는다
57
+ ```
58
+
59
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
60
+ | --- | --- | --- | --- |
61
+ | 바깥 틀 | Web: 제품 화면 레이아웃(문서 스크롤) > Stack. Native: `ScrollView` > `Container` > Stack | 이 구성은 바깥 폭·여백을 정하지 않는다. Native는 `ScrollView` 안 [Container](../components/container.md)에 둔다. 입력이 없어 키보드 처리는 없다. 안전 영역은 화면 골격(내비게이션 헤더·탭 바)이 맡는다 | Native ScrollView 위아래 `spacing.lg` 20, Container gutter 폭 600 미만 `compact` 16 · 이상 `regular` 20. 묶음 사이 Stack `gap="xl"` 24 |
62
+ | 효과 표면 | EffectSurface > 안쪽 틀 > Stack `gap="lg"` | 세로로 쌓음(비교용. 실제 화면은 히어로 하나) | EffectSurface는 여백이 없다. 안쪽 틀 여백 `spacing.xl` 24(두 플랫폼 같음, [랜딩](../screens/landing.md) 히어로와 같다), 내용 사이 `spacing.lg` 20, `intensity` 0.4(예) |
63
+ | 아이콘 행 | Icon `size="lg"` + Text | 가로(Stack `axis="inline"`) | 아이콘 `lg` 28, 사이 `spacing.md` 16 |
64
+ | 얼굴 행 | Avatar ×2 + Text | 가로(Stack `axis="inline"`) | 두 플랫폼 48(Web `size="large"`, Native `size={48}`), 사이 `spacing.md` 16 |
65
+ | 전환 | Text label + TextTransition | 세로 | 사이 `spacing.md` 16, 시간 `motion.normal` |
66
+
67
+ ## 흐름과 상태
68
+
69
+ 1. "배경 움직임"을 켜면 모든 EffectSurface `active`가 켜져 천천히 움직인다. 다시 누르면 멈춘다.
70
+ 2. "다음 장면"을 누르면 네 preset의 문장이 동시에 다음 문장으로 바뀐다.
71
+
72
+ - 장면 문구 키는 순서 있는 상수 배열로 둔다(`sceneKeys[index]`). 번호로 키를 조립하지 않는다.
73
+
74
+ | 상태 | 모습 | 포커스·알림 |
75
+ | --- | --- | --- |
76
+ | 기본 | 효과 정지(`active` 기본 `false`) | 효과 층은 장식 |
77
+ | 진행 중 | — (요청이 없다). 문장 전환 중에는 이전 문장이 나가고 새 문장이 들어온다 | — |
78
+ | 실패 | — (요청이 없다). 프로필 사진이 없거나 로드에 실패하면 Avatar가 `renderFallback`(Blobatar)으로 바꾼다 | Avatar 이름이 읽힌다 |
79
+ | 움직임 켬 | 층이 `period` 주기(기본 12초)로 움직인다 | 토글 버튼 `selected` 상태로 알린다 |
80
+ | reduced motion | 효과·전환 모두 즉시·정지 | — |
81
+ | 사진 없음 | Blobatar 얼굴, 같은 seed는 같은 얼굴 | 그림은 장식, Avatar 이름이 읽힌다 |
82
+ | 다크 | 효과 색이 `primary`·`contentBrand`·`surfaceAccent` 테마 값을 따른다 | — |
83
+
84
+ ## 코드 골격
85
+
86
+ ```tsx
87
+ // Web
88
+ import { Search } from "lucide-react";
89
+ import { Button } from "@hjmds/react/actions";
90
+ import { TextTransition } from "@hjmds/react/content-transition";
91
+ import { Avatar, Icon } from "@hjmds/react/display";
92
+ import { createBlobatarFallback } from "@hjmds/react/avatar-blobatar";
93
+ import { EffectSurface } from "@hjmds/react/effect-surface";
94
+ import { Heading } from "@hjmds/react/heading";
95
+ import { createLucideGlyph } from "@hjmds/react/icon-lucide";
96
+ import { Stack, Text } from "@hjmds/react/layout";
97
+
98
+ const glyph = createLucideGlyph({ search: Search });
99
+ const sceneKeys = ["intro.scene.welcome", "intro.scene.collect", "intro.scene.ready"] as const;
100
+
101
+ <Stack gap="xl">
102
+ <EffectSurface descriptor={{ layers: ["mesh", "glow"], active, seed: "home-hero" }}>
103
+ {/* EffectSurface는 여백이 없다. 안쪽 틀: .hero-inner { padding: var(--hjm-space-xl) } */}
104
+ <div className="hero-inner">
105
+ <Stack gap="lg">
106
+ <Heading level="level2">{t("hero.title")}</Heading>
107
+ <Text>{t("hero.body")}</Text>
108
+ <Button tone="secondary" onClick={start}>{t("hero.start")}</Button>
109
+ </Stack>
110
+ </div>
111
+ </EffectSurface>
112
+ <Stack axis="inline" gap="md">
113
+ <Icon name="search" size="lg" renderGlyph={glyph} />
114
+ <Text>{t("icons.search")}</Text>
115
+ </Stack>
116
+ <Avatar name={user.name} size="large" renderFallback={createBlobatarFallback({ seed: user.publicId })} />
117
+ <TextTransition preset="fade" text={t(sceneKeys[index])} />
118
+ </Stack>;
119
+ ```
120
+
121
+ ```tsx
122
+ // Native
123
+ import { ScrollView, View, useWindowDimensions } from "react-native";
124
+ import { Search } from "lucide-react-native";
125
+ import { spacing } from "@hjmds/design-contracts/foundations";
126
+ import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
127
+ import { Button } from "@hjmds/react-native/actions";
128
+ import { TextTransition } from "@hjmds/react-native/content-transition";
129
+ import { Avatar } from "@hjmds/react-native/data-display";
130
+ import { createBlobatarFallback } from "@hjmds/react-native/avatar-blobatar";
131
+ import { EffectSurface } from "@hjmds/react-native/effect-surface";
132
+ import { Heading } from "@hjmds/react-native/heading";
133
+ import { createLucideGlyph } from "@hjmds/react-native/icon-lucide";
134
+ import { Container, Icon, Stack, Text } from "@hjmds/react-native/primitives";
135
+
136
+ const glyph = createLucideGlyph({ search: Search });
137
+ const sceneKeys = ["intro.scene.welcome", "intro.scene.collect", "intro.scene.ready"] as const;
138
+ const { width } = useWindowDimensions();
139
+ const gutter = resolveWindowClass(width) === "compact" ? "compact" : "regular";
140
+
141
+ <ScrollView contentContainerStyle={{ paddingVertical: spacing.lg }}>
142
+ <Container gutter={gutter}>
143
+ <Stack gap="xl">
144
+ <EffectSurface descriptor={{ layers: ["mesh", "glow"], active, seed: "home-hero" }} visible={isFocused}>
145
+ {/* EffectSurface는 여백이 없다. 안쪽 틀만 View padding으로 준다 */}
146
+ <View style={{ padding: spacing.xl }}>
147
+ <Stack gap="lg">
148
+ <Heading level="level2">{t("hero.title")}</Heading>
149
+ <Text>{t("hero.body")}</Text>
150
+ <Button tone="secondary" onPress={start}>{t("hero.start")}</Button>
151
+ </Stack>
152
+ </View>
153
+ </EffectSurface>
154
+ <Stack axis="inline" gap="md">
155
+ <Icon descriptor={{ name: "search", size: "lg" }} renderGlyph={glyph} />
156
+ <Text>{t("icons.search")}</Text>
157
+ </Stack>
158
+ <Avatar name={user.name} accessibilityLabel={user.name} size={48}
159
+ renderFallback={createBlobatarFallback({ seed: user.publicId })} />
160
+ <TextTransition preset="fade" text={t(sceneKeys[index])} />
161
+ </Stack>
162
+ </Container>
163
+ </ScrollView>;
164
+ ```
165
+
166
+ seed·문구·아이콘 세트·얼굴 목록은 제품 소유다. `createBlobatarFallback`은 optional peer `blobatar@2.7.0`이 필요하다.
167
+
168
+ ## 플랫폼 차이
169
+
170
+ | 항목 | Web | Native |
171
+ | --- | --- | --- |
172
+ | 효과 표면 안쪽 틀 | `div` + CSS `padding: var(--hjm-space-xl)`(`layoutStyle`은 padding을 받지 않는다) | `View style={{ padding: spacing.xl }}` |
173
+ | Avatar 크기 | 이름 단계 `large` 48 | 숫자 pt `48` |
174
+ | 바깥 틀 | 제품 화면 레이아웃(문서 스크롤) | `ScrollView` > `Container` |
175
+ | 가려짐 알림 | 없음 | EffectSurface `visible`(화면 focus) |
176
+ | Icon 크기 지정 | `size` prop | `descriptor.size` |
177
+ | 표정 접근성 이름 | `name`에 넣음 | `accessibilityLabel` |
178
+
179
+ ## 함정
180
+
181
+ - 움직이는 glow 위의 작은 글자를 브랜드 색으로 두면 대비가 4.5:1 아래로 떨어진다. 일반 본문 tone을 쓴다.
182
+ - Blobatar seed에 이름·이메일을 쓰지 않는다. 공개 제품 식별자를 쓴다.
183
+ - EffectSurface는 화면 위쪽 히어로 하나에만 둔다. 스토리의 네 장 세로 쌓기는 비교용이다.
184
+ - 2026-10-06 예제 검수에서 Container·Stack·Heading을 두 플랫폼에 맞췄다. 효과 안쪽 여백은 spacing.xxl(32), 얼굴은 64, 제목은 level2로 동일하다. 행은 큰 글자에서 줄바꿈한다.
185
+ - Native EffectSurface는 AppState·reducedMotion을 반영한다. 백그라운드 중단과 내비게이션으로 가려진 화면의 중단은 다르므로, 마운트된 채 가려지는 제품 화면·목록은 `visible`을 연결한다. Storybook 정지 화면만으로 이 제품 연결을 검증했다고 보고하지 않는다.
@@ -0,0 +1,146 @@
1
+ # 웹 전용 보조 컴포넌트
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [ColorPicker](../../color-picker.md), [Watermark](../../watermark.md), [Affix](../../affix.md), `showcase/web/src/patterns/WebAdditions.stories.tsx`, `packages/react/src/affix.tsx`, `packages/react/src/styles.css`(`.hjm-color-picker`, `.hjm-watermark`, `.hjm-affix`), `src/affix.ts`, `src/color-picker.ts`
9
+ - 스토리북: `배포/구성/비교와 검증/웹 전용 보조 컴포넌트`
10
+
11
+ ## 언제 쓰나
12
+
13
+ Web에만 있는 보조 컴포넌트 세 개(색 고르기, 문서 워터마크, 스크롤 중 고정되는 실행 영역)를 실제 쓰임 하나씩과 함께 보여 주는 모음이다.
14
+ 스토리 셋은 서로 이어진 흐름이 아니다. 무엇을 고를지는 아래 기준으로 정한다.
15
+
16
+ | 필요 | 고를 것 | 스토리 |
17
+ | --- | --- | --- |
18
+ | 사용자가 강조 색 같은 **사용자 데이터 색**을 고른다 | `ColorPicker` | 색상 선택 |
19
+ | 검토용·대외비 문서임을 본문 위에 표시하되 읽기·선택·버튼은 막지 않는다 | `Watermark` | 문서 표시 |
20
+ | 긴 스크롤 영역에서 저장 같은 실행 버튼을 위쪽에 붙여 둔다 | `Affix` | 고정 실행 영역 |
21
+
22
+ Native 화면에서 같은 일이 필요하면 이 구성을 옮기지 않는다. 하단 고정 행동은 [BottomCTA](../components/bottom-cta.md)를 쓴다.
23
+
24
+ ## 구성 요소
25
+
26
+ | 컴포넌트 | 역할 | 지침 |
27
+ | --- | --- | --- |
28
+ | `ColorPicker` | 네이티브 색 입력 + HEX 입력 + 불투명도(`alpha`) + 프리셋 견본. controlled | [ColorPicker](../components/color-picker.md) |
29
+ | 선택값 문구 `Text` `role="status"` | 바뀐 HEX를 알림. 값은 `<bdi dir="ltr">`로 감싼다 | [Text](../components/text.md) |
30
+ | `Watermark` | 본문 위 장식 SVG 타일. 포인터·선택을 가로채지 않는다 | [Watermark](../components/watermark.md) |
31
+ | `Affix` | 가장 가까운 스크롤 조상 상단에 `position: sticky`로 붙는다 | [Affix](../components/affix.md) |
32
+ | `Heading` | 워터마크 안 문서 제목 | [Heading](../components/heading.md) |
33
+ | `Button` | 워터마크 위 저장, Affix 안 저장. 저장 중 `loading` | [Button](../components/button.md) |
34
+ | `Notice` | 저장 실패 안내(스토리에 없음) | [Notice](../components/notice.md) |
35
+
36
+ ## 배치
37
+
38
+ ```text
39
+ 색상 선택 문서 표시
40
+ ┌ fieldset ──────────────────────┐ ┌ Watermark ──────────────────────┐
41
+ │ 강조 색상 (legend) │ │ ╱HJM╱ 검토 중인 문서 ╱HJM╱ │ ← 타일 240×160, -22°, 0.12
42
+ │ [■] [ HEX #b94627cc ] │ │ 본문 문단 (선택 가능) │
43
+ │ 불투명도 ───────●────── │ │ [ 문서 저장 ] │ ← 오버레이 위에서도 눌림
44
+ │ [ 미리보기 막대 ] │ └─────────────────────────────────┘
45
+ │ [■ 프리셋][■ 프리셋][■ 프리셋] │
46
+ └────────────────────────────────┘
47
+ 선택한 색상: #b94627cc ← live
48
+
49
+ 고정 실행 영역(스크롤 조상 안)
50
+ ┌ 스크롤 영역 ───────────────────┐
51
+ │ ↑ offset spacing.xs 8 │
52
+ │ [ 변경 저장 ] 상단 고정 중 │ ← Affix: 스크롤하면 이 줄이 위에 붙는다
53
+ │ … 긴 본문(스크롤) … │
54
+ └────────────────────────────────┘
55
+ 바깥 틀: 제품 페이지(문서 스크롤, Container). 안전 영역·키보드 처리 없음
56
+ ```
57
+
58
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
59
+ | --- | --- | --- | --- |
60
+ | 바깥 틀 | 제품 페이지 레이아웃(문서 스크롤) | 세 스토리 모두 바깥 폭·여백을 정하지 않는다. 페이지 본문은 [Container](../components/container.md)가 정한다. Affix만 가장 가까운 스크롤 조상(문서 또는 제품의 `overflow: auto` 영역)에 붙는다. Web 전용이라 안전 영역·화면 키보드 처리는 없다 | Container gutter 폭 600 미만 `compact` 16 · 이상 `regular` 20 |
61
+ | 색 입력 줄 | `ColorPicker` 색 견본 + HEX | fieldset 첫 줄, 좁으면 줄바꿈(`flex-wrap`) | 입력 최소 높이 `colorPickerRecipe.minTargetSize` 44, 색 견본 폭 3rem, 줄 간격 0.75rem |
62
+ | 불투명도·미리보기·프리셋 | `ColorPicker` 내부 | 색 입력 아래 순서대로 | 각 위 여백 0.75rem, 프리셋 사이 0.5rem, 프리셋 최소 높이 44 |
63
+ | 선택값 | live 문구 | ColorPicker 바로 아래 | 제품 레이아웃 소유 |
64
+ | 워터마크 | `Watermark` | 감싼 본문 전체를 덮는 오버레이(`inset: 0`) | `tileWidth` 240 · `tileHeight` 160 · `rotate` -22 · `opacity` 0.12(기본값), 색 `--hjm-color-text-sub` |
65
+ | 고정 행동 | `Affix` | 스크롤 조상 상단 + `offset` | `offset` 기본 0, 띄울 때는 spacing 토큰 값(스토리 `spacing.xs` 8). 스크롤 조상보다 높으면 고정하지 않고 흐름에 남는다 |
66
+ | 저장 실패 | `Notice` danger | 저장 버튼 위(Watermark 본문 안, Affix 아래 본문 첫머리) | 버튼과 `spacing.md` 16 |
67
+
68
+ - ColorPicker 내부 간격은 CSS가 rem 값으로 직접 둔다(`padding: 1rem`, `gap: 0.75rem`·`0.5rem`). 제품에서 덮지 않는다.
69
+ - Affix는 스크롤 조상 안에서만 붙는다. 그 조상의 높이·overflow는 제품 레이아웃이 정한다(스토리의 280px 박스는 데모 값).
70
+
71
+ ## 흐름과 상태
72
+
73
+ 1. 색상 선택: 색 견본·HEX·프리셋 중 하나로 고른다 → `onValueChange`가 소문자 `#rrggbb`(또는 `alpha`면 `#rrggbbaa`)를 준다 → 문구가 갱신된다.
74
+ 2. HEX 입력은 Enter·blur에 확정되고 Escape는 마지막 `value`로 되돌린다.
75
+ 3. 문서 표시: 워터마크 위에서 본문을 선택하고 버튼을 누른다. 워터마크는 상태가 없다.
76
+ 4. 고정 실행 영역: 스크롤하면 Affix가 상단에 붙고 `onChange(true)`, 다시 올리면 `onChange(false)`가 온다.
77
+ 5. 저장: 버튼이 `loading`이 되고, 실패하면 Notice danger가 나오며 버튼을 다시 누를 수 있다. 성공하면 Notice를 내린다.
78
+
79
+ | 상태 | 모습 | 포커스·알림 |
80
+ | --- | --- | --- |
81
+ | 기본 | ColorPicker는 `value` 색, 워터마크 타일, Affix는 일반 위치(`data-affixed` 없음) | — |
82
+ | 진행 중 | 저장 버튼 `loading`(Watermark·Affix 안). ColorPicker는 요청이 없다 | 포커스는 버튼에 남는다 |
83
+ | 실패 | 저장 실패 Notice danger, 버튼 다시 활성. Affix 고정 중이면 버튼은 그대로 위에 붙어 있다 | Notice `danger`는 `role="alert"` |
84
+ | HEX 잘못 입력 | `labels.invalid` 문구가 오류로 보이고 외부 값은 그대로 | 오류 문구는 alert |
85
+ | 프리셋 선택됨 | 선택한 견본 테두리가 primary 색, `aria-pressed="true"` | — |
86
+ | `disabled` | fieldset 전체 흐림(opacity 0.5), 입력·프리셋 막힘 | 포커스 불가 |
87
+ | Affix 고정 중 | `data-affixed="true"`, 배경 `--hjm-color-bg` | 고정돼도 버튼 상태·키보드 포커스 유지(자식 재마운트 없음) |
88
+ | Affix 너무 큼 | `data-oversize="true"`, 고정하지 않고 일반 흐름 | — |
89
+
90
+ ## 코드 골격
91
+
92
+ ```tsx
93
+ // Web
94
+ import { ColorPicker } from "@hjmds/react/color-picker";
95
+ import { Watermark } from "@hjmds/react/watermark";
96
+ import { Affix } from "@hjmds/react/affix";
97
+ import { Button } from "@hjmds/react/actions";
98
+ import { Notice } from "@hjmds/react/feedback";
99
+ import { Heading } from "@hjmds/react/heading";
100
+ import { Stack, Text } from "@hjmds/react/layout";
101
+
102
+ <Stack gap="md">
103
+ <ColorPicker label={t("theme.accent")} value={color} onValueChange={setColor} alpha
104
+ presets={productPresets}
105
+ labels={{ color: t("theme.color"), hex: t("theme.hex"), opacity: t("theme.opacity"), invalid: t("theme.invalidHex") }} />
106
+ <Text role="status">{t("theme.selected")} <bdi dir="ltr">{color}</bdi></Text>
107
+ </Stack>;
108
+
109
+ <Watermark text={[t("doc.watermark.brand"), t("doc.watermark.review")]}>
110
+ <Stack gap="md">
111
+ <Heading level="level3">{t("doc.title")}</Heading>
112
+ {documentBody}
113
+ {saveFailed ? <Notice tone="danger" title={t("doc.saveFailed")} /> : null}
114
+ <Button loading={saving} onClick={save}>{t("doc.save")}</Button>
115
+ </Stack>
116
+ </Watermark>;
117
+
118
+ <div className="scroll-region" /* 제품 소유 스크롤 조상 */>
119
+ <Affix offset={8 /* spacing.xs */} onChange={setPinned}>
120
+ <Button loading={saving} onClick={save}>{t("doc.saveChanges")}</Button>
121
+ </Affix>
122
+ {longContent}
123
+ </div>;
124
+ ```
125
+
126
+ ```tsx
127
+ // Native
128
+ // 없음. 세 컴포넌트 모두 Native 구현이 없다.
129
+ ```
130
+
131
+ 스토리의 초기 색 `#b94627cc`와 프리셋 세 색은 예시 데이터다. 프리셋은 제품 소유이며 브랜드 색을 넣더라도 사용자 데이터 값으로만 다룬다.
132
+
133
+ ## 플랫폼 차이
134
+
135
+ | 항목 | Web | Native |
136
+ | --- | --- | --- |
137
+ | `ColorPicker` | `/color-picker` | 없음 |
138
+ | `Watermark` | `/watermark` | 없음 |
139
+ | `Affix` | `/affix`(root entry에 없음) | 없음. 하단 고정은 [BottomCTA](../components/bottom-cta.md) |
140
+
141
+ ## 함정
142
+
143
+ - 세 컴포넌트 모두 root barrel에 없다. granular subpath(`@hjmds/react/color-picker` 등)로만 import한다.
144
+ - Affix `offset`이 음수이거나 유한하지 않으면 `TypeError`를 던진다.
145
+ - Watermark 범위(`tileWidth` ≥ 80, `tileHeight` ≥ 60, `rotate` -90~90, `opacity` 0~1, 줄 1~3개·각 120자 이하)를 벗어나면 `TypeError`다.
146
+ - 현재 스토리의 저장은 라벨만 "저장됨"으로 바꾸는 토글이다. 진행 중·실패 경로가 없다.
@@ -0,0 +1,143 @@
1
+ # 보관함과 페이지 이동
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Breadcrumb](../../breadcrumb.md), [Pagination](../../pagination.md), `showcase/web/src/patterns/WebNavigation.stories.tsx`, `packages/react/src/styles.css`(`.hjm-breadcrumb`, `.hjm-pagination`), `src/component-recipes.ts`(`listRowRecipe`)
9
+ - 스토리북: `배포/구성/탐색과 이동/보관함과 페이지 이동`
10
+
11
+ ## 언제 쓰나
12
+
13
+ Web에서 상위 보관함 → 하위 모음으로 들어가고, 그 모음의 긴 목록을 페이지 단위로 넘겨 보는 탐색에 쓴다.
14
+ 위에 경로(Breadcrumb), 가운데 목록(List), 아래 페이지 이동(Pagination)을 둔다. 무한 스크롤·더 보기가 맞으면
15
+ [LoadMore](../components/load-more.md)를, 앱 화면 전환이면 Native 내비게이션을 쓴다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | `Breadcrumb` | 현재 위치까지의 경로. 마지막 항목만 현재 위치(링크 없음) | [Breadcrumb](../components/breadcrumb.md) |
22
+ | `Section` | 모음 제목·설명("산책 기록 125개") | [Section](../components/section.md) |
23
+ | `Link` | 상위 화면에서 하위 모음으로 들어가는 링크 | [Link](../components/link.md) |
24
+ | 범위 문구 `Text` `role="status"` | "1–5번째 기록". 숫자 범위는 `<bdi>`로 감싼다 | [Text](../components/text.md) |
25
+ | `List` + `ListRow` | 현재 페이지 항목 | [List](../components/list.md), [ListRow](../components/list-row.md) |
26
+ | `Pagination` | 이전·번호·다음. `descriptor={{ currentPage, totalCount, pageSize }}` | [Pagination](../components/pagination.md) |
27
+ | `Stack` | 바깥 `gap="lg"`, 모음 안 `gap="md"` | [Stack](../components/stack.md) |
28
+ | `Skeleton` · `Notice` | 서버에서 페이지를 받을 때 목록 자리의 로딩·실패(스토리에 없음) | [Skeleton](../components/skeleton.md), [Notice](../components/notice.md) |
29
+
30
+ ## 배치
31
+
32
+ ```text
33
+ 페이지 본문(문서 스크롤) > Container gutter 16(폭 < 600) · 20(폭 ≥ 600)
34
+ ┌ 콘텐츠(스크롤) ────────────────────────────────┐
35
+ │ 전체 보관함 › 산책 기록 ← Breadcrumb │
36
+ │ ↕ spacing.lg 20 │
37
+ │ ┌ 콘텐츠 영역 (tabIndex=-1, 포커스 대상) ────┐ │
38
+ │ │ 걸었던 날을 모아봤어요 (Section 제목) │ │
39
+ │ │ 산책 기록 125개 (설명) │ │
40
+ │ │ ↕ spacing.md 16 │ │
41
+ │ │ 1–5번째 기록 ← status │ │
42
+ │ │ ↕ spacing.md 16 │ │
43
+ │ │ ┌ 1번째 산책 기록 / 설명 ───────────────┐ │ │ ListRow 두 줄 최소 68
44
+ │ │ ├ 2번째 산책 기록 / 설명 ───────────────┤ │ │
45
+ │ │ └ … (pageSize 5) ───────────────────────┘ │ │
46
+ │ │ ↕ spacing.md 16 │ │
47
+ │ │ [‹] [1] [2] [3] … [25] [›] ← Pagination │ │ 칸 최소 44×44
48
+ │ └────────────────────────────────────────────┘ │
49
+ └─────────────────────────────────────────────────┘
50
+ 고정 영역 없음. Web 전용이라 안전 영역·화면 키보드 처리는 없다
51
+ ```
52
+
53
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
54
+ | --- | --- | --- | --- |
55
+ | 바깥 틀 | 제품 페이지 레이아웃(문서 스크롤) > [Container](../components/container.md) | 이 구성은 바깥 폭·여백을 정하지 않는다(스토리는 Stack만 그린다). 목록 화면이므로 페이지 본문은 Container `size="content"`가 정한다. 고정 영역·안전 영역·키보드 처리는 없다 | Container gutter 폭 600 미만 `compact` 16 · 이상 `regular` 20 |
56
+ | 경로 | `Breadcrumb` | 콘텐츠 맨 위, 스크롤과 함께 | 항목·구분자 사이 `spacing.xxs` 4, 좁으면 줄바꿈 |
57
+ | 콘텐츠 머리 | `Section` 제목·설명 | 경로 아래 | 경로와 `spacing.lg` 20 |
58
+ | 범위 | status 문구 | 머리 아래, 목록 위 | `spacing.md` 16 |
59
+ | 목록 | `List` + `ListRow` | 범위 아래 | 두 줄 행 최소 `layout.rowHeight.twoLine` 68(comfortable), 사이 `spacing.md` 16 |
60
+ | 로딩·실패 | `Skeleton` 행 / `Notice` danger + 다시 시도 | 목록 자리(범위 문구·Pagination은 그대로) | 목록과 같은 자리 |
61
+ | 페이지 이동 | `Pagination` | 목록 바로 아래, 시작 정렬 | 칸 최소 `control.minTouchTarget` 44, 칸 사이 `spacing.xxs` 4, 좁으면 줄바꿈 |
62
+
63
+ - Pagination은 목록 **아래**에 둔다. 위·아래 두 벌을 두지 않는다.
64
+ - 상위 화면(보관함)은 같은 콘텐츠 영역에 Section + Link 하나만 그린다. 경로는 항목 하나(현재 위치)로 줄어든다.
65
+
66
+ ## 흐름과 상태
67
+
68
+ 1. 보관함에서 "산책 기록 125개 보기" Link를 누른다.
69
+ 2. 경로가 "전체 보관함 › 산책 기록"으로 바뀌고 포커스가 새 콘텐츠 영역으로 옮겨진다.
70
+ 3. Pagination으로 페이지를 바꾸면 목록과 범위 문구만 바뀐다. 서버에서 받으면 그동안 목록 자리에 Skeleton이 나온다.
71
+ 4. 페이지를 받지 못하면 목록 자리에 Notice와 "다시 시도"가 나오고 Pagination은 요청한 페이지에 남는다.
72
+ 5. 경로의 "전체 보관함"을 누르면 상위로 돌아가고 페이지는 1로 초기화된다.
73
+
74
+ | 상태 | 모습 | 포커스·알림 |
75
+ | --- | --- | --- |
76
+ | 기본 | 상위(보관함): Section + Link, 경로는 현재 항목 하나 | 첫 진입에는 포커스를 옮기지 않는다 |
77
+ | 진행 중 | 서버에서 받는 페이지면 목록 자리에 `Skeleton` 행, Pagination은 새 페이지 표시. 늦게 온 이전 페이지 응답은 버린다 | 포커스는 Pagination에 남는다 |
78
+ | 실패 | 목록 자리에 `Notice` danger + `action` "다시 시도"(같은 페이지 재요청), 범위 문구는 비운다 | Notice `danger`는 `role="alert"` |
79
+ | 모음 진입 | 경로 두 단계, 목록 1페이지 | 누른 링크가 사라지므로 콘텐츠 영역(`tabIndex={-1}`)에 `focus({ preventScroll: true })` |
80
+ | 페이지 변경 | 목록 5개 교체, 범위 문구 갱신 | 포커스는 Pagination에 남고 범위 문구(`role="status"`)가 알린다 |
81
+ | 첫·마지막 페이지 | 이전/다음 칸이 `aria-disabled`, opacity 0.5 | — |
82
+
83
+ - 문구 키는 상태별 상수로 둔다(`records.range`·`records.loadFailed`·`common.retry`). 현재 페이지 이름은 `current`에 따라 두 키(`records.pageCurrent`·`records.pageGo`) 중 하나를 고른다. 범위 문구는 숫자 → 문구 순서를 코드에 고정한다(스토리와 같다). 언어마다 순서가 다르면 제품 i18n의 rich text 기능으로 `<bdi>`를 끼운다.
84
+
85
+ ## 코드 골격
86
+
87
+ ```tsx
88
+ // Web
89
+ import { Breadcrumb } from "@hjmds/react/breadcrumb";
90
+ import { Pagination } from "@hjmds/react/pagination";
91
+ import { Button } from "@hjmds/react/actions";
92
+ import { List, ListRow } from "@hjmds/react/display";
93
+ import { Notice, Skeleton } from "@hjmds/react/feedback";
94
+ import { Section, Stack, Text } from "@hjmds/react/layout";
95
+
96
+ <Stack gap="lg">
97
+ <Breadcrumb label={t("records.path")} items={[
98
+ { id: "records", label: t("records.all"), destination: { kind: "internal", href: "/records" } },
99
+ { id: "walks", label: t("records.walks") },
100
+ ]} />
101
+ <div ref={contentRef} tabIndex={-1}>
102
+ <Section title={t("records.walks.title")} description={t("records.walks.count", { count: total })}>
103
+ <Stack gap="md">
104
+ <Text role="status">{status === "ready" ? <><bdi>{from}–{to}</bdi>{t("records.range")}</> : ""}</Text>
105
+ {status === "loading"
106
+ ? <Stack gap="xs"><Skeleton shape="text" /><Skeleton shape="text" width="60%" /></Stack>
107
+ : status === "failed"
108
+ ? <Notice tone="danger" title={t("records.loadFailed")}
109
+ action={<Button tone="secondary" size="small" onClick={retry}>{t("common.retry")}</Button>} />
110
+ : <List label={t("records.walks.list")}>
111
+ {rows.map((row) => <ListRow key={row.id} title={row.title} description={row.summary} />)}
112
+ </List>}
113
+ <Pagination label={t("records.pages")}
114
+ descriptor={{ currentPage: page, totalCount: total, pageSize: 5 }}
115
+ labels={{ previous: t("records.prev"), next: t("records.next") }}
116
+ composeAccessibleName={({ page, totalPages, current }) => t(current ? "records.pageCurrent" : "records.pageGo", { page, totalPages })}
117
+ onPageChange={setPage} />
118
+ </Stack>
119
+ </Section>
120
+ </div>
121
+ </Stack>;
122
+ ```
123
+
124
+ ```tsx
125
+ // Native
126
+ // 없음. Breadcrumb·Pagination은 Web 전용이다.
127
+ ```
128
+
129
+ 라우팅(스토리는 데모용 hash fragment), 레코드 125개, pageSize 5는 예시다. 경로·페이지 크기·URL 동기화는 제품 소유다.
130
+
131
+ ## 플랫폼 차이
132
+
133
+ | 항목 | Web | Native |
134
+ | --- | --- | --- |
135
+ | `Breadcrumb` | `/breadcrumb`, `/navigation` | 없음 |
136
+ | `Pagination` | `/pagination`, `/navigation` | 없음. 긴 목록은 [LoadMore](../components/load-more.md) |
137
+
138
+ ## 함정
139
+
140
+ - Breadcrumb 마지막 항목에 `destination`을 주거나, 앞 항목에서 빠뜨리면 `TypeError`다.
141
+ - 범위 숫자를 `<bdi>`로 감싸지 않으면 RTL에서 "5–1"처럼 뒤집혀 읽힌다.
142
+ - 탐색 후 포커스를 옮기지 않으면 누른 링크가 사라져 포커스가 `body`로 떨어진다. 첫 마운트·페이지 변경에서는 옮기지 않는다.
143
+ - 현재 스토리는 로컬 배열을 잘라 쓰므로 로딩·실패 경로가 없다.
@@ -0,0 +1,127 @@
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
+ ChatScreen을 사용해 채팅 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
14
+
15
+ ## 영역 구조
16
+
17
+ ```text
18
+ host: 남은 높이·safe area·키보드
19
+ └─ 헤더 → 메시지 타임라인 → 작성창
20
+ ```
21
+
22
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
23
+ | --- | --- | --- | --- |
24
+ | 바깥 틀 | ChatScreen | route 본문 | [API 배치 규칙](../components/chat-screen.md#배치), host 남은 높이 |
25
+ | 내용 | 공개 슬롯 | 헤더 → 메시지 타임라인 → 작성창 | 화면 recipe의 sectionGap·itemGap; 슬롯 안은 각 지침 토큰 |
26
+ | 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
27
+
28
+ ## 버튼과 행동 위치
29
+
30
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
31
+ | --- | --- | --- | --- |
32
+ | 작업 | ChatScreen 공개 행동 슬롯 | 전송은 composer 끝; 답장은 메시지의 명시 행동과 제스처 | 같은 표면에 경쟁하는 primary 하나만 |
33
+ | 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
34
+
35
+ ## 상태
36
+
37
+ | 상태 | 화면 모습 | 행동 |
38
+ | --- | --- | --- |
39
+ | 기본 | 헤더 → 메시지 타임라인 → 작성창 | 각 공개 콜백을 제품 상태에 연결 |
40
+ | 로딩 | 최초 조회는 본문 상태, 저장은 해당 행동 pending | 중복 제출 차단; 성공을 먼저 표시하지 않음 |
41
+ | 빈 | 실제 조회 0건 또는 아직 작성하지 않은 상태 안내 | 시작·조건 해제 등 맥락에 맞는 대안 |
42
+ | 오류 | 전송 실패는 초안과 타임라인을 유지하고 notice로 안내 | 실패 원인과 재시도 경로 제공 |
43
+
44
+ ## 사용하는 지침
45
+
46
+ | 지침 | 쓰는 곳 |
47
+ | --- | --- |
48
+ | [ChatScreen](../components/chat-screen.md) | 필수 props·슬롯·플랫폼 차이 |
49
+ | [Button](../components/button.md) | 동작·로딩·보조 행동 |
50
+ | [ScreenLayout](../components/screen-layout.md) | 화면 높이·본문 교체·스크롤 소유 |
51
+
52
+ ## 코드 골격
53
+
54
+ ```tsx
55
+ // Web — host가 실제 남은 높이를 준다(예: height: 100dvh)
56
+ import { ChatMessage, ChatScreen, MessageComposer } from "@hjmds/react/screens";
57
+
58
+ <ChatScreen
59
+ title={t("chat.title")}
60
+ scroll="screen" // 짧은 기록. 긴 기록은 제품 가상 목록 + 기본 scroll="content"
61
+ state={isPending ? { kind: "loading", title: t("chat.loading") } : { kind: "ready" }}
62
+ composer={<MessageComposer value={draft} label={t("chat.input")} sendLabel={t("chat.send")}
63
+ pending={sending} onValueChange={setDraft} onSend={send} />}
64
+ >
65
+ {messages.map((m) => (
66
+ <ChatMessage key={m.id} direction={m.direction} author={m.author} timestamp={m.timestamp}
67
+ {...(m.deliveryLabel ? { deliveryLabel: m.deliveryLabel } : {})}>
68
+ {m.text}
69
+ </ChatMessage>
70
+ ))}
71
+ </ChatScreen>
72
+ ```
73
+
74
+ ```tsx
75
+ // Native — host가 safe area·키보드를 한 번 처리한다
76
+ import { FlatList } from "react-native";
77
+ import { ChatMessage, ChatScreen, MessageComposer } from "@hjmds/react-native/screens";
78
+ import { Text } from "@hjmds/react-native/primitives";
79
+
80
+ <ChatScreen
81
+ title={t("chat.title")}
82
+ state={isPending ? { kind: "loading", title: t("chat.loading") } : { kind: "ready" }}
83
+ composer={<MessageComposer value={draft} label={t("chat.input")} sendLabel={t("chat.send")}
84
+ pending={sending} onValueChange={setDraft} onSend={send} />}
85
+ >
86
+ {/* 기본 scroll="content": 가상 목록이 스크롤을 소유한다 */}
87
+ <FlatList
88
+ data={messages}
89
+ keyExtractor={(m) => m.id}
90
+ renderItem={({ item: m }) => (
91
+ <ChatMessage direction={m.direction} author={m.author} timestamp={m.timestamp}
92
+ {...(m.deliveryLabel ? { deliveryLabel: m.deliveryLabel } : {})}>
93
+ <Text>{m.text}</Text>
94
+ </ChatMessage>
95
+ )}
96
+ />
97
+ </ChatScreen>
98
+ ```
99
+
100
+ `ChatScreen`의 `scroll` 기본값은 `"content"`다(본문은 스크롤하지 않고 자식 목록이 스크롤을 소유한다). 메시지 목록은
101
+ HJM이 제공하지 않으므로 제품이 고른다: Native는 `FlatList`, Web은 가변 높이 가상 목록 또는 짧은 기록의 `scroll="screen"`.
102
+ HJM `Timeline`(이벤트 연표)과 `VirtualList`(고정 `rowHeight`)는 높이가 제각각인 메시지 목록용이 아니다.
103
+ `deliveryLabel`은 선택 prop이라 값이 없을 때 `undefined`를 넘기지 않고 조건부 spread로 뺀다(`exactOptionalPropertyTypes`).
104
+ 콜백·데이터·지역화 함수는 제품에서 공급한다.
105
+
106
+ ## 큰 글자·다크·좁은 폭
107
+
108
+ | 조건 | 바뀌는 것 |
109
+ | --- | --- |
110
+ | 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
111
+ | 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
112
+ | 좁은 폭 | 320px부터 한 열로 읽기 순서 유지. 가상화 본문은 scroll=content, 중첩 스크롤 금지 |
113
+ | 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
114
+
115
+ ## 플랫폼 차이
116
+
117
+ | 항목 | Web | Native |
118
+ | --- | --- | --- |
119
+ | 메시지 목록 | 제품 가변 높이 가상 목록 또는 짧은 기록의 `scroll="screen"` | `FlatList`(기본 `scroll="content"`) |
120
+ | 키보드 | 브라우저가 처리, footer 가림만 확인 | host 키보드 어댑터 또는 `KeyboardAvoidingView` 하나로 감싼다(둘 다 쓰지 않음) |
121
+
122
+ ## 함정
123
+
124
+ - 전송 실패는 초안과 타임라인을 유지하고 notice로 안내.
125
+ - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
126
+ - 긴 대화를 `scroll="screen"` + map으로 그리지 않는다. Native는 `FlatList`(기본 `scroll="content"`, 답장 원문 이동은 `scrollToIndex`), Web은 가변 높이 가상 목록이다. Web 예제의 `scroll="screen"`은 짧은 고정 기록이라서다.
127
+ - 실패 복구 스토리(`실패와 초안 복구`)는 다음 전송 실패를 예약하는 데모 도구로 연다.