@hjmds/design-contracts 1.12.1 → 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,109 @@
1
+ # 숫자 변화와 메뉴 변형
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [Statistic](../../statistic.md), [선택 어댑터](../../optional-adapters.md), `showcase/web/src/components/OptionalMotion.stories.tsx`
9
+ - 스토리북: `배포/구성/직접 조작과 모션/숫자 변화와 메뉴 변형`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 선택 설치 모션(숫자 자리 단위 변화, 메뉴 형태 변환)을 기존 컴포넌트 자리에 끼워 넣을 때 쓴다. 숫자 지표가 사용자 행동으로
14
+ 바뀌는 곳에 AnimatedStatistic을, 실행만 하는 짧은 작업 메뉴에 MorphingMenu를 둔다. 모션이 꺼진 환경에서는 둘 다 기본
15
+ 컴포넌트 동작으로 돌아간다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | AnimatedStatistic (`/statistic-motion`) | 숫자 값 변화를 자리 단위로 보인다(optional peer `@number-flow/react` 0.6.2) | [Statistic](../components/statistic.md) |
22
+ | Button | 값을 바꾸는 행동 | [Button](../components/button.md) |
23
+ | MorphingMenu (`/menu-morph`) | 실행 전용 작업 메뉴(optional peer `bloom-menu` 0.1.0) | [Menu](../components/menu.md) |
24
+ | `Text role="status"` | 마지막 실행·갱신 결과 문구 | [Text](../components/text.md) |
25
+ | Stack | 세로 묶음 간격 | [Stack](../components/stack.md) |
26
+
27
+ ## 배치
28
+
29
+ ```text
30
+ ┌ 제품 화면 Container(gutter) 안 ─────────────┐
31
+ │ Stack gap="md" 16 │
32
+ │ 조회 수 │
33
+ │ 1,280 ← AnimatedStatistic │
34
+ │ [ 조회 수 새로고침 ] secondary │
35
+ │ [ 작업 선택 ▾ ] ← MorphingMenu 트리거 │
36
+ │ ├ 저장 │
37
+ │ ├ 공유 │
38
+ │ └ 삭제 (disabled) │
39
+ │ 결과 문구 ← Text role="status" │
40
+ └─────────────────────────────────────────────┘
41
+ ```
42
+
43
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
44
+ | --- | --- | --- | --- |
45
+ | 바깥 틀 | 제품 화면의 `Container`(좌우 여백·최대 폭) 안에 `Stack gap="md"` | 본문 흐름 안. 스크롤·안전 영역은 화면(문서 스크롤)이 소유하고, 텍스트 입력이 없어 키보드 처리는 없다 | 좌우 `Container` `gutter`(폭 600 미만 `compact` 16, 이상 `regular` 20). 요소 사이 `layout.contentGap` 16. 다른 구획과는 `layout.sectionGap` 24 |
46
+ | 지표 | AnimatedStatistic | 맨 위 | 값은 Statistic `comfortable`(`heading` 크기) |
47
+ | 값 갱신 | Button `secondary` | 지표 아래 | 높이 `medium` 44. 화면의 primary는 제품 주 행동에 남긴다 |
48
+ | 작업 메뉴 | MorphingMenu | 값 갱신 아래 | 트리거에서 펼쳐짐 |
49
+ | 결과 문구 | `Text as="p" role="status"` | 맨 아래 | — |
50
+
51
+ ## 흐름과 상태
52
+
53
+ 1. "조회 수 새로고침"을 누르면 제품이 새 값을 요청한다. 응답이 오면 `value`가 바뀌고 AnimatedStatistic이 바뀐 자리만 굴려 보인다.
54
+ 2. "작업 선택"을 열면 메뉴가 트리거에서 형태를 바꿔 펼쳐진다. 항목을 고르면 `onAction(id)`가 오고 메뉴가 닫힌다. 제품은 결과를 상태로 바꿔 결과 문구 키를 고른다.
55
+
56
+ | 상태 | 모습 | 포커스·알림 |
57
+ | --- | --- | --- |
58
+ | 기본 | 현재 값과 닫힌 메뉴, 안내 결과 문구(`post.actionHint`) | — |
59
+ | 진행 중 | 새로고침 Button `loading`(다시 누를 수 없음), 지표는 직전 값 유지 | Button의 진행 표시 |
60
+ | 실패 | 새로고침 요청 실패: 지표는 직전 값을 유지하고 결과 문구가 `stats.refreshFailed`. 버튼을 다시 누르면 재요청하고, 재요청도 실패하면 같은 문구를 다시 둔다. 메뉴 작업 실패도 같은 결과 문구 자리에 둔다 | `role="status"`로 알림 |
61
+ | 값 변화 | 자리 단위 숫자 전환 | 최종 값만 읽힌다 |
62
+ | 비활성 항목 | `disabled` 항목은 고를 수 없다 | — |
63
+ | reduced motion·RTL | AnimatedStatistic은 정적 값, MorphingMenu는 `Menu`로 돌아간다 | 같다 |
64
+ | 라틴 숫자가 아닌 numbering system, `ar`·`fa`·`he`·`ur` locale | AnimatedStatistic이 정적 값으로 그린다 | — |
65
+
66
+ 상태→문구 키는 상수 표로 둔다(아래 `resultKey`). 템플릿 문자열 키는 키 추출·누락 검사가 찾지 못한다.
67
+
68
+ ## 코드 골격
69
+
70
+ ```tsx
71
+ // Web
72
+ import { Stack, Text } from "@hjmds/react/layout";
73
+ import { AnimatedStatistic } from "@hjmds/react/statistic-motion";
74
+ import { MorphingMenu } from "@hjmds/react/menu-morph";
75
+ import { Button } from "@hjmds/react/actions";
76
+
77
+ const resultKey = { idle: "post.actionHint", saved: "post.saved", shared: "post.shared", refreshFailed: "stats.refreshFailed" } as const;
78
+
79
+ <Stack gap="md">
80
+ <AnimatedStatistic descriptor={{ id: "views", label: t("stats.views") }} value={views} locale={locale} />
81
+ <Button tone="secondary" loading={refreshing} onClick={refresh}>{t("stats.refresh")}</Button>
82
+ <MorphingMenu label={t("post.actions")} items={[
83
+ { id: "save", label: t("post.save") },
84
+ { id: "share", label: t("post.share") },
85
+ ]} onAction={(id) => setResult(id === "share" ? "shared" : "saved")} />
86
+ <Text as="p" role="status">{t(resultKey[result])}</Text>
87
+ </Stack>
88
+ ```
89
+
90
+ ```tsx
91
+ // Native
92
+ // 없음. MorphingMenu는 Native가 없다. Native AnimatedStatistic(`@hjmds/react-native/statistic-motion`)은 지표 전체를 rise로 전환한다.
93
+ ```
94
+
95
+ ## 플랫폼 차이
96
+
97
+ | 항목 | Web | Native |
98
+ | --- | --- | --- |
99
+ | 스토리 | 있음 | 없음 |
100
+ | AnimatedStatistic 모션 | 숫자 자리 단위 NumberFlow | 지표 전체 `rise` 전환 |
101
+ | MorphingMenu | `/menu-morph` | 없음, `Menu`를 쓴다 |
102
+
103
+ ## 함정
104
+
105
+ - AnimatedStatistic `descriptor`에 `value`를 넣지 않는다. 값은 `value` prop의 숫자로 받는다.
106
+ - MorphingMenu는 실행 전용이다. 선택 상태나 비동기 목록이 필요하면 `Menu`를 쓴다.
107
+ - optional peer를 설치하지 않으면 subpath import가 실패한다. 제품 package.json에 peer를 명시한다.
108
+ - 예제는 `Stack gap="md"`, secondary 값 변경 버튼과 `Text role="status"`를 사용한다. 작업 ID는 사용자 문구로 변환한 뒤 표시한다.
109
+ - 현재 스토리는 값을 로컬에서 바로 올려 진행 중·실패 상태가 없다. 서버 값을 새로고침하는 제품은 위 진행 중·실패 행을 따른다.
@@ -0,0 +1,104 @@
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
+ 명시적으로 선택 후 플랫폼 picker 실행 흐름이 필요할 때 쓴다. PhotoSourceSheet의 상태·콜백을 제품 로직에 연결하며 새 데이터 엔진을 만들지 않는다.
14
+
15
+ ## 구성 요소
16
+
17
+ | 컴포넌트 | 역할 | 지침 |
18
+ | --- | --- | --- |
19
+ | PhotoSourceSheet | 명시적으로 선택 후 플랫폼 picker 실행 | [공개 계약](../components/photo-source-sheet.md) |
20
+ | Button | 명시 행동·재시도 | [Button](../components/button.md) |
21
+
22
+ ## 배치
23
+
24
+ ```text
25
+ 부모 화면의 공개 슬롯
26
+ └─ 제목 → 앨범 → 촬영
27
+ ```
28
+
29
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
30
+ | --- | --- | --- | --- |
31
+ | 바깥 틀 | PhotoSourceSheet 또는 포함 Surface | 부모 화면의 해당 슬롯 | [배치](../components/photo-source-sheet.md#배치)에 따른다 |
32
+ | 내용 | PhotoSourceSheet 내부 슬롯 | 제목 → 앨범 → 촬영 | spacing 토큰과 포함 컴포넌트 recipe |
33
+ | 행동 | Button 또는 공개 콜백 | 내용과 가까운 명시 진입점 | 주 행동 하나, 보조 행동과 구분 |
34
+
35
+ ## 흐름과 상태
36
+
37
+ 1. 명시적으로 선택 후 플랫폼 picker 실행.
38
+ 2. 진행 상태와 제품의 실제 확정을 분리한다.
39
+ 3. 권한 거부·취소는 기존 선택 유지.
40
+
41
+ | 상태 | 모습 | 포커스·알림 |
42
+ | --- | --- | --- |
43
+ | 기본 | 제목 → 앨범 → 촬영 | 이름·선택 여부를 보조공학에 노출 |
44
+ | 진행 중 | 해당 작업 pending, 입력·기존 결과 보존 | 중복 요청 차단, 로딩에 포커스를 옮기지 않음 |
45
+ | 실패 | 권한 거부·취소는 기존 선택 유지 | 오류 근처 재시도, 필요할 때만 오류 읽기 |
46
+
47
+ ## 코드 골격
48
+
49
+ ```tsx
50
+ // Web
51
+ import { PhotoSourceSheet } from "@hjmds/react/screen-flows";
52
+
53
+ const libraryInput = useRef<HTMLInputElement>(null);
54
+ const cameraInput = useRef<HTMLInputElement>(null);
55
+
56
+ <>
57
+ <input ref={libraryInput} type="file" accept="image/*" multiple hidden onChange={(e) => addFiles(e.currentTarget.files)} />
58
+ <input ref={cameraInput} type="file" accept="image/*" capture="environment" hidden onChange={(e) => addFiles(e.currentTarget.files)} />
59
+ <PhotoSourceSheet
60
+ open={open}
61
+ onOpenChange={setOpen}
62
+ labels={{
63
+ title: t("photo.source.title"),
64
+ library: t("photo.source.library"),
65
+ camera: t("photo.source.camera"),
66
+ cancel: t("common.cancel"),
67
+ }}
68
+ cameraAvailable={hasCamera}
69
+ onSelect={(source) => (source === "camera" ? cameraInput : libraryInput).current?.click()}
70
+ />
71
+ </>
72
+ ```
73
+
74
+ ```tsx
75
+ // Native
76
+ import { PhotoSourceSheet } from "@hjmds/react-native/screen-flows";
77
+
78
+ <PhotoSourceSheet
79
+ open={open}
80
+ onOpenChange={setOpen}
81
+ labels={{
82
+ title: t("photo.source.title"),
83
+ library: t("photo.source.library"),
84
+ camera: t("photo.source.camera"),
85
+ cancel: t("common.cancel"),
86
+ }}
87
+ cameraAvailable={hasCamera}
88
+ onSelect={(source) => (source === "camera" ? takePhoto() : pickFromLibrary())}
89
+ />
90
+ ```
91
+
92
+ 제품 데이터·콜백은 주입한다. 위 공개 API 지침에 Web·Native 차이를 유지한다.
93
+
94
+ ## 플랫폼 차이
95
+
96
+ | 항목 | Web | Native |
97
+ | --- | --- | --- |
98
+ | `onSelect` 호출 시점 | 버튼 클릭 안에서 동기 호출 — 파일 입력 `click()`이 사용자 활성화 안에 있어야 브라우저가 막지 않는다 | 시트 닫힘이 끝난 뒤(`onDismissComplete`) 호출 — iOS는 닫히는 Modal 위에 카메라·앨범을 띄우지 못한다 |
99
+ | picker 실행 | 제품의 `<input type="file">`(촬영은 `capture`) | 제품의 이미지 picker·카메라 모듈 |
100
+
101
+ ## 함정
102
+
103
+ - 권한 거부·취소는 기존 선택 유지.
104
+ - 포인터·제스처만으로 기능을 숨기지 않는다. 키보드·단일 탭 경로와 취소 후 복귀도 검증한다.
@@ -0,0 +1,110 @@
1
+ # 댓글 작성
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [MessageComposer](../components/message-composer.md), [공통 실행과 실패 복구](../../action-session.md), `showcase/web/src/patterns/conversation-previews.tsx`, `showcase/native/src/conversation-previews.tsx`. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/구성/입력과 작성/댓글 작성`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
+ - 스토리북: `배포/구성/입력과 작성/댓글 작성`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 게시물·기록 아래에서 댓글이나 특정 댓글에 대한 답글을 남기고, 실패하면 글과 답글 대상을 그대로 남겨 다시 등록하게 할 때 쓴다. 새 입력 엔진을 만들지 않고 MessageComposer를 쓴다.
14
+
15
+ 댓글 목록·답글 펼침·더 보기까지 포함한 화면은 [댓글 화면](../screens/common-comments.md)이 소유한다. 이 구성은 그 화면 하단 composer의 진행 중·실패·초안 유지만 따로 보인다.
16
+ 사진을 함께 보내는 대화는 [메시지 작성](purpose-input-message.md)을 쓴다.
17
+
18
+ ## 구성 요소
19
+
20
+ | 컴포넌트 | 역할 | 지침 |
21
+ | --- | --- | --- |
22
+ | `MessageComposer` | 입력·전송. `pending`이면 전송 버튼이 스피너만 보이고 접근성 이름은 유지한다 | [MessageComposer](../components/message-composer.md) |
23
+ | `replyTo` | 답글을 달 댓글 발췌와 취소 | [MessageComposer](../components/message-composer.md) |
24
+ | `context` 슬롯 + `Text tone="danger"` | 실패 문구를 입력 바로 위에 둔다 | [Text](../components/text.md) |
25
+ | `Heading level="level5"` | 구획 제목(선택). 화면 composer 슬롯에 넣을 때는 두지 않는다 | [Heading](../components/heading.md) |
26
+ | `createActionSession` | 전송 진행·중복 차단·재시도 | [계약](../../action-session.md) |
27
+
28
+ ## 배치
29
+
30
+ ```text
31
+ ┌ 바깥 틀: Web 문서 스크롤 · Native ScrollView(위아래 spacing.md 16) ┐
32
+ │ ← Container gutter 16(폭 600 미만)/20 → │
33
+ │ ┌ Surface padding lg ────────────────────────────────┐ │
34
+ │ │ 댓글 작성 Heading level5 │ │
35
+ │ │ ┌ MessageComposer ───────────────────────────────┐ │ │
36
+ │ │ │ context: 실패 문구(실패일 때만) │ │ │
37
+ │ │ │ 답글 대상: 서연 · 발췌 ............ [답글 취소]│ │ │
38
+ │ │ │ [ 댓글을 남겨 주세요 (1~5줄) (↑) ] │ │ ← circle 등록│
39
+ │ │ └────────────────────────────────────────────────┘ │ │
40
+ │ │ 등록한 댓글 목록(예제) │ │
41
+ │ └────────────────────────────────────────────────────┘ │
42
+ │ [답글 대상 선택] [다음 전송 실패시키기] ← 데모 보조 행동 │
43
+ └──────────────────────────────────────────────────────────────────────┘
44
+ ```
45
+
46
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
47
+ | --- | --- | --- | --- |
48
+ | 바깥 틀 | 화면의 composer 슬롯(ChatScreen·CommentThreadScreen `composer`). 단독 예제는 Web 문서 스크롤 > `Container`, Native `ScrollView` > `Container` | 화면 하단(footer). 키보드 adapter는 화면 host 하나만 | 좌우 `Container gutter`: 폭 600 미만 `compact` 16, 이상 `regular` 20. Native 위아래 `spacing.md` 16 |
49
+ | 제목(선택) | `Heading level5` | Surface 맨 위 | 아래 `spacing.md` 16 |
50
+ | composer | `MessageComposer` | 제목 아래 | 입력 1~5줄(`composerMaxLines`), 세로 묶음 `screenPatternRecipe.itemGap` 12(두 플랫폼, 1.12.1 이하 Web은 8)([배치](../components/message-composer.md#배치)) |
51
+ | 실패 문구 | `context` | composer 맨 위 | composer 세로 간격 |
52
+ | 데모 보조 행동 | `Button` secondary·ghost | Surface 아래 가로 줄 | `spacing.sm` 12. 제품에는 넣지 않는다 |
53
+
54
+ ## 흐름과 상태
55
+
56
+ 1. 사용자가 글을 쓰고 필요하면 답글 대상을 고른 뒤 전송한다(Enter는 줄바꿈).
57
+ 2. 전송 중에는 `pending`으로 입력과 전송을 잠그고 같은 요청을 다시 보내지 않는다.
58
+ 3. 서버가 성공을 확정한 뒤에만 글·답글 대상을 지운다.
59
+ 4. 실패하면 모두 그대로 두고 `context`에 실패 문구를 보인다. 같은 전송 버튼으로 다시 보낸다.
60
+
61
+ | 상태 | 모습 | 포커스·알림 |
62
+ | --- | --- | --- |
63
+ | 기본 | 빈 입력, 전송 비활성(글이 없을 때) | 입력 이름 `label` |
64
+ | 진행 중 | `pending`: 입력 잠금, 전송 자리 스피너 | 포커스 유지, 중복 요청 차단 |
65
+ | 실패 | 글·답글 대상 유지, `context`에 실패 문구 | 실패 문구는 입력 근처, 포커스 이동 없음 |
66
+ | 성공 | 초안 정리, 보낸 항목이 목록에 추가 | 포커스는 입력에 남는다 |
67
+
68
+ ## 코드 골격
69
+
70
+ ```tsx
71
+ // Web
72
+ import { MessageComposer } from "@hjmds/react/screens";
73
+ import { Text } from "@hjmds/react/layout";
74
+
75
+ <MessageComposer
76
+ label={t("comments.input")}
77
+ sendLabel={t("comments.send")}
78
+ value={draft}
79
+ onValueChange={setDraft}
80
+ pending={sending}
81
+ context={failed ? <Text tone="danger">{t("comments.sendFailed")}</Text> : null}
82
+ {...(replyTo ? { replyTo: { author: replyTo.author, excerpt: replyTo.text, cancelLabel: t("comments.cancelReply"), onCancel: () => setReplyTo(null) } } : {})}
83
+ onSend={async (text) => { const ok = await send(text); if (ok) clearDraft(); }}
84
+ />
85
+ ```
86
+
87
+ ```tsx
88
+ // Native
89
+ import { MessageComposer } from "@hjmds/react-native/screens";
90
+ import { Text } from "@hjmds/react-native/primitives";
91
+
92
+ <MessageComposer
93
+ label={t("comments.input")}
94
+ sendLabel={t("comments.send")}
95
+ value={draft}
96
+ onValueChange={setDraft}
97
+ pending={sending}
98
+ context={failed ? <Text tone="danger">{t("comments.sendFailed")}</Text> : null}
99
+ {...(replyTo ? { replyTo: { author: replyTo.author, excerpt: replyTo.text, cancelLabel: t("comments.cancelReply"), onCancel: () => setReplyTo(null) } } : {})}
100
+ onSend={async (text) => { const ok = await send(text); if (ok) clearDraft(); }}
101
+ />
102
+ ```
103
+
104
+ `send`·`clearDraft`·`replyTo`는 제품 소유다. `onSend`는 문자열만 넘기므로 답글 대상은 제품 상태에서 읽는다.
105
+
106
+ ## 함정
107
+
108
+ - 실패 시 글·답글 대상을 지우지 않는다. 성공을 먼저 표시하고 지우면 재전송할 내용이 사라진다.
109
+ - Enter 전송을 덧붙이지 않는다. IME 조합 중 Enter를 전송으로 가로채면 한글 마지막 글자가 빠진다.
110
+ - 스토리의 "다음 전송 실패시키기"·350ms 지연·예제 사진은 로컬 시연이다. 업로드·서버 저장·권한은 제품이 연결한다.
@@ -0,0 +1,119 @@
1
+ # 메시지 작성
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [MessageComposer](../components/message-composer.md), [공통 실행과 실패 복구](../../action-session.md), `showcase/web/src/patterns/conversation-previews.tsx`, `showcase/native/src/conversation-previews.tsx`. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/구성/입력과 작성/메시지 작성`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
+ - 스토리북: `배포/구성/입력과 작성/메시지 작성`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 대화 화면 하단에서 글과 사진 여러 장을 함께 보내고, 실패하면 글·사진·답장 대상을 그대로 남겨 다시 보내게 할 때 쓴다. 새 입력 엔진을 만들지 않고 MessageComposer를 쓴다.
14
+
15
+ 2026-10-06 `실험/구성/공통 화면/메시지 작성`(Web id `common-screen-composer`)이 같은 MessageComposer를 보이던 중복이라 이 구성으로 합쳤다.
16
+ 그 항목의 고유 변형이던 여러 사진 전송은 `여러 사진 전송` 스토리로 옮겼고, 효과가 없던 화면 상태 스토리(불러오는 중·빈·오류·로그인 필요)는 [채팅 화면](../screens/common-chat.md)이 소유한다.
17
+ 댓글처럼 사진 없이 답글 대상만 다루면 [댓글 작성](purpose-input-comment.md)을 쓴다.
18
+
19
+ ## 구성 요소
20
+
21
+ | 컴포넌트 | 역할 | 지침 |
22
+ | --- | --- | --- |
23
+ | `MessageComposer` | 입력·전송. `pending`이면 전송 버튼이 스피너만 보이고 접근성 이름은 유지한다 | [MessageComposer](../components/message-composer.md) |
24
+ | `attachmentAction`·`attachments`·`onRemoveAttachment` | 사진 추가(여러 장), 썸네일마다 제거 | [MessageComposer](../components/message-composer.md) |
25
+ | `replyTo` | 답장할 메시지 발췌와 취소 | [MessageComposer](../components/message-composer.md) |
26
+ | `context` 슬롯 + `Text tone="danger"` | 실패 문구를 입력 바로 위에 둔다 | [Text](../components/text.md) |
27
+ | `Heading level="level5"` | 구획 제목(선택). 화면 composer 슬롯에 넣을 때는 두지 않는다 | [Heading](../components/heading.md) |
28
+ | `createActionSession` | 전송 진행·중복 차단·재시도 | [계약](../../action-session.md) |
29
+
30
+ ## 배치
31
+
32
+ ```text
33
+ ┌ 바깥 틀: Web 문서 스크롤 · Native ScrollView(위아래 spacing.md 16) ┐
34
+ │ ← Container gutter 16(폭 600 미만)/20 → │
35
+ │ ┌ Surface padding lg ────────────────────────────────┐ │
36
+ │ │ 메시지 작성 Heading level5 │ │
37
+ │ │ ┌ MessageComposer ───────────────────────────────┐ │ │
38
+ │ │ │ context: 실패 문구(실패일 때만) │ │ │
39
+ │ │ │ 답장 대상: 서연 · 발췌 ............ [답장 취소]│ │ │
40
+ │ │ │ [사진][사진] [+ 사진 추가] ← 가로 스크롤 │ │ │
41
+ │ │ │ [ 메시지 입력 (1~5줄) (↑) ] │ │ ← circle 전송│
42
+ │ │ └────────────────────────────────────────────────┘ │ │
43
+ │ │ 보낸 메시지 목록(예제) │ │
44
+ │ └────────────────────────────────────────────────────┘ │
45
+ │ [답장할 메시지 선택] [다음 전송 실패시키기] ← 데모 보조 행동 │
46
+ └──────────────────────────────────────────────────────────────────────┘
47
+ ```
48
+
49
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
50
+ | --- | --- | --- | --- |
51
+ | 바깥 틀 | 화면의 composer 슬롯(ChatScreen·CommentThreadScreen `composer`). 단독 예제는 Web 문서 스크롤 > `Container`, Native `ScrollView` > `Container` | 화면 하단(footer). 키보드 adapter는 화면 host 하나만 | 좌우 `Container gutter`: 폭 600 미만 `compact` 16, 이상 `regular` 20. Native 위아래 `spacing.md` 16 |
52
+ | 제목(선택) | `Heading level5` | Surface 맨 위 | 아래 `spacing.md` 16 |
53
+ | composer | `MessageComposer` | 제목 아래 | 입력 1~5줄(`composerMaxLines`), 세로 묶음 `screenPatternRecipe.itemGap` 12(두 플랫폼, 1.12.1 이하 Web은 8)([배치](../components/message-composer.md#배치)) |
54
+ | 실패 문구 | `context` | composer 맨 위 | composer 세로 간격 |
55
+ | 데모 보조 행동 | `Button` secondary·ghost | Surface 아래 가로 줄 | `spacing.sm` 12. 제품에는 넣지 않는다 |
56
+
57
+ ## 흐름과 상태
58
+
59
+ 1. 사용자가 글을 쓰고 사진을 여러 장 고른 뒤 전송한다(Enter는 줄바꿈).
60
+ 2. 전송 중에는 `pending`으로 입력과 전송을 잠그고 같은 요청을 다시 보내지 않는다.
61
+ 3. 서버가 성공을 확정한 뒤에만 글·사진·답장 대상을 지운다.
62
+ 4. 실패하면 모두 그대로 두고 `context`에 실패 문구를 보인다. 같은 전송 버튼으로 다시 보낸다.
63
+
64
+ | 상태 | 모습 | 포커스·알림 |
65
+ | --- | --- | --- |
66
+ | 기본 | 빈 입력, 전송 비활성(글·사진이 없을 때) | 입력 이름 `label` |
67
+ | 진행 중 | `pending`: 입력 잠금, 전송 자리 스피너 | 포커스 유지, 중복 요청 차단 |
68
+ | 실패 | 글·사진·답장 대상 유지, `context`에 실패 문구 | 실패 문구는 입력 근처, 포커스 이동 없음 |
69
+ | 성공 | 초안 정리, 보낸 항목이 목록에 추가 | 포커스는 입력에 남는다 |
70
+
71
+ ## 코드 골격
72
+
73
+ ```tsx
74
+ // Web
75
+ import { MessageComposer } from "@hjmds/react/screens";
76
+ import { Text } from "@hjmds/react/layout";
77
+
78
+ <MessageComposer
79
+ label={t("chat.input")}
80
+ sendLabel={t("chat.send")}
81
+ value={draft}
82
+ onValueChange={setDraft}
83
+ pending={sending}
84
+ context={failed ? <Text tone="danger">{t("chat.sendFailed")}</Text> : null}
85
+ {...(replyTo ? { replyTo: { author: replyTo.author, excerpt: replyTo.text, cancelLabel: t("chat.cancelReply"), onCancel: () => setReplyTo(null) } } : {})}
86
+ attachmentAction={{ label: t("chat.addPhoto"), icon: <PlusIcon />, onPress: pickPhotos }}
87
+ attachments={photos.map((photo) => ({ id: photo.id, removeLabel: t("chat.removePhoto"), preview: <img src={photo.uri} alt="" /> }))}
88
+ onRemoveAttachment={(id) => setPhotos((list) => list.filter((photo) => photo.id !== id))}
89
+ onSend={async (text) => { const ok = await send(text); if (ok) clearDraft(); }}
90
+ />
91
+ ```
92
+
93
+ ```tsx
94
+ // Native
95
+ import { MessageComposer } from "@hjmds/react-native/screens";
96
+ import { Text } from "@hjmds/react-native/primitives";
97
+
98
+ <MessageComposer
99
+ label={t("chat.input")}
100
+ sendLabel={t("chat.send")}
101
+ value={draft}
102
+ onValueChange={setDraft}
103
+ pending={sending}
104
+ context={failed ? <Text tone="danger">{t("chat.sendFailed")}</Text> : null}
105
+ {...(replyTo ? { replyTo: { author: replyTo.author, excerpt: replyTo.text, cancelLabel: t("chat.cancelReply"), onCancel: () => setReplyTo(null) } } : {})}
106
+ attachmentAction={{ label: t("chat.addPhoto"), icon: <PlusIcon />, onPress: pickPhotos }}
107
+ attachments={photos.map((photo) => ({ id: photo.id, removeLabel: t("chat.removePhoto"), preview: <Image source={{ uri: photo.uri }} style={thumb} /> }))}
108
+ onRemoveAttachment={(id) => setPhotos((list) => list.filter((photo) => photo.id !== id))}
109
+ onSend={async (text) => { const ok = await send(text); if (ok) clearDraft(); }}
110
+ />
111
+ ```
112
+
113
+ `send`·`clearDraft`·`pickPhotos`·`photos`·`replyTo`는 제품 소유다. `onSend`는 문자열만 넘기므로 사진 목록은 제품 상태에서 읽는다.
114
+
115
+ ## 함정
116
+
117
+ - 실패 시 글·사진·답장 대상을 지우지 않는다. 성공을 먼저 표시하고 지우면 재전송할 내용이 사라진다.
118
+ - Enter 전송을 덧붙이지 않는다. IME 조합 중 Enter를 전송으로 가로채면 한글 마지막 글자가 빠진다.
119
+ - 스토리의 "다음 전송 실패시키기"·350ms 지연·예제 사진은 로컬 시연이다. 업로드·서버 저장·권한은 제품이 연결한다.
@@ -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
+ 첫 기록을 단계별 작성하고 중단한 초안 이어가기 흐름이 필요할 때 쓴다. Steps의 상태·콜백을 제품 로직에 연결하며 새 데이터 엔진을 만들지 않는다.
14
+
15
+ ## 구성 요소
16
+
17
+ | 컴포넌트 | 역할 | 지침 |
18
+ | --- | --- | --- |
19
+ | Steps | 첫 기록을 단계별 작성하고 중단한 초안 이어가기 | [공개 계약](../components/steps.md) |
20
+ | Button | 명시 행동·재시도 | [Button](../components/button.md) |
21
+
22
+ ## 배치
23
+
24
+ ```text
25
+ 부모 화면의 공개 슬롯
26
+ └─ 주제 → 입력 → 검토 → 결과
27
+ ```
28
+
29
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
30
+ | --- | --- | --- | --- |
31
+ | 바깥 틀 | Steps 또는 포함 Surface | 부모 화면의 해당 슬롯 | [배치](../components/steps.md#배치)에 따른다 |
32
+ | 내용 | Steps 내부 슬롯 | 주제 → 입력 → 검토 → 결과 | spacing 토큰과 포함 컴포넌트 recipe |
33
+ | 행동 | Button 또는 공개 콜백 | 내용과 가까운 명시 진입점 | 주 행동 하나, 보조 행동과 구분 |
34
+
35
+ ## 흐름과 상태
36
+
37
+ 1. 첫 기록을 단계별 작성하고 중단한 초안 이어가기.
38
+ 2. 진행 상태와 제품의 실제 확정을 분리한다.
39
+ 3. 저장 실패는 입력·단계 유지; 메모리 초안은 새로고침 복구를 보장하지 않음.
40
+
41
+ | 상태 | 모습 | 포커스·알림 |
42
+ | --- | --- | --- |
43
+ | 기본 | 주제 → 입력 → 검토 → 결과 | 이름·선택 여부를 보조공학에 노출 |
44
+ | 진행 중 | 해당 작업 pending, 입력·기존 결과 보존 | 중복 요청 차단, 로딩에 포커스를 옮기지 않음 |
45
+ | 실패 | 저장 실패는 입력·단계 유지; 메모리 초안은 새로고침 복구를 보장하지 않음 | 오류 근처 재시도, 필요할 때만 오류 읽기 |
46
+
47
+ ## 코드 골격
48
+
49
+ ```tsx
50
+ // Web
51
+ import { Steps } from "@hjmds/react/steps";
52
+
53
+ <Steps
54
+ descriptor={{
55
+ currentStepId: "team",
56
+ steps: [
57
+ { id: "welcome", label: t("onboarding.welcome") },
58
+ { id: "team", label: t("onboarding.team") },
59
+ { id: "alerts", label: t("onboarding.alerts") },
60
+ ],
61
+ }}
62
+ statusLabels={{
63
+ pending: t("steps.pending"), current: t("steps.current"),
64
+ complete: t("steps.complete"), error: t("steps.error"),
65
+ }}
66
+ composeAccessibleName={({ position, total, label }) => t("steps.name", { position, total, label })}
67
+ />
68
+ ```
69
+
70
+ ```tsx
71
+ // Native
72
+ import { Steps } from "@hjmds/react-native/steps";
73
+
74
+ <Steps
75
+ descriptor={{
76
+ currentStepId: "team",
77
+ steps: [
78
+ { id: "welcome", label: t("onboarding.welcome") },
79
+ { id: "team", label: t("onboarding.team") },
80
+ { id: "alerts", label: t("onboarding.alerts") },
81
+ ],
82
+ }}
83
+ statusLabels={{
84
+ pending: t("steps.pending"), current: t("steps.current"),
85
+ complete: t("steps.complete"), error: t("steps.error"),
86
+ }}
87
+ composeAccessibleName={({ position, total, label }) => t("steps.name", { position, total, label })}
88
+ />
89
+ ```
90
+
91
+ 제품 데이터·콜백은 주입한다. 위 공개 API 지침에 Web·Native 차이를 유지한다.
92
+
93
+ ## 함정
94
+
95
+ - 저장 실패는 입력·단계 유지; 메모리 초안은 새로고침 복구를 보장하지 않음.
96
+ - 포인터·제스처만으로 기능을 숨기지 않는다. 키보드·단일 탭 경로와 취소 후 복귀도 검증한다.
@@ -0,0 +1,107 @@
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
+ 선택 내용을 검토하고 수정 후 명시적으로 확정 흐름이 필요할 때 쓴다. Form의 상태·콜백을 제품 로직에 연결하며 새 데이터 엔진을 만들지 않는다.
14
+
15
+ ## 구성 요소
16
+
17
+ | 컴포넌트 | 역할 | 지침 |
18
+ | --- | --- | --- |
19
+ | Form | 선택 내용을 검토하고 수정 후 명시적으로 확정 | [공개 계약](../components/form.md) |
20
+ | Button | 명시 행동·재시도 | [Button](../components/button.md) |
21
+
22
+ ## 배치
23
+
24
+ ```text
25
+ 부모 화면의 공개 슬롯
26
+ └─ 입력 → 요약 → 수정·저장
27
+ ```
28
+
29
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
30
+ | --- | --- | --- | --- |
31
+ | 바깥 틀 | Form 또는 포함 Surface | 부모 화면의 해당 슬롯 | [배치](../components/form.md#배치)에 따른다 |
32
+ | 내용 | Form 내부 슬롯 | 입력 → 요약 → 수정·저장 | spacing 토큰과 포함 컴포넌트 recipe |
33
+ | 행동 | Button 또는 공개 콜백 | 내용과 가까운 명시 진입점 | 주 행동 하나, 보조 행동과 구분 |
34
+
35
+ ## 흐름과 상태
36
+
37
+ 1. 선택 내용을 검토하고 수정 후 명시적으로 확정.
38
+ 2. 진행 상태와 제품의 실제 확정을 분리한다.
39
+ 3. 실패한 저장은 요약·입력 유지; 확인 전 자동 저장 금지.
40
+
41
+ | 상태 | 모습 | 포커스·알림 |
42
+ | --- | --- | --- |
43
+ | 기본 | 입력 → 요약 → 수정·저장 | 이름·선택 여부를 보조공학에 노출 |
44
+ | 진행 중 | 해당 작업 pending, 입력·기존 결과 보존 | 중복 요청 차단, 로딩에 포커스를 옮기지 않음 |
45
+ | 실패 | 실패한 저장은 요약·입력 유지; 확인 전 자동 저장 금지 | 오류 근처 재시도, 필요할 때만 오류 읽기 |
46
+
47
+ ## 코드 골격
48
+
49
+ ```tsx
50
+ // Web
51
+ import { useRef } from "react";
52
+ import { Form, TextField } from "@hjmds/react/forms";
53
+ import { Button } from "@hjmds/react/actions";
54
+
55
+ const nicknameInputRef = useRef<HTMLInputElement>(null);
56
+
57
+ <Form
58
+ aria-label={t("profile.formLabel")}
59
+ onSubmit={async () => {
60
+ setServerError(null);
61
+ if (nicknameError) { nicknameInputRef.current?.focus(); return; } // Web은 첫 오류 포커스를 제품이 한다
62
+ try { await save(values); } catch { setServerError(t("profile.saveFailed")); }
63
+ }}
64
+ formError={serverError}
65
+ actions={<Button type="submit">{t("profile.save")}</Button>}
66
+ >
67
+ <TextField ref={nicknameInputRef} label={t("profile.nickname")} value={values.nickname}
68
+ onValueChange={setNickname} error={nicknameError ? t(nicknameError) : undefined} />
69
+ </Form>
70
+ ```
71
+
72
+ ```tsx
73
+ // Native
74
+ import { useRef } from "react";
75
+ import type { TextInput } from "react-native";
76
+ import { Form } from "@hjmds/react-native/forms";
77
+ import { TextField } from "@hjmds/react-native/inputs";
78
+
79
+ const nicknameInputRef = useRef<TextInput>(null);
80
+
81
+ <Form
82
+ label={t("profile.formLabel")}
83
+ values={values}
84
+ onSubmit={save} // 거절되면 Form이 오류를 표시한다(메시지가 없으면 fallbackErrorMessage)
85
+ submitLabel={t("profile.save")}
86
+ fallbackErrorMessage={t("profile.saveFailed")}
87
+ {...(nicknameError ? { firstInvalidFieldRef: nicknameInputRef } : {})} // Native는 Form이 첫 오류로 포커스를 옮긴다
88
+ >
89
+ <TextField ref={nicknameInputRef} label={t("profile.nickname")} value={values.nickname}
90
+ onValueChange={setNickname} {...(nicknameError ? { error: t(nicknameError) } : {})} />
91
+ </Form>
92
+ ```
93
+
94
+ 제품 데이터·콜백은 주입한다. 위 공개 API 지침에 Web·Native 차이를 유지한다.
95
+
96
+ ## 플랫폼 차이
97
+
98
+ | 항목 | Web | Native |
99
+ | --- | --- | --- |
100
+ | 첫 오류 포커스 | 제품이 `onSubmit`에서 해당 입력 `ref.focus()` | `firstInvalidFieldRef`를 주면 Form이 그 입력에 포커스·보조공학 포커스를 옮기고 제출을 멈춘다 |
101
+ | 저장 실패 표시 | 제품이 `formError`로 넘긴다 | `onSubmit`이 거절되면 Form이 오류 메시지(없으면 `fallbackErrorMessage`)를 표시한다 |
102
+ | 제출 버튼 | `actions`에 `type="submit"` Button | `submitLabel`로 내장 버튼, `actions`로 교체 가능 |
103
+
104
+ ## 함정
105
+
106
+ - 실패한 저장은 요약·입력 유지; 확인 전 자동 저장 금지.
107
+ - 포인터·제스처만으로 기능을 숨기지 않는다. 키보드·단일 탭 경로와 취소 후 복귀도 검증한다.