@hjmds/design-contracts 1.26.1 → 1.27.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 (278) hide show
  1. package/dist/agreement.d.ts +1 -0
  2. package/dist/agreement.d.ts.map +1 -1
  3. package/dist/agreement.js +2 -1
  4. package/dist/agreement.js.map +1 -1
  5. package/dist/auth-screen.d.ts +1 -1
  6. package/dist/behaviors.d.ts +1 -1
  7. package/dist/calendar.d.ts +1 -0
  8. package/dist/calendar.d.ts.map +1 -1
  9. package/dist/calendar.js +4 -1
  10. package/dist/calendar.js.map +1 -1
  11. package/dist/catalog.d.ts +78 -30
  12. package/dist/catalog.d.ts.map +1 -1
  13. package/dist/catalog.js +10 -5
  14. package/dist/catalog.js.map +1 -1
  15. package/dist/chart.d.ts +2 -0
  16. package/dist/chart.d.ts.map +1 -1
  17. package/dist/chart.js +1 -1
  18. package/dist/chart.js.map +1 -1
  19. package/dist/component-definitions.d.ts +1 -0
  20. package/dist/component-definitions.d.ts.map +1 -1
  21. package/dist/component-definitions.js +1 -0
  22. package/dist/component-definitions.js.map +1 -1
  23. package/dist/component-recipes.d.ts +53 -6
  24. package/dist/component-recipes.d.ts.map +1 -1
  25. package/dist/component-recipes.js +64 -10
  26. package/dist/component-recipes.js.map +1 -1
  27. package/dist/data-table.d.ts +11 -0
  28. package/dist/data-table.d.ts.map +1 -1
  29. package/dist/data-table.js +5 -0
  30. package/dist/data-table.js.map +1 -1
  31. package/dist/description-list.d.ts +3 -0
  32. package/dist/description-list.d.ts.map +1 -1
  33. package/dist/description-list.js +5 -1
  34. package/dist/description-list.js.map +1 -1
  35. package/dist/design-profile-layout.d.ts +28 -0
  36. package/dist/design-profile-layout.d.ts.map +1 -1
  37. package/dist/design-profile-layout.js +27 -3
  38. package/dist/design-profile-layout.js.map +1 -1
  39. package/dist/design-profile.d.ts.map +1 -1
  40. package/dist/design-profile.js +40 -8
  41. package/dist/design-profile.js.map +1 -1
  42. package/dist/field-group.d.ts +13 -0
  43. package/dist/field-group.d.ts.map +1 -1
  44. package/dist/field-group.js +17 -2
  45. package/dist/field-group.js.map +1 -1
  46. package/dist/foundations.d.ts +24 -11
  47. package/dist/foundations.d.ts.map +1 -1
  48. package/dist/foundations.js +40 -7
  49. package/dist/foundations.js.map +1 -1
  50. package/dist/heading.d.ts +5 -5
  51. package/dist/image-choice.d.ts +23 -0
  52. package/dist/image-choice.d.ts.map +1 -0
  53. package/dist/image-choice.js +26 -0
  54. package/dist/image-choice.js.map +1 -0
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/masonry.d.ts +8 -0
  60. package/dist/masonry.d.ts.map +1 -1
  61. package/dist/masonry.js +12 -1
  62. package/dist/masonry.js.map +1 -1
  63. package/dist/number-field.d.ts +2 -2
  64. package/dist/number-field.d.ts.map +1 -1
  65. package/dist/number-field.js +4 -2
  66. package/dist/number-field.js.map +1 -1
  67. package/dist/qr-code-recipe.d.ts +1 -0
  68. package/dist/qr-code-recipe.d.ts.map +1 -1
  69. package/dist/qr-code-recipe.js +3 -1
  70. package/dist/qr-code-recipe.js.map +1 -1
  71. package/dist/recipes.d.ts +1 -1
  72. package/dist/recipes.d.ts.map +1 -1
  73. package/dist/recipes.js +1 -1
  74. package/dist/recipes.js.map +1 -1
  75. package/dist/result.d.ts +9 -0
  76. package/dist/result.d.ts.map +1 -1
  77. package/dist/result.js +5 -0
  78. package/dist/result.js.map +1 -1
  79. package/dist/screen-patterns.d.ts +139 -2
  80. package/dist/screen-patterns.d.ts.map +1 -1
  81. package/dist/screen-patterns.js +140 -1
  82. package/dist/screen-patterns.js.map +1 -1
  83. package/dist/sheet.d.ts +15 -1
  84. package/dist/sheet.d.ts.map +1 -1
  85. package/dist/sheet.js +20 -1
  86. package/dist/sheet.js.map +1 -1
  87. package/dist/showcase.d.ts +27 -27
  88. package/dist/showcase.js +27 -27
  89. package/dist/showcase.js.map +1 -1
  90. package/dist/tag.d.ts +10 -2
  91. package/dist/tag.d.ts.map +1 -1
  92. package/dist/tag.js +3 -2
  93. package/dist/tag.js.map +1 -1
  94. package/dist/tags-input.d.ts +6 -0
  95. package/dist/tags-input.d.ts.map +1 -1
  96. package/dist/tags-input.js +3 -1
  97. package/dist/tags-input.js.map +1 -1
  98. package/dist/text-formats.d.ts +2 -2
  99. package/dist/top.d.ts +2 -2
  100. package/dist/transfer-list.d.ts +1 -1
  101. package/dist/transfer-list.d.ts.map +1 -1
  102. package/dist/transfer-list.js +2 -1
  103. package/dist/transfer-list.js.map +1 -1
  104. package/dist/upload-item.d.ts +1 -10
  105. package/dist/upload-item.d.ts.map +1 -1
  106. package/dist/upload-item.js +1 -2
  107. package/dist/upload-item.js.map +1 -1
  108. package/dist/version.d.ts +1 -1
  109. package/dist/version.js +1 -1
  110. package/dist/version.js.map +1 -1
  111. package/docs/activity-chip.md +4 -3
  112. package/docs/catalog-freeze.json +5 -2
  113. package/docs/chart.md +1 -1
  114. package/docs/consumer-policy.md +13 -6
  115. package/docs/description-list.md +4 -0
  116. package/docs/design-profile.md +28 -2
  117. package/docs/font-roles.md +1 -1
  118. package/docs/generated/component-maturity.md +5 -4
  119. package/docs/generated/renderer-evidence.json +92 -6
  120. package/docs/generated/renderer-evidence.md +5 -3
  121. package/docs/generated/showcase-manifest.json +49 -1
  122. package/docs/gravity-letters.md +7 -2
  123. package/docs/masonry.md +4 -2
  124. package/docs/screen-patterns.md +5 -2
  125. package/docs/showcase.md +7 -9
  126. package/docs/stable-promotion.md +13 -1
  127. package/docs/usage/README.md +40 -10
  128. package/docs/usage/SELECTION.md +30 -4
  129. package/docs/usage/STANDARD.md +11 -2
  130. package/docs/usage/components/account-switcher.md +98 -0
  131. package/docs/usage/components/activity-chip.md +4 -4
  132. package/docs/usage/components/badge.md +1 -1
  133. package/docs/usage/components/bezier-editor.md +99 -0
  134. package/docs/usage/components/bottom-cta.md +2 -1
  135. package/docs/usage/components/button.md +26 -12
  136. package/docs/usage/components/carousel.md +4 -4
  137. package/docs/usage/components/chart.md +1 -0
  138. package/docs/usage/components/chat-message.md +18 -12
  139. package/docs/usage/components/chat-screen.md +6 -3
  140. package/docs/usage/components/cohort-retention-table.md +83 -0
  141. package/docs/usage/components/collapsible.md +7 -2
  142. package/docs/usage/components/comment-thread-row.md +102 -0
  143. package/docs/usage/components/comment-thread-screen.md +27 -13
  144. package/docs/usage/components/composition-layout.md +1 -0
  145. package/docs/usage/components/context-menu.md +32 -15
  146. package/docs/usage/components/conversation-row.md +11 -4
  147. package/docs/usage/components/conversion-funnel.md +85 -0
  148. package/docs/usage/components/correlation-matrix.md +80 -0
  149. package/docs/usage/components/data-table.md +4 -0
  150. package/docs/usage/components/description-list.md +2 -1
  151. package/docs/usage/components/editor-screen.md +9 -5
  152. package/docs/usage/components/effect-surface.md +4 -2
  153. package/docs/usage/components/empty-state.md +2 -2
  154. package/docs/usage/components/event-attendance-card.md +104 -0
  155. package/docs/usage/components/expandable-search.md +105 -0
  156. package/docs/usage/components/file-picker.md +5 -0
  157. package/docs/usage/components/filter-recovery.md +97 -0
  158. package/docs/usage/components/follow-profile-card.md +100 -0
  159. package/docs/usage/components/heading.md +6 -2
  160. package/docs/usage/components/histogram-explorer.md +89 -0
  161. package/docs/usage/components/icon-button.md +18 -2
  162. package/docs/usage/components/icon-tile.md +12 -4
  163. package/docs/usage/components/image-choice-group.md +81 -0
  164. package/docs/usage/components/import-field-mapping.md +94 -0
  165. package/docs/usage/components/inline-edit.md +105 -0
  166. package/docs/usage/components/keyboard-avoidance-context.md +60 -0
  167. package/docs/usage/components/keyboard-avoiding.md +5 -1
  168. package/docs/usage/components/list-detail-screen.md +4 -4
  169. package/docs/usage/components/list.md +1 -1
  170. package/docs/usage/components/masonry.md +6 -7
  171. package/docs/usage/components/media-selection-screen.md +7 -5
  172. package/docs/usage/components/media-transport.md +104 -0
  173. package/docs/usage/components/message-composer.md +13 -3
  174. package/docs/usage/components/moderation-screen.md +5 -2
  175. package/docs/usage/components/notification-inbox-screen.md +3 -3
  176. package/docs/usage/components/notification-row.md +64 -8
  177. package/docs/usage/components/onboarding-screen.md +1 -1
  178. package/docs/usage/components/overview-screen.md +9 -3
  179. package/docs/usage/components/pagination.md +18 -3
  180. package/docs/usage/components/password-rules-field.md +90 -0
  181. package/docs/usage/components/permission-screen.md +1 -1
  182. package/docs/usage/components/photo-source-sheet.md +1 -1
  183. package/docs/usage/components/popover.md +6 -3
  184. package/docs/usage/components/pricing-choice.md +85 -0
  185. package/docs/usage/components/profile-summary.md +56 -8
  186. package/docs/usage/components/progress.md +3 -3
  187. package/docs/usage/components/qr-code.md +5 -9
  188. package/docs/usage/components/repeatable-field-group.md +92 -0
  189. package/docs/usage/components/reply-composer.md +83 -0
  190. package/docs/usage/components/reply-message.md +85 -0
  191. package/docs/usage/components/result.md +3 -3
  192. package/docs/usage/components/saved-items-screen.md +7 -5
  193. package/docs/usage/components/screen-layout.md +21 -10
  194. package/docs/usage/components/search-field.md +1 -1
  195. package/docs/usage/components/section.md +2 -2
  196. package/docs/usage/components/selection-action-bar.md +89 -0
  197. package/docs/usage/components/settings-screen.md +13 -11
  198. package/docs/usage/components/sheet.md +19 -7
  199. package/docs/usage/components/shortcut-recorder.md +77 -0
  200. package/docs/usage/components/split-action.md +94 -0
  201. package/docs/usage/components/streaming-answer.md +94 -0
  202. package/docs/usage/components/switch.md +6 -2
  203. package/docs/usage/components/table-column-settings.md +88 -0
  204. package/docs/usage/components/target-summary.md +79 -0
  205. package/docs/usage/components/text.md +11 -5
  206. package/docs/usage/components/toggle-feedback-button.md +94 -0
  207. package/docs/usage/components/top-bar.md +8 -12
  208. package/docs/usage/components/transfer-task.md +87 -0
  209. package/docs/usage/compositions/action-recovery-optimistic.md +18 -14
  210. package/docs/usage/compositions/action-recovery-save.md +18 -14
  211. package/docs/usage/compositions/action-recovery-undo.md +18 -14
  212. package/docs/usage/compositions/activity-status.md +1 -1
  213. package/docs/usage/compositions/adaptive-content.md +2 -1
  214. package/docs/usage/compositions/collection-detail.md +6 -5
  215. package/docs/usage/compositions/collection-editing.md +13 -6
  216. package/docs/usage/compositions/command-records.md +5 -4
  217. package/docs/usage/compositions/communication.md +7 -5
  218. package/docs/usage/compositions/compound-controls.md +7 -3
  219. package/docs/usage/compositions/content-transition-comparison.md +10 -5
  220. package/docs/usage/compositions/data-layouts.md +12 -16
  221. package/docs/usage/compositions/data-workflows.md +7 -4
  222. package/docs/usage/compositions/date-time-selection.md +5 -5
  223. package/docs/usage/compositions/design-profile-comparison.md +22 -6
  224. package/docs/usage/compositions/disclosure.md +14 -9
  225. package/docs/usage/compositions/document-resource.md +1 -1
  226. package/docs/usage/compositions/environment-matrix.md +16 -12
  227. package/docs/usage/compositions/expo-interactions.md +9 -8
  228. package/docs/usage/compositions/family-drawer.md +9 -5
  229. package/docs/usage/compositions/field-group.md +7 -3
  230. package/docs/usage/compositions/interaction-adapters.md +11 -9
  231. package/docs/usage/compositions/interaction-flow-apply.md +17 -15
  232. package/docs/usage/compositions/interaction-flow-search.md +11 -7
  233. package/docs/usage/compositions/live-list.md +12 -8
  234. package/docs/usage/compositions/navigation-bar-collection.md +8 -3
  235. package/docs/usage/compositions/optional-motion.md +2 -2
  236. package/docs/usage/compositions/paper-surface.md +11 -6
  237. package/docs/usage/compositions/purpose-input-comment.md +10 -4
  238. package/docs/usage/compositions/purpose-input-message.md +10 -4
  239. package/docs/usage/compositions/reference-first.md +4 -4
  240. package/docs/usage/compositions/reference-review.md +4 -4
  241. package/docs/usage/compositions/reference-settings.md +4 -4
  242. package/docs/usage/compositions/selection-motion.md +2 -1
  243. package/docs/usage/compositions/social-compositions.md +7 -6
  244. package/docs/usage/compositions/stea-event-ticket.md +2 -4
  245. package/docs/usage/compositions/stea-order-progress.md +19 -21
  246. package/docs/usage/compositions/stea-pixel-empty.md +26 -26
  247. package/docs/usage/compositions/task-controls.md +7 -2
  248. package/docs/usage/compositions/texture-comparison.md +5 -5
  249. package/docs/usage/compositions/toast-layout.md +7 -3
  250. package/docs/usage/compositions/toggle-feedback.md +1 -1
  251. package/docs/usage/compositions/visual-foundations.md +10 -5
  252. package/docs/usage/compositions/web-additions.md +5 -1
  253. package/docs/usage/screens/common-chat.md +5 -3
  254. package/docs/usage/screens/common-comments.md +14 -9
  255. package/docs/usage/screens/common-conversation-list.md +109 -0
  256. package/docs/usage/screens/common-inbox.md +5 -3
  257. package/docs/usage/screens/common-login.md +7 -4
  258. package/docs/usage/screens/common-profile.md +16 -12
  259. package/docs/usage/screens/common-saved.md +12 -8
  260. package/docs/usage/screens/common-search.md +16 -8
  261. package/docs/usage/screens/common-settings.md +12 -11
  262. package/docs/usage/screens/common-shell.md +12 -10
  263. package/docs/usage/screens/dashboard.md +59 -56
  264. package/docs/usage/screens/discovery-gallery.md +18 -9
  265. package/docs/usage/screens/flow-collection.md +10 -6
  266. package/docs/usage/screens/flow-editor.md +9 -6
  267. package/docs/usage/screens/flow-media.md +12 -7
  268. package/docs/usage/screens/flow-moderation.md +9 -3
  269. package/docs/usage/screens/flow-onboarding.md +13 -8
  270. package/docs/usage/screens/flow-permission.md +5 -3
  271. package/docs/usage/screens/landing.md +20 -14
  272. package/docs/usage/screens/mockup-studio.md +100 -88
  273. package/docs/usage/screens/notification-settings.md +9 -7
  274. package/docs/usage/screens/product-bento.md +22 -10
  275. package/docs/usage/screens/reference-comparison.md +2 -2
  276. package/docs/usage/tokens/font-roles.md +18 -2
  277. package/docs/usage/tokens/typography.md +22 -11
  278. package/package.json +7 -1
@@ -0,0 +1,79 @@
1
+ # TargetSummary
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [데이터 구성 계약](../../data-workflows.md), `src/data-workflows.ts`의 `resolveTarget`, 양 renderer `src/data-workflows.tsx`
9
+ - 스토리북: `배포/구성/정보 표시/목표 달성 요약`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 실적과 목표를 받아 실적 수치, 달성 설명, 달성률 막대를 한 묶음으로 보여 준다. 달성률은 `actual / target`을 1에서
14
+ 멈춘 값이다. 목표가 0이면 비율의 분모가 없으므로 막대를 그리지 않고 100% 달성으로 꾸미지 않는다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 목표 없는 단일 수치 | [Statistic](statistic.md) |
21
+ | 작업 진행률(업로드·단계) | [Progress](progress.md), [TransferTask](transfer-task.md) |
22
+ | 여러 계열의 시간 추이 | [Chart](chart.md) |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `TargetSummary` | 실적·목표 달성 요약 | `@hjmds/react/data-workflows` | `@hjmds/react-native/data-workflows` |
29
+
30
+ `resolveTarget`은 `@hjmds/design-contracts/data-workflows`에서 가져온다.
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { TargetSummary } from "@hjmds/react/data-workflows";
37
+
38
+ <TargetSummary label={t("sales.monthly")} actual={actual} target={target}
39
+ formatActual={value => formatCurrency(value, locale)}
40
+ summaryLabel={s => s.ratio === null ? t("sales.noTarget") : t("sales.achieved", { percent: formatPercent(s.ratio) })} />
41
+ ```
42
+
43
+ ```tsx
44
+ // Native
45
+ import { TargetSummary } from "@hjmds/react-native/data-workflows";
46
+
47
+ <TargetSummary label={t("sales.monthly")} actual={actual} target={target}
48
+ formatActual={value => formatCurrency(value, locale)}
49
+ summaryLabel={s => s.ratio === null ? t("sales.noTarget") : t("sales.achieved", { percent: formatPercent(s.ratio) })} />
50
+ ```
51
+
52
+ ## 축과 기본값
53
+
54
+ | prop | 값 | 기본값 | 설명 |
55
+ | --- | --- | --- | --- |
56
+ | `label` | string | 필수 | 수치 라벨이자 막대 이름 |
57
+ | `actual` · `target` | number | 필수 | 유한한 0 이상 값. 아니면 `RangeError` |
58
+ | `formatActual` | `(actual) => string` | 필수 | 실적 표시 문자열 |
59
+ | `summaryLabel` | `(summary) => string` | 필수 | `summary`는 `{ actual, target, difference, ratio, progress }`. 목표 0이면 `ratio`·`progress`가 `null` |
60
+
61
+ ## 배치
62
+
63
+ | 항목 | 값 | 근거 |
64
+ | --- | --- | --- |
65
+ | 크기 | [Statistic](statistic.md) 기본(`comfortable`), [Progress](progress.md) 기본 `medium` 두께 8 | `data-workflows.tsx` |
66
+ | 간격 | 수치와 막대 사이 `spacing.sm` 12 | `Stack gap="sm"` |
67
+ | 순서·정렬 | 수치(라벨·값·`summaryLabel` hint) → 달성률 막대 | 양 renderer |
68
+ | 고정·스크롤 | 없음. 카드·대시보드 격자가 정한다 | — |
69
+ | 좁은 폭·큰 글자 | 막대는 부모 폭을 채우고 문구는 Statistic 규칙으로 줄바꿈 | Progress·Statistic 지침 |
70
+
71
+ ## 꼭 지킬 것
72
+
73
+ - `summaryLabel`은 `ratio === null`(목표 0)을 따로 처리한다. 이 경우 막대가 없다.
74
+ - 100%를 넘는 실적도 막대는 가득 찬 상태에서 멈춘다. 초과분은 `difference`로 문구에 적는다.
75
+ - 숫자·통화·백분율 형식은 제품 locale로 만든다.
76
+
77
+ ## 관련 지침
78
+
79
+ [데이터 해석과 가져오기](../compositions/data-workflows.md), [Statistic](statistic.md), [Progress](progress.md).
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-07
7
+ - 검토일: 2026-10-17
8
8
  - 근거: `src/component-recipes.ts`(`textRecipe`), [Gravity Letters](../../gravity-letters.md)
9
9
  - 스토리북: `배포/컴포넌트/글자와 아이콘/본문 글자`, `배포/컴포넌트/시각 효과/중력 글자`
10
10
 
@@ -61,13 +61,13 @@ import { Text } from "@hjmds/react-native/primitives";
61
61
  | --- | --- | --- | --- |
62
62
  | `variant` | `caption`(11) · `label`(12) · `body`(14) · `bodyLarge`(16) · `title`(18) · `titleLarge`(20) · `heading`(24) | `body` | 화면·섹션 제목은 모양이 아니라 [Heading](heading.md)으로 쓴다 |
63
63
  | `fontRole` | `ui` · `display` · `reading` · `code` | 제목 variant는 display, 본문은 reading, caption/label은 ui | 명시값이 기본을 덮는다. [역할 지침](../tokens/font-roles.md), 1.16.0 변경 |
64
- | `emphasis` | `regular` · `medium`(semibold) · `strong`(bold) | 프로필이 있으면 variant 굵기, 없으면 `regular` | 명시값이 프로필과 variant보다 우선 |
64
+ | `emphasis` | `regular` · `medium`(semibold) · `strong`(bold) | 프로필이 있으면 variant 굵기, 없으면 `regular`. `label`은 프로필과 무관하게 variant 굵기 500 | 명시값이 프로필과 variant보다 우선 |
65
65
  | `tone` | `primary` · `body` · `muted` · `subtle` · `weak` · `brand` · `danger` · `inverse` | `primary` | — |
66
66
  | `as` | `span` · `p` · `div` · `strong` · `small` | `span` | Web만. 문단은 `as="p"` |
67
67
  | `align` | `TextStyle["textAlign"]` | 쓰기 방향의 시작 쪽 | Native만 |
68
68
  | `layoutStyle` | `HjmCompositionStyleProp` | — | 배치. Web·Native 모두 |
69
69
  | `GravityLetters` `glyphs` | `readonly string[]`(1~32개) | — (필수) | — |
70
- | `GravityLetters` `active`·`replayKey` | `boolean`·`string \| number` | `false`·`0` | `replayKey`가 바뀌면 다시 떨어진다. Web만 `layoutStyle`도 받는다 |
70
+ | `GravityLetters` `active`·`replayKey` | `boolean`·`string \| number` | `false`·`0` | `replayKey`가 바뀌면 다시 떨어진다. Web만 `layoutStyle`도 받는다. Native만 `level`(Heading 단계)을 받는다 — Web은 바깥 Heading의 글자 모양을 물려받는다 |
71
71
 
72
72
  ## 배치
73
73
 
@@ -87,7 +87,7 @@ Native는 Text의 역할을 해당 stack에, 입력/조작 host는 제품 프로
87
87
  - 배치(여백·폭·flex)는 `layoutStyle`로만 한다. 색·크기·굵기는 `style`/`className`으로 덮지 않는다.
88
88
  (Native Text의 `style`은 layout primitive라 deprecated 경고 대상에서 빠져 있을 뿐 권장 경로가 아니다.)
89
89
  - 색으로 의미를 바꿀 때는 `tone`만 쓴다(오류 문구 `danger`, 어두운 배경 위 `inverse`).
90
- - `GravityLetters`는 장식이며 접근성 트리에서 숨는다. 같은 문구의 정적 Text·Heading을 바깥에 함께 둔다.
90
+ - `GravityLetters`는 장식이며 접근성 트리에서 숨는다. 읽을 수 있는 같은 문구를 함께 두되 화면에 두 번 보이지 않게 한다: Web은 같은 Heading 안의 `VisuallyHidden`, Native는 `accessibilityRole="header"`·`accessibilityLabel`을 가진 감싸는 View.
91
91
  `glyphs`는 32개 이하, 빈 문자열·줄바꿈 없이 넘긴다(어기면 `RangeError`). `active` 기본은 `false`다.
92
92
 
93
93
  ## 플랫폼 차이
@@ -103,8 +103,14 @@ Native는 Text의 역할을 해당 stack에, 입력/조작 host는 제품 프로
103
103
  ## 함정
104
104
 
105
105
  - `emphasis`를 명시하면 `variant`의 굵기를 덮는다. `designProfile`이 있으면 생략한 `emphasis`는 해당 프로필의 `tokens.typography[variant].fontWeight`를 따른다. 프로필이 없으면 기존 기본값 `regular`를 유지하므로 `variant="title"`도 보통 굵기로 그려진다. 그 경우 굵은 제목은 `emphasis="strong"`으로 지정한다(두 renderer 동일).
106
+ - `variant="label"`은 예외로, 프로필이 없어도 생략한 `emphasis`가 `typography.label` 굵기 500(medium)으로 그려진다. 500은 `emphasis` 단계에 없으므로(`medium`은 600) label을 400으로 그리려면 `emphasis="regular"`를 명시한다.
107
+ Web은 이 기본 굵기를 inline `font-weight`로 주며 `data-emphasis`는 `regular`로 남는다. Native는 `fonts="pretendard"`일 때 `Pretendard-Medium` face로 그린다.
106
108
 
107
109
 
110
+ ## 근거
111
+
112
+ 2026-10-17 사용자 결정: 폼 라벨·섹션 부제목이 본문 대비 약해 `label` 기본 굵기를 500으로 정했다(본문 400과 제목 600 사이). 이전에는 토큰이 600이었지만 프로필 없는 Text는 400으로 그렸다. 두 renderer는 `resolveTextDefaultWeight`(`src/component-recipes.ts`) 하나로 기본 굵기를 고른다.
113
+
108
114
  ## StatusText
109
115
 
110
116
  Text와 같은 표현을 유지하면서 상태 변경 알림을 연결하는 공개 companion이다.
@@ -147,7 +153,7 @@ StatusText: 결과 문자열
147
153
 
148
154
  | prop | 기본값 | 의미 |
149
155
  | --- | --- | --- |
150
- | `children: string` | 필수 | 제품의 지역화된 상태 문구. 빈 문자열도 허용 |
156
+ | `children: string` | 필수 | 제품의 지역화된 상태 문구. 빈 문자열도 허용. Web은 빈 동안 live node를 유지한 채 레이아웃에서 빠져 부모 Stack gap을 만들지 않는다. Native는 비어 있으면 렌더하지 않는 조건부 결과로 둔다 |
151
157
  | `announcement` | `polite` | `none`·`polite`·`assertive`. 시각적 tone과 알림 우선순위는 독립 |
152
158
  | `announceOnMount` | `false` | Native iOS 옵션. 조건부로 새로 나타난 행동 결과를 처음에도 알림 |
153
159
  | 나머지 Text props·ref | Text와 같음 | Web role/live/atomic, Native live region은 StatusText가 소유 |
@@ -0,0 +1,94 @@
1
+ # ToggleFeedbackButton
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [선택 반응 계약](../../toggle-feedback.md), `src/toggle-feedback.ts`의 `resolveToggleFeedbackProposal`·`toggleFeedbackRecipe`, 양 renderer `src/toggle-feedback.tsx`
9
+ - 스토리북: `배포/구성/직접 조작과 모션/선택 반응 버튼`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 좋아요·저장·알림 받기처럼 켜고 끄는 선택에 반응 모션과 개수를 붙인다. 누르면 예상 상태(선택·개수)를 바로 보여 주고,
14
+ 제품이 실제 API 응답으로 확정한 `state`를 다시 넘긴다. 실패하면 확정 상태로 돌아가고 오류를 알린다.
15
+ 효과는 `ripple`(종) · `ribbon`(책갈피) · `burst`(하트) 세 가지이며 선택으로 바뀌는 순간에만 재생한다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 개수·모션 없는 켜기/끄기 | [Button](button.md)의 `selected`, [Switch](switch.md) |
22
+ | 설정 값 켜기/끄기 | [Switch](switch.md) |
23
+ | 팔로우 상태 기계 | [FollowProfileCard](follow-profile-card.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `ToggleFeedbackButton` | 선택 반응 버튼 + 개수 | `@hjmds/react/toggle-feedback` | `@hjmds/react-native/toggle-feedback` |
30
+
31
+ `ToggleFeedbackState`·`ToggleFeedbackEffect` 타입은 `@hjmds/design-contracts/toggle-feedback`에서 가져온다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { ToggleFeedbackButton } from "@hjmds/react/toggle-feedback";
38
+
39
+ <ToggleFeedbackButton entityId={post.id} effect="burst" locale={locale}
40
+ state={{ selected: post.liked, count: post.likeCount }} labels={likeLabels}
41
+ onSelectedChange={async selected => { const next = await setLike(post.id, selected); updatePost(next); }} />
42
+ ```
43
+
44
+ ```tsx
45
+ // Native
46
+ import { ToggleFeedbackButton } from "@hjmds/react-native/toggle-feedback";
47
+
48
+ <ToggleFeedbackButton entityId={post.id} effect="burst" locale={locale}
49
+ state={{ selected: post.liked, count: post.likeCount }} labels={likeLabels}
50
+ onSelectedChange={async selected => { const next = await setLike(post.id, selected); updatePost(next); }} />
51
+ ```
52
+
53
+ ## 축과 기본값
54
+
55
+ | prop | 값 | 기본값 | 설명 |
56
+ | --- | --- | --- | --- |
57
+ | `entityId` | string | 필수 | 안정적인 id. 비면 던지고, 바뀌면 진행 상태를 새로 시작한다 |
58
+ | `state` | `{ selected, count }` | 필수 | 확정 상태. `count`는 0 이상 정수, 선택인데 0이면 `RangeError` |
59
+ | `effect` | `"ripple"` · `"ribbon"` · `"burst"` | 필수 | 아이콘 모양과 반응 모션 |
60
+ | `locale` | string | 필수 | 개수 형식 |
61
+ | `labels` | `{ select, selected, pending, count, error }` | 필수 | 버튼 문구·진행 중 이름·개수 라벨·실패 문구 |
62
+ | `onSelectedChange` | `(selected) => void \| Promise<void>` | 필수 | 실제 API를 호출하고 확정 `state`를 갱신한 뒤 끝낸다 |
63
+ | `icon` · `renderIcon` | ReactNode · `({ selected, pending }) => ReactNode` | 기본 효과 아이콘 | 제품 아이콘. `renderIcon`이 우선 |
64
+ | `disabled` | boolean | false | 잠금 |
65
+ | `active` | boolean | true | false면 잠그고 모션·개수 애니메이션을 끈다(화면 밖 등) |
66
+
67
+ ## 배치
68
+
69
+ | 항목 | 값 | 근거 |
70
+ | --- | --- | --- |
71
+ | 크기 | 아이콘 24, 효과 영역 40(아이콘 둘레 −8 오프셋), 버튼은 `secondary` [Button](button.md) 기본 `medium`(높이 44) | `toggleFeedbackRecipe` `glyphSize`·`effectSize` |
72
+ | 간격 | 버튼과 개수 사이 `spacing.md` 16, 버튼 줄과 실패 문구 사이 `spacing.sm` 12 | `Stack gap="md"`, 바깥 `gap="sm"` |
73
+ | 순서·정렬 | [아이콘+문구 버튼] → 개수(`AnimatedStatistic` `compact`), 세로 가운데 맞춤. 실패 문구는 아래 가운데 | 양 renderer |
74
+ | 고정·스크롤 | 없음. 카드·행 안 행동 영역에 둔다 | — |
75
+ | 좁은 폭·큰 글자 | 버튼 줄이 `wrap`으로 줄바꿈 | `Stack axis="inline" wrap` |
76
+
77
+ ## 꼭 지킬 것
78
+
79
+ - 개수를 타이머·클라이언트 계산으로 확정하지 않는다. 진행 중 표시만 예상값이고 확정은 API 응답 `state`다.
80
+ - 모션은 비선택→선택 전환에서만 재생한다. 동작 줄이기, `active={false}`, 화면이 숨겨진 상태에서는 즉시 마지막 프레임으로 끝낸다.
81
+ - 이미 선택된 상태로 처음 그려지거나 환경이 바뀌어도 모션을 다시 재생하지 않는다.
82
+ - 문구·개수 라벨은 제품 i18n에서 만든다.
83
+
84
+ ## 플랫폼 차이
85
+
86
+ | 항목 | Web | Native |
87
+ | --- | --- | --- |
88
+ | 진행 중 이름 | `aria-label={labels.pending}` | `loadingLabel={labels.pending}` |
89
+ | 모션 | `requestAnimationFrame`, `visibilitychange`로 숨김 시 종료 | `Animated.timing`, `AppState`가 active가 아니면 종료 |
90
+ | 실패 알림 | `role="alert"` | `accessibilityRole="alert"`, `accessibilityLiveRegion="assertive"` |
91
+
92
+ ## 관련 지침
93
+
94
+ [선택 반응 버튼](../compositions/toggle-feedback.md), [Button](button.md), [Statistic](statistic.md).
@@ -4,13 +4,13 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-07
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [화면 제목과 마지막 행동](../../screen-chrome.md), [NavigationBar](../../navigation-bar.md), `src/component-recipes.ts`(`topBarRecipe`)
9
9
  - 스토리북: `배포/컴포넌트/탐색/상단 탐색 막대`, `배포/컴포넌트/탐색/검색·메뉴가 있는 상단 바`, `배포/컴포넌트/탐색/내비게이션 바`
10
10
 
11
11
  ## 언제 쓰나
12
12
 
13
- 화면 맨 위에 붙는 **크롬**에 쓴다. 뒤로가기·닫기(`leading`), 짧은 화면 이름(`title`), 소수의 행동
13
+ 화면 맨 위에 붙는 **크롬**에 쓴다. 뒤로가기·닫기(`exit`), 짧은 화면 이름(`title`), 소수의 행동
14
14
  (`actions`/`trailing`)을 담는다. 스크롤과 무관한 자리이며 safe area 위쪽 여백을 처리한다.
15
15
 
16
16
  ## 쓰지 않을 때
@@ -38,12 +38,11 @@ Top과 TopBar의 구분: TopBar는 고정 크롬(이동·화면 이름·액션),
38
38
 
39
39
  ```tsx
40
40
  // Web
41
- import { IconButton } from "@hjmds/react/actions";
42
41
  import { TopBar } from "@hjmds/react/top-bar";
43
42
 
44
43
  <TopBar
45
44
  title={t("settings.title")}
46
- leading={<IconButton label={t("common.back")} onClick={goBack}><BackIcon /></IconButton>}
45
+ exit={{ kind: "back", label: t("common.back"), onClick: goBack }}
47
46
  />
48
47
  ```
49
48
 
@@ -54,11 +53,7 @@ import { TopBar, TopBarAction } from "@hjmds/react-native/top-bar";
54
53
  <TopBar
55
54
  title={t("settings.title")}
56
55
  safeAreaTop={insets.top}
57
- leading={
58
- <TopBarAction label={t("common.back")} labelVisibility="accessibility-only" onPress={goBack}>
59
- <BackIcon />
60
- </TopBarAction>
61
- }
56
+ exit={{ kind: "back", label: t("common.back"), onPress: goBack }}
62
57
  actions={<TopBarAction label={t("common.save")} onPress={save}><SaveIcon /></TopBarAction>}
63
58
  />
64
59
  ```
@@ -67,14 +62,15 @@ import { TopBar, TopBarAction } from "@hjmds/react-native/top-bar";
67
62
 
68
63
  | prop | 값 | 기본값 | 설명 |
69
64
  | --- | --- | --- | --- |
70
- | `centered` | `boolean` | `true` | 남은 제목 영역 안에서의 정렬이며 좌우 열을 같은 폭으로 맞추지 않는다 |
71
65
  | `safeAreaTop` | 0 이상 유한수 | 0 | Web은 `env(safe-area-inset-top)`과 큰 값을 쓴다 |
72
66
  | `headingLevel` | 1~6 | 1 | Web만. 페이지 구조에 맞춘다 |
73
67
  | `onTitleClick`(Web) | `(event: MouseEvent<HTMLButtonElement>) => void` | — | 제목을 버튼으로 만든다 |
74
68
  | `onTitlePress`(Native) | `(event: GestureResponderEvent) => void` | — | 제목을 버튼으로 만든다 |
75
69
  | `subtitle` | `string` | — | 제목 아래 보조 문맥. 제목이 필요하며 배치·색·글자 규격은 HJM이 소유(1.24.0) |
76
70
  | `titleLeading` | `ReactNode` | — | 제목 앞 작은 시각 요소 |
77
- | `leading` / `actions`(별칭 `trailing`) | `ReactNode` | — | 둘 중 한 이름만 쓴다 |
71
+ | `centered` | `boolean` | `true` | 제목 가운데 정렬(기본). `false`면 왼쪽 정렬 |
72
+ | `exit` | `{ kind: "back" \| "close"; label; onClick(Web) \| onPress(Native); disabled? }` | — | 화면 나가기를 시작 쪽 아이콘 버튼으로 고정한다(글자 없음, `label`은 보조기술 이름). `leading`과 함께 주면 `TypeError`. [ScreenLayout `exit`](screen-layout.md)과 같은 버튼. 2026-10-17 추가, 미게시 |
73
+ | `leading` / `actions`(별칭 `trailing`) | `ReactNode` | — | `actions`·`trailing`은 둘 중 한 이름만 쓴다. 뒤로·닫기는 `leading`으로 만들지 않고 `exit`을 쓴다 |
78
74
  | `layoutStyle` | `HjmCompositionStyleProp` | — | 루트 배치. Web·Native 모두 |
79
75
  | Native `style`·`leadingStyle`·`titleStyle`·`trailingStyle` | 지원하지 않음 | — | 제거된 속성 — `layoutStyle` 또는 `centered`. 현재 공개 props에서 제거되었다 |
80
76
  | `TopBarAction` `intent` | `button` · `link` | `button` | `button`은 `onPress: (event) => void`, `link`는 `destination` + `onNavigate?: (destination: LinkDestination) => void \| Promise<void>` 또는 `renderLink` |
@@ -91,7 +87,7 @@ NavigationBar의 Native 프레임 모서리는 Provider의 `tokens.radius.xl`을
91
87
  | --- | --- | --- |
92
88
  | 크기 | 최소 높이 `control.buttonHeight.large` 52 + 위쪽 안전 영역. Web은 `max(env(safe-area-inset-top), safeAreaTop)`을 위 여백으로, Native는 `safeAreaTop`을 `paddingTop`과 `minHeight`(52 + `safeAreaTop`)에 더한다. 좌우 열 최소 폭 `control.minTouchTarget` 44, Native `TopBarAction` 최소 44×44 | `topBarRecipe.minHeight`·`sideMinWidth`·`action`, `.hjm-top-bar` |
93
89
  | 간격 | 좌우 여백 `spacing.md` 16, 슬롯 사이·슬롯 안 행동 사이 `spacing.xs` 8. `TopBarAction` 아이콘–라벨 `spacing.xxs` 4, 좌우 여백 4. 배경 `canvas` | `topBarRecipe.paddingHorizontal`·`gap`·`action` |
94
- | 순서·정렬 | 세 열: `leading`(뒤로·닫기) · 제목 · `actions`(끝 정렬). `centered`(기본)는 남은 제목 영역 안에서만 가운데 정렬. 행동은 1~2개만 끝 열에, 넘치면 하나만 남기고 [Menu](menu.md)로 묶는다. 주 행동(저장·완료)은 끝 열의 마지막 | `.hjm-top-bar` grid, Native `TopBar` |
90
+ | 순서·정렬 | 세 열: `leading`(뒤로·닫기) · 제목 · `actions`(끝 정렬). `centered`(기본 `true`)는 남은 제목 영역 안에서 가운데 정렬. 왼쪽 정렬은 `centered={false}`로 고른다. 행동은 1~2개만 끝 열에, 넘치면 하나만 남기고 [Menu](menu.md)로 묶는다. 주 행동(저장·완료)은 끝 열의 마지막 | `.hjm-top-bar` grid, Native `TopBar` |
95
91
  | 고정·스크롤 | 화면 맨 위, 스크롤 영역 **밖**에 둔다. TopBar 자체는 `position: sticky`/고정을 걸지 않으므로 화면 골격이 그 아래에 스크롤 영역을 둬야 고정된다. 바로 아래 본문 첫 블록은 [Top](top.md) | `.hjm-top-bar`(position 없음) |
96
92
  | 좁은 폭·큰 글자 | 글자 배율 ≥ `largeTextThreshold` 1.6: Web은 첫 행 leading·행동, 둘째 행 제목(시작 정렬, 위아래 `spacing.xs` 8). Native는 첫 행 leading+제목, 둘째 행 행동(끝 정렬, 줄바꿈 허용) | `.hjm-top-bar[data-large-text]`, `react-native/src/navigation.tsx` `TopBar` |
97
93
 
@@ -0,0 +1,87 @@
1
+ # TransferTask
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [실제 전송 계약](../../transfer-task.md), `src/transfer-task.ts`의 `resolveTransferObservation`·`isConfirmedTransferAbort`, 양 renderer `src/transfer-task.tsx`
9
+ - 스토리북: `배포/구성/피드백과 복구/전송 진행과 취소`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 업로드·다운로드·동기화처럼 실제 전송 계층이 진행량을 알려 주는 작업에 시작·취소·진행률·결과를 한 묶음으로 붙인다.
14
+ 진행률은 어댑터가 `report`로 보낸 실제 수치만 쓴다. 총량을 모르면 진행 중 표시만 하고 백분율을 만들지 않는다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 파일 고르기 | [FilePicker](file-picker.md) |
21
+ | 진행량 없는 짧은 요청 | [Button](button.md)의 `loading` |
22
+ | 스트림으로 받는 답 | [StreamingAnswer](streaming-answer.md) |
23
+ | 진행 막대만 필요할 때 | [Progress](progress.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `TransferTask` | 전송 시작·취소·진행 | `@hjmds/react/transfer-task` | `@hjmds/react-native/transfer-task` |
30
+
31
+ `TransferObservation`·`TransferContext` 타입은 `@hjmds/design-contracts/transfer-task`에서 가져온다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { TransferTask } from "@hjmds/react/transfer-task";
38
+
39
+ <TransferTask labels={uploadLabels}
40
+ onTransfer={({ signal, report }) => uploadFile(file, { signal, onProgress: (completed, total) => report({ completed, total }) })}
41
+ formatProgress={o => o.total === null ? formatBytes(o.completed) : t("upload.progress", { done: formatBytes(o.completed), total: formatBytes(o.total) })} />
42
+ ```
43
+
44
+ ```tsx
45
+ // Native
46
+ import { TransferTask } from "@hjmds/react-native/transfer-task";
47
+
48
+ <TransferTask labels={uploadLabels}
49
+ onTransfer={({ signal, report }) => uploadFile(file, { signal, onProgress: (completed, total) => report({ completed, total }) })}
50
+ formatProgress={o => o.total === null ? formatBytes(o.completed) : t("upload.progress", { done: formatBytes(o.completed), total: formatBytes(o.total) })} />
51
+ ```
52
+
53
+ ## 축과 기본값
54
+
55
+ | prop | 값 | 기본값 | 설명 |
56
+ | --- | --- | --- | --- |
57
+ | `onTransfer` | `({ signal, report }) => Promise<void>` | 필수 | 실제 전송. `report({ completed, total })`, 총량을 모르면 `total: null` |
58
+ | `labels` | `{ start, queued, transferring, cancel, cancelling, cancelled, success, error }` | 필수 | 버튼·상태 문구 |
59
+ | `formatProgress` | `(observation) => string` | 필수 | 진행 막대 값 문구 |
60
+ | `disabled` | boolean | false | 시작·취소를 잠근다 |
61
+
62
+ ## 배치
63
+
64
+ | 항목 | 값 | 근거 |
65
+ | --- | --- | --- |
66
+ | 크기 | 버튼은 [Button](button.md) 기본 `medium`(높이 44), 막대는 [Progress](progress.md) 기본 `medium` 두께 8, 부모 폭을 채운다 | `transfer-task.tsx` |
67
+ | 간격 | 버튼 줄·막대·상태 사이 `spacing.md` 16, 버튼 사이 `spacing.sm` 12 | `Stack gap="md"`, 버튼 줄 `gap="sm"` |
68
+ | 순서·정렬 | [시작] [취소(진행 중만)] → 진행 막대 → 결과 문구(가운데) | 양 renderer |
69
+ | 고정·스크롤 | 없음. 놓는 영역이 정한다 | — |
70
+ | 좁은 폭·큰 글자 | 버튼 줄이 `wrap`으로 줄바꿈 | `Stack axis="inline" wrap` |
71
+
72
+ ## 꼭 지킬 것
73
+
74
+ - `signal`을 전송 계층에 연결한다. 취소 요청만으로 취소 완료가 아니며, 어댑터가 `AbortError`로 거부해야 `cancelled`다.
75
+ - `completed`는 한 작업 안에서 줄어들 수 없고 `total`을 넘을 수 없다. 어기면 `RangeError`다.
76
+ - 총량을 나중에 알게 돼도 완료량을 꾸미지 않는다. 작업이 끝난 뒤의 `report`는 무시된다.
77
+ - 결과 문구는 `role="status"`, 실패는 경고로 알린다. 재시도는 같은 시작 버튼이다.
78
+
79
+ ## 플랫폼 차이
80
+
81
+ | 항목 | Web | Native |
82
+ | --- | --- | --- |
83
+ | 결과 알림 | `role="status"` / 실패 `role="alert"` | `accessibilityLiveRegion="polite"`, 실패 `accessibilityRole="alert"` |
84
+
85
+ ## 관련 지침
86
+
87
+ [전송 진행과 취소](../compositions/transfer-task.md), [Progress](progress.md), [FilePicker](file-picker.md).
@@ -46,7 +46,7 @@
46
46
  │ │ 제목 Heading level3 (선택) │ │
47
47
  │ │ ↕ spacing.md 16 │ │
48
48
  │ │ [ 북마크 추가 / 해제 (selected) ] │ │ ← 토글, 높이 44
49
- │ │ ↕ spacing.md 16 │ │
49
+ │ │ ↕ spacing.xs 8 (행동 묶음) │ │
50
50
  │ │ [ 북마크 다시 반영 (secondary) ] │ │ ← error일 때만
51
51
  │ │ ↕ spacing.md 16 │ │
52
52
  │ │ 상태 문구 (role=status) │ │
@@ -56,10 +56,10 @@
56
56
 
57
57
  | 영역 | 컴포넌트 | 위치 | 크기·간격 |
58
58
  | --- | --- | --- | --- |
59
- | 바깥 틀 | Web 문서 스크롤 > `Container`. Native `ScrollView` > `Container` | 이 구성을 감싸는 제품 화면(대상 콘텐츠 상세 등). 상단 안전 영역은 내비게이션 헤더(또는 [TopBar](../components/top-bar.md))가 맡는다 | 좌우 `Container gutter`: 폭 600 미만 `compact` 16, 이상 `regular` 20([화면 여백](../tokens/layout.md)). Native 위아래는 `contentContainerStyle` `paddingVertical: spacing.lg` 20 |
59
+ | 바깥 틀 | Web 문서 스크롤 > `Container size="reading"`. Native `ScrollView` > `Container size="reading"` | 이 구성을 감싸는 제품 화면(대상 콘텐츠 상세 등). 넓은 화면에서도 버튼이 읽기 폭을 넘지 않는다. 상단 안전 영역은 내비게이션 헤더(또는 [TopBar](../components/top-bar.md))가 맡는다 | 좌우 `Container gutter`: 폭 600 미만 `compact` 16, 이상 `regular` 20([화면 여백](../tokens/layout.md)). Native 위아래는 `contentContainerStyle` `paddingVertical: spacing.lg` 20 |
60
60
  | 제목(선택) | `Heading level="level3"` | Stack 맨 위 | 아래 `spacing.md` 16 |
61
- | 토글 | `Button selected` | 대상 콘텐츠 아래, 스크롤 | `medium` 44(`control.buttonHeight`), 이웃 `spacing.md` 16. Stack 기본 `align="stretch"`로 꽉 찬 폭 |
62
- | 재시도 | `Button tone="secondary"` | 토글 아래, error일 때만 | `medium` 44, `spacing.md` 16. 라벨은 무엇을 다시 하는지 적는다(`t("bookmark.retry")`) |
61
+ | 토글 | `Button selected` | 대상 콘텐츠 아래, 스크롤 | `medium` 44(`control.buttonHeight`), 위 블록과 `spacing.md` 16. 재시도와 함께 `Stack gap="xs"` 행동 묶음에 둔다. Stack 기본 `align="stretch"`로 꽉 찬 폭 |
62
+ | 재시도 | `Button tone="secondary"` | 토글 아래, error일 때만 | `medium` 44, 토글과 `spacing.xs` 8(`actionGroupRecipe`). 라벨은 무엇을 다시 하는지 적는다(`t("bookmark.retry")`) |
63
63
  | 상태 | `Text` | 행동 아래 | `spacing.md` 16 |
64
64
 
65
65
  - 토글이 목록 행 끝에 들어가면 Button 지침대로 `size="small"`(36, `control.buttonHitSlop.small` 4로 터치 44)과 `secondary`·`ghost`를 쓴다.
@@ -116,12 +116,14 @@ function BookmarkToggle({ initial, setBookmark }: { initial: boolean; setBookmar
116
116
  };
117
117
  const gutter = resolveWindowClass(window.innerWidth) === "compact" ? "compact" : "regular";
118
118
  return (
119
- <Container gutter={gutter}>
119
+ <Container size="reading" gutter={gutter}>
120
120
  <Stack gap="md">
121
- <Button selected={state.value} disabled={state.status === "pending"} onClick={toggle}>
122
- {state.value ? t("bookmark.remove") : t("bookmark.add")}
123
- </Button>
124
- {state.status === "error" && <Button tone="secondary" onClick={() => void session.retry()}>{t("bookmark.retry")}</Button>}
121
+ <Stack gap="xs">
122
+ <Button selected={state.value} disabled={state.status === "pending"} onClick={toggle}>
123
+ {state.value ? t("bookmark.remove") : t("bookmark.add")}
124
+ </Button>
125
+ {state.status === "error" && <Button tone="secondary" onClick={() => void session.retry()}>{t("bookmark.retry")}</Button>}
126
+ </Stack>
125
127
  <Text as="p" role="status">{t(bookmarkStatusKey[state.status])}</Text>
126
128
  </Stack>
127
129
  </Container>
@@ -152,12 +154,14 @@ function BookmarkToggle({ initial, setBookmark }: { initial: boolean; setBookmar
152
154
  };
153
155
  return (
154
156
  <ScrollView contentContainerStyle={{ paddingVertical: spacing.lg }}>
155
- <Container gutter={resolveWindowClass(width) === "compact" ? "compact" : "regular"}>
157
+ <Container size="reading" gutter={resolveWindowClass(width) === "compact" ? "compact" : "regular"}>
156
158
  <Stack gap="md">
157
- <Button selected={state.value} disabled={state.status === "pending"} onPress={toggle}>
158
- {state.value ? t("bookmark.remove") : t("bookmark.add")}
159
- </Button>
160
- {state.status === "error" && <Button tone="secondary" onPress={() => void session.retry()}>{t("bookmark.retry")}</Button>}
159
+ <Stack gap="xs">
160
+ <Button selected={state.value} disabled={state.status === "pending"} onPress={toggle}>
161
+ {state.value ? t("bookmark.remove") : t("bookmark.add")}
162
+ </Button>
163
+ {state.status === "error" && <Button tone="secondary" onPress={() => void session.retry()}>{t("bookmark.retry")}</Button>}
164
+ </Stack>
161
165
  <StatusText>{t(bookmarkStatusKey[state.status])}</StatusText>
162
166
  </Stack>
163
167
  </Container>
@@ -50,7 +50,7 @@
50
50
  │ │ [ 입력 ] │ │ TextField, 높이 44
51
51
  │ │ ↕ spacing.md 16 │ │
52
52
  │ │ [ 저장 (primary) ] │ │ ← 주 행동, 높이 44
53
- │ │ ↕ spacing.md 16 │ │
53
+ │ │ ↕ spacing.xs 8 (행동 묶음) │ │
54
54
  │ │ [ 실패한 내용 다시 저장 (secondary)] │ │ ← error일 때만
55
55
  │ │ ↕ spacing.md 16 │ │
56
56
  │ │ 상태 문구 (role=status) │ │
@@ -66,12 +66,12 @@
66
66
  | 바깥 틀 | Web 문서 스크롤 > `Container size="reading"`. Native `ScrollView`(`keyboardShouldPersistTaps="handled"`) > `Container size="reading"` | 이 구성을 감싸는 제품 화면. 상단 안전 영역은 내비게이션 헤더(또는 [TopBar](../components/top-bar.md))가 맡는다 | 좌우 `Container gutter`: 폭 600 미만 `compact` 16, 이상 `regular` 20([화면 여백](../tokens/layout.md)). Native 위아래는 `contentContainerStyle` `paddingVertical: spacing.lg` 20 |
67
67
  | 제목 | `Heading level="level3"` | Stack 맨 위, 스크롤 | 아래 `spacing.md` 16 |
68
68
  | 입력 | `TextField` | 제목 아래, 스크롤 | 높이 `control.fieldHeight` 44 |
69
- | 주 행동 | `Button` 저장(primary) | 입력 바로 아래, 스크롤 | `medium` 44(`control.buttonHeight`), 이웃 `spacing.md` 16. Stack 기본 `align="stretch"`로 꽉 찬 폭이 되므로 `fullWidth`·`layoutStyle`은 필요 없다 |
70
- | 재시도 | `Button tone="secondary"` 다시 저장 | 주 행동 아래, error일 때만 | `medium` 44, `spacing.md` 16. 라벨은 무엇을 다시 하는지 적는다(스토리 "실패한 내용 다시 저장"). 일반 "다시 시도"만 쓰지 않는다 |
69
+ | 주 행동 | `Button` 저장(primary) | 입력 바로 아래, 스크롤 | `medium` 44(`control.buttonHeight`), 입력과 `spacing.md` 16. 재시도와 함께 `Stack gap="xs"` 행동 묶음에 둔다. Stack 기본 `align="stretch"`로 꽉 찬 폭이 되므로 `fullWidth`·`layoutStyle`은 필요 없다 |
70
+ | 재시도 | `Button tone="secondary"` 다시 저장 | 주 행동 아래, error일 때만 | `medium` 44, 주 행동과 `spacing.xs` 8(`actionGroupRecipe`). 라벨은 무엇을 다시 하는지 적는다(스토리 "실패한 내용 다시 저장"). 일반 "다시 시도"만 쓰지 않는다 |
71
71
  | 상태 | `Text` | 행동 아래 | `spacing.md` 16 |
72
72
  | 확정 값(선택) | `Text` — `state.value` | 상태 문구 아래 | `spacing.md` 16 |
73
73
 
74
- - 모든 간격은 `Stack gap="md"`(`spacing.md` 16) 하나로 맞춘다. `stackRecipe.defaults.gap`도 `md`다.
74
+ - 블록 사이 간격은 `Stack gap="md"`(`spacing.md` 16)로 맞춘다. `stackRecipe.defaults.gap`도 `md`다. 세로로 쌓인 저장·재시도 버튼만 `Stack gap="xs"`(`spacing.xs` 8) 행동 묶음으로 둔다([버튼 묶음 간격](../SELECTION.md)).
75
75
  - 스토리는 좁은 폭 기준 바깥 틀(Web `Container gutter="compact" size="reading"`, Native `ScrollView` > 같은 Container)을 그린다. 넓은 폭에서는 위 표의 첫 행대로 `regular`를 고른다.
76
76
  키보드가 입력·저장 버튼을 가리는 Native 폼은 입력이 여러 개면 [KeyboardFormScrollView](../components/keyboard-form-scroll-view.md)
77
77
  (peer 필요), peer가 없으면 [KeyboardAvoiding](../components/keyboard-avoiding.md) 기준을 따른다.
@@ -132,11 +132,13 @@ function SaveNote({ save }: { save(value: string): Promise<string> }) {
132
132
  <Stack gap="md">
133
133
  <Heading level="level3" semanticLevel={2}>{t("note.title")}</Heading>
134
134
  <TextField label={t("note.body")} value={draft} onValueChange={setDraft} />
135
- <Button loading={state.status === "pending"} disabled={!draft.trim()}
136
- onClick={() => { const submitted = draft; void session.run(() => save(submitted), { retryable: true }); }}>
137
- {t("common.save")}
138
- </Button>
139
- {state.status === "error" && <Button tone="secondary" onClick={() => void session.retry()}>{t("save.retry")}</Button>}
135
+ <Stack gap="xs">
136
+ <Button loading={state.status === "pending"} disabled={!draft.trim()}
137
+ onClick={() => { const submitted = draft; void session.run(() => save(submitted), { retryable: true }); }}>
138
+ {t("common.save")}
139
+ </Button>
140
+ {state.status === "error" && <Button tone="secondary" onClick={() => void session.retry()}>{t("save.retry")}</Button>}
141
+ </Stack>
140
142
  <Text as="p" role="status">{t(saveStatusKey[state.status])}</Text>
141
143
  {state.value && <Text as="p">{t("save.savedValue", { value: state.value })}</Text>}
142
144
  </Stack>
@@ -171,11 +173,13 @@ function SaveNote({ save }: { save(value: string): Promise<string> }) {
171
173
  <Stack gap="md">
172
174
  <Heading level="level3" semanticLevel={2}>{t("note.title")}</Heading>
173
175
  <TextField label={t("note.body")} value={draft} onValueChange={setDraft} />
174
- <Button loading={state.status === "pending"} disabled={!draft.trim()}
175
- onPress={() => { const submitted = draft; void session.run(() => save(submitted), { retryable: true }); }}>
176
- {t("common.save")}
177
- </Button>
178
- {state.status === "error" && <Button tone="secondary" onPress={() => void session.retry()}>{t("save.retry")}</Button>}
176
+ <Stack gap="xs">
177
+ <Button loading={state.status === "pending"} disabled={!draft.trim()}
178
+ onPress={() => { const submitted = draft; void session.run(() => save(submitted), { retryable: true }); }}>
179
+ {t("common.save")}
180
+ </Button>
181
+ {state.status === "error" && <Button tone="secondary" onPress={() => void session.retry()}>{t("save.retry")}</Button>}
182
+ </Stack>
179
183
  <StatusText>{t(saveStatusKey[state.status])}</StatusText>{/* 공개 Text companion */}
180
184
  {state.value ? <Text>{t("save.savedValue", { value: state.value })}</Text> : null}
181
185
  </Stack>
@@ -50,7 +50,7 @@ Toast의 `action`을 진입점으로 쓰면 토스트는 진입점만, 세션은
50
50
  │ │ 대상: 목록 / 보관함 + 항목 이름 │ │
51
51
  │ │ ↕ spacing.md 16 │ │
52
52
  │ │ [ 보관 ] → [ 실행 취소 ] │ │ ← 같은 자리, loading 중 스피너
53
- │ │ ↕ spacing.md 16 │ │
53
+ │ │ ↕ spacing.xs 8 (행동 묶음) │ │
54
54
  │ │ [ 실패한 작업 다시 시도 (secondary)] │ │ ← error일 때만
55
55
  │ │ ↕ spacing.md 16 │ │
56
56
  │ │ 상태 문구 (role=status) │ │
@@ -61,11 +61,11 @@ Toast의 `action`을 진입점으로 쓰면 토스트는 진입점만, 세션은
61
61
 
62
62
  | 영역 | 컴포넌트 | 위치 | 크기·간격 |
63
63
  | --- | --- | --- | --- |
64
- | 바깥 틀 | Web 문서 스크롤 > `Container`. Native `ScrollView` > `Container` | 이 구성을 감싸는 제품 화면. 상단 안전 영역은 내비게이션 헤더(또는 [TopBar](../components/top-bar.md))가 맡는다 | 좌우 `Container gutter`: 폭 600 미만 `compact` 16, 이상 `regular` 20([화면 여백](../tokens/layout.md)). Native 위아래는 `contentContainerStyle` `paddingVertical: spacing.lg` 20 |
64
+ | 바깥 틀 | Web 문서 스크롤 > `Container size="reading"`. Native `ScrollView` > `Container size="reading"` | 이 구성을 감싸는 제품 화면. 넓은 화면에서도 버튼이 읽기 폭을 넘지 않는다. 상단 안전 영역은 내비게이션 헤더(또는 [TopBar](../components/top-bar.md))가 맡는다 | 좌우 `Container gutter`: 폭 600 미만 `compact` 16, 이상 `regular` 20([화면 여백](../tokens/layout.md)). Native 위아래는 `contentContainerStyle` `paddingVertical: spacing.lg` 20 |
65
65
  | 제목(선택) | `Heading level="level3"` | Stack 맨 위 | 아래 `spacing.md` 16 |
66
66
  | 대상 표시 | `Text` | 행동 위, 스크롤 | 아래 `spacing.md` 16 |
67
- | 행동 | `Button` 보관/실행 취소(primary) | 대상 아래, 스크롤 | `medium` 44(`control.buttonHeight`). Stack 기본 `align="stretch"`로 꽉 찬 폭 |
68
- | 재시도 | `Button tone="secondary"` | 행동 아래, error일 때만 | `medium` 44, `spacing.md` 16. 라벨은 무엇을 다시 하는지 적는다(스토리 "실패한 작업 다시 시도") |
67
+ | 행동 | `Button` 보관/실행 취소(primary) | 대상 아래, 스크롤 | `medium` 44(`control.buttonHeight`). 재시도와 함께 `Stack gap="xs"` 행동 묶음에 둔다. Stack 기본 `align="stretch"`로 꽉 찬 폭 |
68
+ | 재시도 | `Button tone="secondary"` | 행동 아래, error일 때만 | `medium` 44, 행동과 `spacing.xs` 8(`actionGroupRecipe`). 라벨은 무엇을 다시 하는지 적는다(스토리 "실패한 작업 다시 시도") |
69
69
  | 상태 | `Text` | 맨 아래 | `spacing.md` 16 |
70
70
  | 토스트(선택) | `Toast` `action` | 화면 단위 오버레이 | action 높이 `control.minTouchTarget` 44 |
71
71
 
@@ -124,13 +124,15 @@ function ArchiveItem({ id, name, api }: { id: string; name: string; api: Archive
124
124
  };
125
125
  const gutter = resolveWindowClass(window.innerWidth) === "compact" ? "compact" : "regular";
126
126
  return (
127
- <Container gutter={gutter}>
127
+ <Container size="reading" gutter={gutter}>
128
128
  <Stack gap="md">
129
129
  <Text as="p">{state.value ? t("archive.inArchive", { name }) : t("archive.inList", { name })}</Text>
130
- <Button loading={state.status === "pending"} onClick={toggle}>
131
- {state.value ? t("archive.undo") : t("archive.action")}
132
- </Button>
133
- {state.status === "error" && <Button tone="secondary" onClick={() => void session.retry()}>{t("archive.retry")}</Button>}
130
+ <Stack gap="xs">
131
+ <Button loading={state.status === "pending"} onClick={toggle}>
132
+ {state.value ? t("archive.undo") : t("archive.action")}
133
+ </Button>
134
+ {state.status === "error" && <Button tone="secondary" onClick={() => void session.retry()}>{t("archive.retry")}</Button>}
135
+ </Stack>
134
136
  <Text as="p" role="status">{t(statusKeyOf(state))}</Text>
135
137
  </Stack>
136
138
  </Container>
@@ -165,13 +167,15 @@ function ArchiveItem({ id, name, api }: { id: string; name: string; api: Archive
165
167
  };
166
168
  return (
167
169
  <ScrollView contentContainerStyle={{ paddingVertical: spacing.lg }}>
168
- <Container gutter={resolveWindowClass(width) === "compact" ? "compact" : "regular"}>
170
+ <Container size="reading" gutter={resolveWindowClass(width) === "compact" ? "compact" : "regular"}>
169
171
  <Stack gap="md">
170
172
  <Text>{state.value ? t("archive.inArchive", { name }) : t("archive.inList", { name })}</Text>
171
- <Button loading={state.status === "pending"} onPress={toggle}>
172
- {state.value ? t("archive.undo") : t("archive.action")}
173
- </Button>
174
- {state.status === "error" && <Button tone="secondary" onPress={() => void session.retry()}>{t("archive.retry")}</Button>}
173
+ <Stack gap="xs">
174
+ <Button loading={state.status === "pending"} onPress={toggle}>
175
+ {state.value ? t("archive.undo") : t("archive.action")}
176
+ </Button>
177
+ {state.status === "error" && <Button tone="secondary" onPress={() => void session.retry()}>{t("archive.retry")}</Button>}
178
+ </Stack>
175
179
  <StatusText>{t(statusKeyOf(state))}</StatusText>
176
180
  </Stack>
177
181
  </Container>
@@ -163,7 +163,7 @@ function ReviewHeader({ task }: { task: ReviewTask | null }) {
163
163
  - `media`는 장식이다. 마스코트에 의미가 있으면 그 뜻을 `title`·`description`에 쓴다.
164
164
  - Native effect는 `useToastRegion()`의 안정적인 `publish` 함수에 의존한다. controller 객체 전체를 의존성에 넣으면
165
165
  publish로 controller가 갱신될 때마다 effect가 다시 발행하는 반복 렌더가 생긴다(실제 예제 host에서 재현).
166
- - 결과 보기의 예제 안내는 Web `role="status"`, 공개 `StatusText`로 전달한다.
166
+ - 결과 보기의 예제 안내는 Web·Native 모두 공개 `StatusText`로 전달한다(Web은 비어 있는 동안 레이아웃에서 빠져 간격을 만들지 않는다).
167
167
  Web은 패널이 실제 소유하던 초점 요소가 닫힘으로 제거됐을 때만 칩으로 돌아가고, 완료 칩 자체에 초점이 있을 때 자동 숨김이 일어나면 작업 시작 버튼으로 옮긴다.
168
168
  Enter로 결과를 선택해 초점 요소가 제거되면 복구하지만, 포인터 클릭이 버튼에 초점을 두지 않은 경우에는 새 초점을 강제하지 않는다.
169
169
  사용자가 외부 컨트롤로 이동하거나 명시적으로 해제한 초점은 유지한다. body 자체를 패널 초점 소유의 근거로 삼지 않는다. 이는 예제의 결과 안내이며 실제 결과 화면 이동은 제품 소유다.
@@ -32,7 +32,8 @@
32
32
 
33
33
  | 영역 | 컴포넌트 | 위치 | 크기·간격 |
34
34
  | --- | --- | --- | --- |
35
- | 바깥 틀 | Stack | 화면의 본문 흐름 | gap md=16px, 전체 폭 |
35
+ | 바깥 틀 | Stack. Web은 제품 화면의 `Container size="reading"` 안 | 화면의 본문 흐름 | gap md=16px, 폭은 Container를 따른다(Web 최대 `layout.readingMaxWidth` 720) |
36
+ | 패널 제목 | `Heading level="level5"` | 패널 Surface 맨 위 | `title` 글자 크기·semibold |
36
37
  | 내용 | 해당 공개 API | 입력과 행동 사이 | 자체 recipe 크기, 줄바꿈 허용 |
37
38
  | 행동 | Button/기존 컨트롤 | 관련 항목 바로 뒤 | inline일 때 wrap, md=16px |
38
39