@hjmds/design-contracts 1.26.0 → 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
@@ -1,28 +1,76 @@
1
1
  # ProfileSummary
2
2
 
3
- - 단계: 구성
4
- - 상태: 후보
3
+ - 단계: 컴포넌트
4
+ - 상태: 실험
5
5
  - 지원: Web · Native
6
- - 근거: [프로필 화면](../screens/common-profile.md)
6
+ - 적용: 1.26.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [프로필 화면](../screens/common-profile.md), `src/screen-patterns.ts`의 `profileSummaryRecipe`, 양 renderer `src/screen-flows.tsx`
9
+ - 스토리북: 별도 예제 미등록
7
10
 
8
11
  ## 언제 쓰나
9
- 프로필의 아바타·이름·배지·통계 수치·행동을 구성한다. 2026-10-15 제품별 프로필 헤더 복제를 없애기 위해 공통 구조를 추출했다.
12
+
13
+ 프로필의 아바타·이름·배지·통계 수치·행동을 구성한다. 2026-10-15 제품별 프로필 헤더 복제를 없애기 위해 공통 구조를 추출했다. [ProfileScreen](profile-screen.md)의 `summary` 슬롯에 넣는다.
14
+
15
+ ## 쓰지 않을 때
16
+
17
+ | 상황 | 대신 쓸 것 |
18
+ | --- | --- |
19
+ | 팔로우 상태 기계까지 필요한 카드 | [FollowProfileCard](follow-profile-card.md) |
20
+ | 프로필 화면 전체 틀 | [ProfileScreen](profile-screen.md) |
21
+ | 목록 안 사용자 한 행 | [ListRow](list-row.md) |
10
22
 
11
23
  ## 공개 이름과 import
12
- Web은 `@hjmds/react/screen-flows`, Native는 `@hjmds/react-native/screen-flows`의 `ProfileSummary`를 쓴다.
24
+
25
+ | 이름 | 역할 | Web | Native |
26
+ | --- | --- | --- | --- |
27
+ | `ProfileSummary` | 프로필 머리 요약 | `@hjmds/react/screen-flows` | `@hjmds/react-native/screen-flows` |
13
28
 
14
29
  ## 최소 사용 예
30
+
15
31
  ```tsx
16
32
  // Web
17
33
  import { ProfileSummary } from '@hjmds/react/screen-flows';
18
34
  <ProfileSummary name={name} avatar={avatar} stats={stats} actions={actions} />
19
35
  ```
36
+
20
37
  ```tsx
21
38
  // Native
22
39
  import { ProfileSummary } from '@hjmds/react-native/screen-flows';
23
40
  <ProfileSummary name={name} avatar={avatar} stats={stats} actions={actions} />
24
41
  ```
25
42
 
26
- ## 배치와 소유권
27
- HJM이 원형 72px 아바타 프레임, 이름, 수치, 행동의 순서와 간격을 소유한다. 제품은 `name`, `avatar`, 선택적 `badge`, `nameActions`, `actions`, 하단 `children`을 제공한다. `headingLevel` 기본값은 2다.
28
- `stats`는 `{id,value,label,onPress?,disabled?}` 목록이다. 각 수치의 독립 행동을 보존하며 조회·팔로우·인증·라우팅은 앱에서 연결한다. 팔로우 상태 기계까지 필요한 경우 기존 FollowProfileCard 사용 여부를 먼저 검토한다.
43
+ ## 축과 기본값
44
+
45
+ | prop | 값 | 기본값 | 설명 |
46
+ | --- | --- | --- | --- |
47
+ | name | string | 필수 | 제목(이름) |
48
+ | avatar | ReactNode | 필수 | 원형 72 프레임 안에 넣는 아바타 |
49
+ | headingLevel | 1~6 | 2 | 이름 제목 수준 |
50
+ | badge | ReactNode | 없음 | 이름 옆 배지 |
51
+ | nameActions | ReactNode | 없음 | 이름 행의 행동 |
52
+ | stats | `{id,value,label,onPress?,disabled?}[]` | `[]` | 통계 수치. `onPress`가 있으면 버튼 |
53
+ | actions | ReactNode | 없음 | 수치 아래 행동 줄 |
54
+ | children | ReactNode | 없음 | 하단 footer |
55
+
56
+ `stats`는 각 수치의 독립 행동을 보존하며 조회·팔로우·인증·라우팅은 앱에서 연결한다. 팔로우 상태 기계까지 필요한 경우 기존 FollowProfileCard 사용 여부를 먼저 검토한다.
57
+
58
+ ## 배치
59
+
60
+ | 항목 | 값 | 근거 |
61
+ | --- | --- | --- |
62
+ | 크기 | 원형 아바타 프레임 72 | `profileSummaryRecipe.avatarSize` |
63
+ | 간격 | 수치 칸 사이 `spacing.md` 16, 값·라벨 사이 `spacing.xxs` 4, 행동 사이 `spacing.sm` 12. 영역 간격은 [CompositionLayout](composition-layout.md) | 양 renderer |
64
+ | 순서·정렬 | 머리(아바타 → 이름·배지 → nameActions) → 수치 줄 → actions → children, 수치 칸은 균등 폭·가운데 | `CompositionLayout` + `flex: 1` 칸 |
65
+ | 고정·스크롤 | 없음. ProfileScreen 본문 스크롤 안에 놓인다 | — |
66
+ | 좁은 폭·큰 글자 | 수치 칸은 `minWidth: 0`으로 나눠 갖고, 행동 줄은 `wrap` | 양 renderer |
67
+
68
+ HJM이 원형 72px 아바타 프레임, 이름, 수치, 행동의 순서와 간격을 소유한다. 버튼 수치와 정적 수치는 같은 칸을 차지해 기준선이 어긋나지 않는다.
69
+
70
+ ## 꼭 지킬 것
71
+
72
+ 제품은 `name`, `avatar`, 선택적 `badge`, `nameActions`, `actions`, 하단 `children`을 제공한다. 같은 머리 배치를 별도 View/div로 다시 만들지 않는다. 수치 문구와 숫자 형식은 제품 i18n에서 만든다. 버튼 수치의 접근성 이름은 `값 라벨`로 조립된다.
73
+
74
+ ## 관련 지침
75
+
76
+ [프로필 화면](../screens/common-profile.md), [ProfileScreen](profile-screen.md), [CompositionLayout](composition-layout.md).
@@ -70,7 +70,7 @@ Native `ScrollProgress`는 제품 ScrollView의 `onScroll`(contentOffset.y)·`on
70
70
 
71
71
  | prop | 값 | 기본값 | 설명 |
72
72
  | --- | --- | --- | --- |
73
- | `label` | Web `ReactNode`(필수) · Native `string` | — | Native는 `label` 또는 `accessibilityLabel` 중 하나가 필수 |
73
+ | `label` | Web `ReactNode` · Native `string` | — | `label` 또는 이름 prop(Web `aria-label`, Native `accessibilityLabel`) 중 하나가 필수. 이름 prop만 주면 문구 줄(label·값)을 그리지 않는다 |
74
74
  | `max` | 양수 | `100` | 두 renderer 동일. 0–1 비율이면 `max={1}`을 함께 준다 |
75
75
  | `value` | `0`~`max` | — | 생략하면 불확정 진행 |
76
76
  | `size` | `small` · `medium` · `large` | `medium` | 두께 4 · 8 · 12, circular 지름 24 · 40 · 64 |
@@ -102,7 +102,7 @@ linear circular (Web) circular (Native)
102
102
 
103
103
  - `value`와 `max`는 같은 단위로 넘긴다. `value={0.64}`만 넘기면 0.64%다([단위 표](../../progress.md)).
104
104
  `max ≤ 0`, 범위 밖 `value`는 던진다.
105
- - 이름은 지역화한 `label`로 준다. Native는 보이는 label 없이 `accessibilityLabel`만 줄 수도 있다(`ScrollProgress`는 `label` 필수).
105
+ - 이름은 지역화한 `label`로 준다. 같은 문구를 옆 컴포넌트(예: TargetSummary의 Statistic)가 이미 보이면 보이는 label 없이 Web `aria-label`, Native `accessibilityLabel`만 준다(`ScrollProgress`는 `label` 필수).
106
106
  - 색은 `tone`, 두께는 `size`로만 바꾸고 배치는 `layoutStyle`로 한다. Native의 `style`·`labelStyle`·`valueStyle`·`trackStyle`·`indicatorStyle`은
107
107
  제거된 속성이다.
108
108
  - `ScrollProgress`는 window에 자동으로 붙지 않는다. Web은 스크롤 host를, Native는 측정값을 반드시 넘긴다.
@@ -112,7 +112,7 @@ linear circular (Web) circular (Native)
112
112
 
113
113
  | 항목 | Web | Native |
114
114
  | --- | --- | --- |
115
- | `label` 타입 | `ReactNode`, 필수 | `string`, 또는 `accessibilityLabel`만 |
115
+ | `label` 타입 | `ReactNode`, 또는 `aria-label`만 | `string`, 또는 `accessibilityLabel`만 |
116
116
  | 요소 | `<progress>` (ref 전달) | `accessibilityRole="progressbar"` View |
117
117
  | 보조 문구 | 없음 | `accessibilityHint` |
118
118
  | 스크롤 측정 | `useScrollMetrics(host)` | 제품이 ScrollView 이벤트로 계산 |
@@ -42,13 +42,11 @@ granular subpath로만 가져온다. 이 subpath는 optional peer를 import 한
42
42
  // Web
43
43
  import { Button } from "@hjmds/react/actions";
44
44
  import { QRCode } from "@hjmds/react/qr-code";
45
- import { Stack } from "@hjmds/react/layout";
46
- import { spacing } from "@hjmds/design-contracts/foundations";
47
45
 
48
46
  <QRCode
49
47
  value={inviteUrl}
50
48
  label={t("invite.qrLabel")}
51
- fallback={<Stack layoutStyle={{ marginTop: spacing.md }}><Button tone="secondary" onClick={copyLink}>{t("invite.copyLink")}</Button></Stack>}
49
+ fallback={<Button tone="secondary" onClick={copyLink}>{t("invite.copyLink")}</Button>}
52
50
  />
53
51
  ```
54
52
 
@@ -56,14 +54,12 @@ import { spacing } from "@hjmds/design-contracts/foundations";
56
54
  // Native
57
55
  import { Button } from "@hjmds/react-native/actions";
58
56
  import { QRCode } from "@hjmds/react-native/qr-code";
59
- import { Stack } from "@hjmds/react-native/primitives";
60
- import { spacing } from "@hjmds/design-contracts/foundations";
61
57
 
62
58
  <QRCode
63
59
  value={inviteUrl}
64
60
  label={t("invite.qrLabel")}
65
61
  size={224}
66
- fallback={<Stack layoutStyle={{ marginTop: spacing.md }}><Button tone="secondary" onPress={copyLink}>{t("invite.copyLink")}</Button></Stack>}
62
+ fallback={<Button tone="secondary" onPress={copyLink}>{t("invite.copyLink")}</Button>}
67
63
  />
68
64
  ```
69
65
 
@@ -85,8 +81,8 @@ import { spacing } from "@hjmds/design-contracts/foundations";
85
81
  | 항목 | 값 | 근거 |
86
82
  | --- | --- | --- |
87
83
  | 크기 | 기본 192×192. 실제 변은 (모듈 수 + 여백 4모듈×2)의 정수배로 내림돼 `size`보다 조금 작을 수 있다. 모듈당 최소 2px. 흰 여백 4모듈이 그림 안에 포함되므로 바깥에 흰 테두리를 또 두르지 않는다 | `qrCodeRecipe.quietZone`·`minModuleSize`, `qr-code.tsx` |
88
- | 간격 | QR과 `fallback` 사이 간격을 컴포넌트가 주지 않는다(Web CSS 없음, Native는 감싼 `View`만). `fallback` 슬롯 안의 [Stack](stack.md)에 `layoutStyle={{ marginTop: spacing.md }}`로 16을 준다. QRCode 바깥 Stack은 내부 두 요소의 간격을 바꿀 수 없다 | `packages/react/src/qr-code.tsx`, `packages/react-native/src/qr-code.tsx` |
89
- | 순서·정렬 | QR 위, `fallback` 바로 아래. 가운데 정렬은 감싸는 레이아웃이 한다 | 같은 파일 |
84
+ | 간격 | QR과 `fallback` 사이는 `qrCodeRecipe.fallbackGap` 12(`spacing.sm`)를 컴포넌트가 준다(2026-10-17. 그전에는 간격·정렬이 없어 Web에서 링크가 QR 오른쪽 아래에 붙었다) | `qrCodeRecipe`, `packages/react/src/qr-code.tsx`, `packages/react-native/src/qr-code.tsx` |
85
+ | 순서·정렬 | QR 위, `fallback` 아래. 루트가 세로로 쌓고 둘을 가운데 정렬한다 | 같은 파일 |
90
86
  | 고정·스크롤 | 고정되지 않는다. 카드·시트 본문 안에 둔다 | — |
91
87
  | 좁은 폭·큰 글자 | 좁은 폭에서 `size`를 화면 폭보다 크게 두지 않는다. 큰 글자에서도 QR 크기는 그대로이고 `fallback`만 커진다 | — |
92
88
 
@@ -95,7 +91,7 @@ import { spacing } from "@hjmds/design-contracts/foundations";
95
91
  │ ┌────────┐ │
96
92
  │ │ ▓▒▓▒▓▒ │ 192 │ ← 흰 여백 4모듈 포함
97
93
  │ └────────┘ │
98
- │ (fallback marginTop md) │
94
+ │ (fallbackGap 12) │
99
95
  │ [ 링크 복사 ] │ ← fallback (필수)
100
96
  └──────────────────────┘
101
97
  ```
@@ -0,0 +1,92 @@
1
+ # RepeatableFieldGroup
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [목록 편집과 복구 계약](../../collection-editing.md), `src/collection-editing.ts`의 `collectionEditingRecipe`·`focusAfterRemoval`, 양 renderer `src/collection-editing.tsx`
9
+ - 스토리북: `배포/구성/입력과 작성/반복 입력`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 참여자 이름·연락처처럼 같은 입력 행을 사용자가 추가·삭제하는 묶음에 쓴다. [FieldGroup](../compositions/field-group.md) 위에
14
+ 행별 삭제 버튼과 마지막 추가 버튼, 추가·삭제 뒤 포커스 이동을 얹는다. 행은 stable id로 묶어 입력 host를 유지한다.
15
+ 묶음 사용 기준은 [목록 편집과 복구](../compositions/collection-editing.md)다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 개수가 고정된 관련 입력 묶음 | [FieldGroup](../compositions/field-group.md) |
22
+ | 목록 항목 순서 바꾸기 | [SortableCollection](sortable-collection.md) |
23
+ | 자유 입력 태그 여러 개 | [TagsInput](tags-input.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `RepeatableFieldGroup` | 추가·삭제 가능한 입력 행 묶음 | `@hjmds/react/collection-editing` | `@hjmds/react-native/collection-editing` |
30
+
31
+ 루트 barrel에는 없다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { RepeatableFieldGroup } from "@hjmds/react/collection-editing";
38
+ import { TextField } from "@hjmds/react/forms";
39
+
40
+ <RepeatableFieldGroup descriptor={{ label: t("people.title"), fields: rows.map(row => ({ id: row.id, label: t("people.name") })) }}
41
+ addLabel={t("people.add")} removeLabel={id => t("people.remove", { name: nameOf(id) })} minFields={1} maxFields={4}
42
+ onAdd={() => { const id = createId(); setRows(r => [...r, { id, value: "" }]); return id; }}
43
+ onRemove={id => setRows(r => r.filter(row => row.id !== id))}
44
+ renderField={({ id, controlProps, guardChange, focusRef }) =>
45
+ <TextField {...controlProps} ref={focusRef} value={valueOf(id)} onValueChange={guardChange((v: string) => update(id, v))} />} />
46
+ ```
47
+
48
+ ```tsx
49
+ // Native
50
+ import { RepeatableFieldGroup } from "@hjmds/react-native/collection-editing";
51
+ import { TextField } from "@hjmds/react-native/inputs";
52
+
53
+ <RepeatableFieldGroup descriptor={descriptor} addLabel={t("people.add")}
54
+ removeLabel={id => t("people.remove", { name: nameOf(id) })} minFields={1} maxFields={4}
55
+ onAdd={addRow} onRemove={removeRow}
56
+ renderField={({ id, controlProps, guardChange, focusRef }) =>
57
+ <TextField {...controlProps} ref={focusRef} value={valueOf(id)} onValueChange={guardChange((v: string) => update(id, v))} />} />
58
+ ```
59
+
60
+ ## 축과 기본값
61
+
62
+ | prop | 값 | 기본값 | 설명 |
63
+ | --- | --- | --- | --- |
64
+ | `descriptor` | `FieldGroupDescriptor` | 필수 | 묶음 이름과 행(`fields`: id·label·disabled 등). FieldGroup과 같다 |
65
+ | `renderField` | `(binding & { focusRef }) => ReactNode` | 필수 | `controlProps`·`guardChange`는 FieldGroup binding, `focusRef`는 추가·삭제 뒤 포커스를 받을 입력에 연결 |
66
+ | `addLabel` | `string` | 필수 | 마지막 추가 버튼 |
67
+ | `removeLabel` | `(id) => string` | 필수 | 행마다 다른 삭제 이름(대상 이름 포함) |
68
+ | `minFields` / `maxFields` | `number` | `0` / `Infinity` | 이하면 삭제, 이상이면 추가 비활성 |
69
+ | `onAdd` | `() => string \| undefined` | 필수 | 새 행 id를 돌려주면 그 행이 descriptor에 나타난 뒤 포커스 |
70
+ | `onRemove` | `(id) => void` | 필수 | 제품 상태에서 행 제거 |
71
+ | `layout` · Web `className`·`layoutStyle` | FieldGroup 값 | FieldGroup 기본 | FieldGroup으로 전달 |
72
+
73
+ ## 배치
74
+
75
+ | 항목 | 값 | 근거 |
76
+ | --- | --- | --- |
77
+ | 크기 | 입력은 제품이 넣은 컨트롤, 버튼은 Button 기본 | 양 renderer |
78
+ | 간격 | 묶음과 추가 버튼 사이 `collectionEditingRecipe.gap`=`md`(16), 행 안 입력–삭제 `sm`(12) | `collectionEditingRecipe`, 양 renderer |
79
+ | 순서·정렬 | FieldGroup 제목 → 행마다 입력 → [삭제](ghost) → … → 마지막 [추가](secondary) | 렌더 순서 |
80
+ | 고정·스크롤 | 자체 고정·스크롤 없음, 바깥 폼·화면 host 소유 | 별도 scroller 없음 |
81
+ | 좁은 폭·큰 글자 | 행은 세로로 쌓여 폭이 좁아도 입력과 삭제가 겹치지 않는다 | 세로 Stack |
82
+
83
+ ## 꼭 지킬 것
84
+
85
+ - 행 id는 배열 index가 아니라 stable id다. index로 만들면 삭제 뒤 입력 host와 포커스가 다른 행으로 옮겨 간다.
86
+ - 입력 변경은 `guardChange`로 감싸 FieldGroup 편집 세션의 보호를 유지한다.
87
+ - `focusRef`를 연결하지 않으면 추가·삭제 뒤 포커스가 이동할 대상이 없다.
88
+
89
+ ## 상태와 상호작용
90
+
91
+ 삭제하면 `focusAfterRemoval`에 따라 다음 활성 행 → 이전 활성 행 → 추가 버튼 순서로 포커스를 옮긴다.
92
+ 추가는 `onAdd`가 돌려준 id의 입력이 등록된 뒤 포커스한다. descriptor가 `disabled`이면 추가·삭제 모두 잠근다. 언마운트 뒤 늦은 클릭은 무시한다.
@@ -0,0 +1,83 @@
1
+ # ReplyComposer
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [연결 계약](../../communication.md), `src/communication.ts`의 `validateReplyTarget`, 양 renderer `src/communication.tsx`(`MessageComposer` 래퍼)
9
+ - 스토리북: `배포/구성/입력과 작성/메시지 답장 연결`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 답장 대상(실제 메시지 id·저자·발췌)을 가진 작성창에서 실제 전송 Promise를 기다리고, 실패하면 초안을 보존해 다시 보낼 때 쓴다.
14
+ [MessageComposer](message-composer.md)를 그대로 렌더하며 `replyTo`·`pending`·`onSend`를 이 구성이 연결한다. 대상 선택은 [ReplyMessage](reply-message.md)가 맡는다.
15
+ 묶음 사용 기준은 [펼치는 검색과 실제 메시지 답장](../compositions/communication.md)이다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 답장·전송 진행 상태를 제품이 직접 관리하는 작성창 | [MessageComposer](message-composer.md) |
22
+ | 대화 화면 전체(헤더·타임라인·작성창) | [ChatScreen](chat-screen.md) |
23
+ | 댓글 작성 | [CommentThreadScreen](comment-thread-screen.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `ReplyComposer` | 답장 대상·실제 전송·실패 보존 작성창 | `@hjmds/react/communication` | `@hjmds/react-native/communication` |
30
+
31
+ 루트 barrel에는 없다. `ReplyTarget` 타입은 `@hjmds/design-contracts/communication`에 있다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { ReplyComposer } from "@hjmds/react/communication";
38
+
39
+ <ReplyComposer value={draft} onValueChange={setDraft} label={t("chat.input")} sendLabel={t("chat.send")}
40
+ replyTarget={replyTarget} onReplyCancel={() => setReplyTarget(null)}
41
+ cancelReplyLabel={t("chat.replyCancel")} errorLabel={t("chat.sendFailed")}
42
+ onSend={async (value, { replyToId }) => { await sendMessage({ value, replyToId }); setDraft(""); setReplyTarget(null); }} />
43
+ ```
44
+
45
+ ```tsx
46
+ // Native
47
+ import { ReplyComposer } from "@hjmds/react-native/communication";
48
+
49
+ <ReplyComposer value={draft} onValueChange={setDraft} label={t("chat.input")} sendLabel={t("chat.send")}
50
+ replyTarget={replyTarget} onReplyCancel={() => setReplyTarget(null)}
51
+ cancelReplyLabel={t("chat.replyCancel")} errorLabel={t("chat.sendFailed")}
52
+ onSend={sendConfirmed} />
53
+ ```
54
+
55
+ ## 축과 기본값
56
+
57
+ | prop | 값 | 기본값 | 설명 |
58
+ | --- | --- | --- | --- |
59
+ | `replyTarget` | `{ id, author, excerpt } \| null` | 필수 | `null`이면 답장 없음. id가 비어 있으면 `TypeError`(`validateReplyTarget`) |
60
+ | `onReplyCancel` / `cancelReplyLabel` | `() => void` / `string` | 필수 | MessageComposer `replyTo`의 취소 행동·이름 |
61
+ | `onSend` | `(value, { replyToId }) => void \| Promise<void>` | 필수 | 전송 시작 시점의 대상 id(없으면 `null`)를 넘긴다. 실제 전송 확인 뒤 resolve, 실패면 reject |
62
+ | `errorLabel` | `string` | 필수 | 실패 문구 |
63
+ | 나머지 | `MessageComposerProps` | — | `onSend`·`replyTo`·`pending`·`error`를 뺀 props(`value`·`onValueChange`·`label`·`sendLabel`·`maxLength`·`lengthFeedback`·`disabled` 등)를 그대로 전달 |
64
+
65
+ ## 배치
66
+
67
+ | 항목 | 값 | 근거 |
68
+ | --- | --- | --- |
69
+ | 크기 | MessageComposer와 같다 | [MessageComposer](message-composer.md) |
70
+ | 간격 | 작성창과 실패 문구 사이 `Stack gap="sm"`(12) | 양 renderer `ReplyComposer` |
71
+ | 순서·정렬 | 작성창(답장 대상이 있으면 입력창 위에 대상·취소) → 실패 문구(가운데 정렬) | 렌더 순서, MessageComposer `replyTo` |
72
+ | 고정·스크롤 | 자체 고정 없음. 키보드 위 고정은 ChatScreen·KeyboardDock host 소유 | 별도 scroller 없음 |
73
+ | 좁은 폭·큰 글자 | MessageComposer와 같다. 실패 문구는 줄바꿈된다 | 양 renderer |
74
+
75
+ ## 꼭 지킬 것
76
+
77
+ - 초안을 지우는 것은 제품이다. 이 구성은 controlled `value`를 비우지 않는다. 전송 확인 뒤 `setDraft("")`·대상 해제를 한다.
78
+ - 전송 중 대상이 바뀌어도 `onSend`에는 시작 시점의 `replyToId`가 간다. 늦은 응답 검증은 제품 상태 계층에서 한다.
79
+ - `pending`·`error`·`replyTo`를 직접 넘기지 않는다(타입에서 제외).
80
+
81
+ ## 상태와 상호작용
82
+
83
+ 전송 중에는 MessageComposer `pending`으로 중복 전송을 막는다. reject하면 초안과 답장 대상을 유지하고 실패 문구를 표시한다(Web `role="alert"`, Native `accessibilityRole="alert"`·assertive live region).
@@ -0,0 +1,85 @@
1
+ # ReplyMessage
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [연결 계약](../../communication.md), 양 renderer `src/communication.tsx`(`ChatMessage` 래퍼)
9
+ - 스토리북: `배포/구성/입력과 작성/메시지 답장 연결`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 대화 타임라인의 메시지에 실제 메시지 id를 묶은 답장 동작을 붙일 때 쓴다. [ChatMessage](chat-message.md)를 그대로 렌더하고,
14
+ `replyAction`을 `messageId`로 만들어 넣는다. 가로 스와이프 한 번의 소유자를 답장 또는 시각 공개 중 하나로 정한다.
15
+ 답장 대상을 받아 전송하는 쪽은 [ReplyComposer](reply-composer.md)다. 묶음 사용 기준은 [펼치는 검색과 실제 메시지 답장](../compositions/communication.md)이다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 답장 기능이 없는 메시지 | [ChatMessage](chat-message.md) |
22
+ | 답장 동작을 직접 조합해야 하는 메시지(반응 메뉴 항목 등) | [ChatMessage](chat-message.md)의 `replyAction` |
23
+ | 댓글의 답글 | [CommentThreadScreen](comment-thread-screen.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `ReplyMessage` | 실제 id를 가진 답장 가능 메시지 | `@hjmds/react/communication` | `@hjmds/react-native/communication` |
30
+
31
+ 루트 barrel에는 없다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { ReplyMessage } from "@hjmds/react/communication";
38
+
39
+ <ReplyMessage messageId={message.id} direction="incoming" author={message.authorName}
40
+ timestamp={formatTime(message.sentAt)} replyLabel={t("chat.reply")}
41
+ onReply={id => setReplyTarget({ id, author: message.authorName, excerpt: message.text })}>
42
+ {message.text}
43
+ </ReplyMessage>
44
+ ```
45
+
46
+ ```tsx
47
+ // Native
48
+ import { ReplyMessage } from "@hjmds/react-native/communication";
49
+
50
+ <ReplyMessage messageId={message.id} direction="incoming" author={message.authorName}
51
+ timestamp={formatTime(message.sentAt)} replyLabel={t("chat.reply")} onReply={selectReplyTarget}>
52
+ {message.text}
53
+ </ReplyMessage>
54
+ ```
55
+
56
+ ## 축과 기본값
57
+
58
+ | prop | 값 | 기본값 | 설명 |
59
+ | --- | --- | --- | --- |
60
+ | `messageId` | `string` | 필수 | 실제 메시지 id. 비어 있거나 공백뿐이면 렌더 중 `TypeError` |
61
+ | `onReply` | `(messageId) => void` | 필수 | 답장 대상으로 고른 id를 제품에 넘긴다 |
62
+ | `replyLabel` | `string` | 필수 | 답장 동작 이름(스와이프·접근성 동작·메뉴 항목) |
63
+ | `replyDisabled` | `boolean` | `false` | 답장 동작 비활성 |
64
+ | `swipeAction` | `"reply"` · `"timestamp"` | `"reply"` | `"timestamp"`면 ChatMessage `timestampPresentation="swipe"`, 아니면 `"always"`. 접근성 답장 동작은 두 모드 모두 남는다 |
65
+ | 나머지 | `ChatMessageProps` | — | `replyAction`·`timestampPresentation`을 뺀 ChatMessage props를 그대로 전달 |
66
+
67
+ ## 배치
68
+
69
+ | 항목 | 값 | 근거 |
70
+ | --- | --- | --- |
71
+ | 크기 | ChatMessage와 같다 | [ChatMessage 배치](chat-message.md#배치) |
72
+ | 간격 | ChatMessage와 같다(묶음 `groupGap` 12, 같은 묶음 `runGap` 6) | `chatMessageRecipe` |
73
+ | 순서·정렬 | ChatMessage와 같다(수신 시작 쪽, 발신 끝 쪽) | 래퍼는 배치를 바꾸지 않는다 |
74
+ | 고정·스크롤 | 자체 고정 없음, 타임라인 host가 스크롤 | ChatMessage |
75
+ | 좁은 폭·큰 글자 | ChatMessage와 같다 | ChatMessage |
76
+
77
+ ## 꼭 지킬 것
78
+
79
+ - `messageId`에 index나 임시 키를 넣지 않는다. 서버가 아는 실제 id여야 답장 연결이 맞는다.
80
+ - 답장 대상의 저자·발췌 보관은 제품 상태다. `onReply`는 id만 넘긴다.
81
+ - `swipeAction="timestamp"`이면 같은 스와이프로 답장하지 않는다. 사진 캐러셀처럼 가로 제스처가 있는 메시지는 ChatMessage 지침의 선택을 따른다.
82
+
83
+ ## 상태와 상호작용
84
+
85
+ 답장은 가로 스와이프, Web Alt+←/→, Native 접근성 동작 `reply`로 실행한다(ChatMessage 계약). 길게 누른 반응 메뉴가 있으면 첫 항목으로도 보인다.
@@ -69,8 +69,8 @@ import { Result } from "@hjmds/react-native/feedback";
69
69
  | `title` | `string` | 필수 | 현지화 |
70
70
  | `description` | `string` | — | — |
71
71
  | `actions` | 0~2개 `readonly { label: string; onAction(): void; accessibilityLabel?: string }[]` | — | 첫째가 primary Button, 둘째가 secondary Button. `accessibilityLabel` 생략 시 `label`. 3개 이상이면 `RangeError` |
72
- | `icon`(Web) | `ReactNode` | — | 제품 glyph. 의미는 title·status가 이미 전한다 |
73
- | `renderIcon`(Native) | `(props: { status, color: string, backgroundColor: string }) => ReactNode` | — | 받은 `color`로 그린다 |
72
+ | `icon`(Web) | `ReactNode` | `resultRecipe.glyphs[status]` | 제품 glyph. 생략하면 상태 표식(✓ · ! · i)을 그린다. 의미는 title·status가 이미 전한다 |
73
+ | `renderIcon`(Native) | `(props: { status, color: string, backgroundColor: string }) => ReactNode` | `resultRecipe.glyphs[status]` | 받은 `color`로 그린다. 생략하면 Web과 같은 상태 표식을 그린다 |
74
74
  | `headingLevel`(Web) | `1` · `2` | `2` | 화면 전체가 Result면 `1` |
75
75
  | `layoutStyle` | 배치 전용 style 객체 | — | 바깥 여백·폭만 |
76
76
 
@@ -101,7 +101,7 @@ import { Result } from "@hjmds/react-native/feedback";
101
101
 
102
102
  - 행동이 3개 이상이면 `RangeError`를 던진다. 잘리지 않으니 흐름을 다시 설계한다.
103
103
  - 행동 버튼을 children으로 직접 조립하지 않는다(`children` prop이 없다). `actions`로 넘긴다.
104
- - 아이콘은 제품 소유 glyph다. Web은 `icon`, Native는 `renderIcon`이 주는 `color`를 그대로 쓴다.
104
+ - 아이콘은 제품 소유 glyph다. Web은 `icon`, Native는 `renderIcon`이 주는 `color`를 그대로 쓴다. 넘기지 않으면 두 renderer 모두 `resultRecipe.glyphs`의 상태 표식을 그린다(2026-10-17: Native만 빈 원을 그리던 차이).
105
105
  아이콘 배경·tone 색은 HJM(`resultRecipe.tones`) 소유다.
106
106
  - `failure`는 Web `role="alert"`, Native live region·iOS 낭독으로 바로 알린다. 같은 실패를 Toast로
107
107
  한 번 더 알리지 않는다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.17.1
7
- - 검토일: 2026-10-07
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [반복 화면 계약](../../screen-patterns.md); 저장한 항목을 Instagram의 컬렉션 탐색 방식으로 바꾸라는 사용자 요청. 기존 ListDetailScreen·Grid를 합성하며 별도 저장 엔진은 만들지 않는다. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
9
  - 스토리북: `배포/화면/콘텐츠/저장한 항목`
10
10
 
@@ -34,6 +34,7 @@
34
34
 
35
35
  ```tsx
36
36
  // Web
37
+ import { Icon } from "@hjmds/react/display";
37
38
  import { SavedItemsScreen } from "@hjmds/react/saved-items";
38
39
 
39
40
  <SavedItemsScreen title={t("saved.title")} items={items} collections={collections}
@@ -41,7 +42,7 @@ import { SavedItemsScreen } from "@hjmds/react/saved-items";
41
42
  selectedItemId={selectedItemId}
42
43
  labels={{allItems:t("saved.all"),privateNotice:t("saved.private"),back:t("common.back"),empty:t("saved.empty"),createCollection:t("saved.create")}}
43
44
  onOpenCollection={openCollection} onOpenItem={openItem} onBack={goBack}
44
- onCreateCollection={openCreateSheet}
45
+ onCreateCollection={openCreateSheet} createCollectionIcon={<Icon name="add" />}
45
46
  renderThumbnail={renderThumbnail} renderDetail={renderDetail}/>
46
47
  ```
47
48
 
@@ -83,8 +84,9 @@ import { SavedItemsScreen } from "@hjmds/react-native/saved-items";
83
84
  | selectedItemId | `string` 또는 `null`, 생략 시 상세 없음 | — | 현재 컬렉션에 존재하는 항목만 상세로 표시 |
84
85
  | labels | SavedItemsLabels, 필수 | — | 모든 공개 문구를 지역화하여 공급 |
85
86
  | onOpenCollection · onOpenItem · onCreateCollection | 콜백, 필수 | — | 제품 상태/라우터 변경 요청 |
87
+ | createCollectionIcon | `ReactNode`, 필수 | — | 새 컬렉션 행동 `IconButton`(이름 `labels.createCollection`)의 아이콘. 도구성 행동은 아이콘으로 둔다([IconButton](icon-button.md)). 미게시(1.18.0 이후): 글자 버튼 대체 분기를 없애고 필수로 바꿨다 |
86
88
  | onBack | `() => void`, 필수 | — | 한 단계만 올라간다. 상세가 열려 있으면 제품이 `selectedItemId`를 비우고, 아니면 `collectionId`를 생략해 컬렉션 홈으로 간다. 화면은 제품 history를 모른다 |
87
- | actions · leading | ScreenLayout 상속 | 없음 | 홈에서는 제품 `actions` 대신 컬렉션 만들기 버튼이 그 자리를 쓰고 제품 `leading`은 유지된다. 컬렉션·상세에서는 `leading`이 뒤로 버튼이 되고 제품 `actions`가 보인다 |
89
+ | actions · exit | ScreenLayout 상속 | 없음 | 홈에서는 제품 `actions` 대신 컬렉션 만들기 버튼이 그 자리를 쓰고 제품 `exit`은 유지된다. 컬렉션·상세에서는 `exit`이 뒤로 아이콘이 되고 제품 `actions`가 보인다 |
88
90
  | renderThumbnail · renderDetail | `(item) => ReactNode`, 필수 | — | 사진·상세 슬롯, API·자산은 제품 소유 |
89
91
  | state · notice | ScreenLayout 상속 | ready · 없음 | 조회 상태, 저장 해제/복구 안내 |
90
92
  | layoutStyle | `HjmCompositionStyleProp` | 없음 | Web·Native 모두 목록·상세를 담는 ListDetailScreen 바깥 틀에 적용한다(컬렉션·격자·상세 전환과 상관없이 같은 루트). 1.13.0 변경 |
@@ -106,8 +108,8 @@ Native collection cover의 모서리는 Provider의 `tokens.radius.md`다. 저
106
108
 
107
109
  | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
108
110
  | --- | --- | --- | --- |
109
- | 새 컬렉션 | `Button` ghost(`labels.createCollection`) → `onCreateCollection` | 홈 헤더 `actions` 자리(홈에 넘긴 제품 `actions`는 그리지 않는다). 생성 Sheet와 그 footer 확정 버튼은 제품이 그린다 | 홈에서만 1개. 컬렉션·상세에서는 제품 `actions`로 바뀐다 |
110
- | 뒤로 | `Button` ghost(`labels.back`) → `onBack` | 컬렉션·상세 헤더 `leading` | 1개. 상세 → 격자 → 홈으로 한 단계씩, 단계는 제품이 상태를 비워 정한다 |
111
+ | 새 컬렉션 | ghost `IconButton`(아이콘 `createCollectionIcon`, 이름 `labels.createCollection`) → `onCreateCollection` | 홈 헤더 `actions` 자리(홈에 넘긴 제품 `actions`는 그리지 않는다). 생성 Sheet와 그 footer 확정 버튼은 제품이 그린다 | 홈에서만 1개. 컬렉션·상세에서는 제품 `actions`로 바뀐다 |
112
+ | 뒤로 | 헤더 ‹ 아이콘(`exit` `back`, 이름 `labels.back`) → `onBack` | 컬렉션·상세 헤더 시작 쪽 | 1개. 상세 → 격자 → 홈으로 한 단계씩, 단계는 제품이 상태를 비워 정한다 |
111
113
  | 컬렉션·게시물 열기 | 표지·썸네일 전체가 버튼 → `onOpenCollection` · `onOpenItem` | 격자 각 칸 | 항목마다 1개 |
112
114
  | 저장 해제 | `IconButton` ghost(제품) | `renderDetail` 머리 줄 끝(제목과 같은 줄) | 상세마다 1개. 실행 취소는 `notice` |
113
115
 
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.17.1
7
- - 검토일: 2026-10-14
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screens.tsx`·`src/screen-flows.tsx`; 기존 개별 지침을 새 규격으로 통합. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
9
  - 스토리북: `배포/화면/화면 틀과 도구/화면 골격과 상태`
10
10
 
@@ -35,13 +35,12 @@
35
35
 
36
36
  ```tsx
37
37
  // Web
38
- import { IconButton } from "@hjmds/react/actions";
39
38
  import { BottomCTA } from "@hjmds/react/bottom-cta";
40
39
  import { ScreenLayout } from "@hjmds/react/screens";
41
40
 
42
41
  <ScreenLayout
43
42
  title={t("saved.title")}
44
- leading={<IconButton label={t("common.back")} onClick={goBack}><BackGlyph /></IconButton>}
43
+ exit={{ kind: "back", label: t("common.back"), onClick: goBack }}
45
44
  state={isLoading ? { kind: "loading", title: t("saved.loading") } : { kind: "ready" }}
46
45
  footer={<BottomCTA primaryAction={{ label: t("saved.add"), onClick: add }} />}
47
46
  >
@@ -69,19 +68,21 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
69
68
 
70
69
  | prop | 값 | 기본값 | 설명 |
71
70
  | --- | --- | --- | --- |
71
+ | `keyboardAvoidance` (Native) | `boolean` | `true`(미게시) | 키보드와 실제로 겹치는 만큼 footer·본문을 올리고 포커스된 입력칸을 스크롤해 보인다. `KeyboardAvoiding`·키보드 처리 Sheet/Dialog 안에서는 건너뛴다 |
72
72
  | `title` | `string` | 필수 | 화면 제목. `header`를 주면 접근성 이름으로만 쓴다 |
73
73
  | `titleBadge` | `ReactNode` | 없음 | 기본 제목 바로 옆 배지. 값·의미는 제품, 간격·정렬은 HJM. `header` 교체 또는 제목 숨김이면 그리지 않는다. 1.23.0 추가 API |
74
- | `header` | `ReactNode` | 없음 | 기존 navigator 헤더 유지용. 주면 기본 헤더(`leading`·제목·`description`·`actions`)를 그리지 않는다 |
74
+ | `header` | `ReactNode` | 없음 | 기존 navigator 헤더 유지용. 주면 기본 헤더(`exit`·제목·`description`·`actions`)를 그리지 않는다 |
75
75
  | `description` | `string` | 없음 | 제목 아래 muted 설명 |
76
- | `leading` · `actions` | `ReactNode` | 없음 | 제목 앞(뒤로 가기)·뒤(도구) 슬롯 |
76
+ | `exit` | `{ kind: "back" \| "close"; label; onClick(Web) \| onPress(Native); disabled? }` | 없음 | 화면 나가기(뒤로·닫기·"설정 나가기")를 헤더 시작 쪽 아이콘 버튼으로 고정한다. 글자는 보이지 않고 `label`은 보조기술 이름이다. `back`은 ‹(RTL 반전), `close`는 ×. 헤더 시작 쪽에는 이 버튼 외의 자유 슬롯이 없다. 미게시(1.26.1 이후) |
77
+ | `actions` | `ReactNode` | 없음 | 제목 뒤(도구) 슬롯. 시작 쪽 `leading` 슬롯은 2026-10-17 제거됐다 — 제품 사용처가 모두 직접 만든 뒤로 버튼이었다 |
77
78
  | `notice` | `ReactNode` | 없음 | 헤더 아래 비차단 안내. 상태 교체 중에도 남는다 |
78
- | `footer` | `ReactNode` | 없음 | 하단 고정 행동 |
79
+ | `footer` | `ReactNode` | 없음 | 하단 고정 행동. footer가 위 경계·여백·safe area를 맡고, 안에 둔 [BottomCTA](bottom-cta.md)는 자기 바를 그리지 않고 설명·행동 줄만 그린다(2026-10-17, 경계 두 겹·버튼 이중 들여쓰기 수정) |
79
80
  | `state` | `{ kind: "ready" }` · `{ kind: "loading" \| "empty" \| "error" \| "restricted"; title; description? }` | `{ kind: "ready" }` | ready가 아니면 children을 그리지 않고 상태와 `stateAction`을 가운데에 놓는다. 로딩은 Spinner 하나만 보이고 `title`·`description`은 Spinner의 접근성 이름이 된다 |
80
81
  | `stateAction` | `ReactNode` | 없음 | 상태 안내 아래 행동(재시도 등) |
81
82
  | `scroll` | `"screen"` · `"content"` | `"screen"` | `content`는 본문 자식(가상화 목록)이 스크롤을 소유한다. 상태 교체 중에는 `screen`으로 돌아간다 |
82
83
  | `contentAlignment` | `"start"` · `"center"` | `"start"` | ready 본문을 사용 영역 중앙에 배치. 긴 본문은 위가 잘리지 않게 스크롤한다. 로그인/제품 안내를 가짜 query state로 바꾸지 않는다(1.24.0) |
83
84
  | `surface` | `"default"` · `"transparent"` | `"default"` | 투명은 제품 host의 배경·wallpaper를 보존. 배경색 style 우회 대신 사용(1.24.0) |
84
- | `contentInset` | `"default"` · `"none"` | `"default"` | `none`은 헤더·notice·본문·footer padding을 0으로(Native는 footer 위 테두리도 뺀다) |
85
+ | `contentInset` | `"default"` · `"none"` · `"body-edge"` | `"default"` | `none`은 헤더·notice·본문·footer padding을 0으로(Native는 footer 위 테두리도 뺀다). host가 모든 여백을 이미 줄 때 쓴다. `body-edge`는 헤더·notice·footer 여백(16)과 footer 테두리를 유지하고 **본문 좌우 padding만 0**으로 한다. 자기 좌우 여백을 가진 전체 폭 목록 행(ConversationRow·NotificationRow 등 행 padding 16)을 본문에 바로 둘 때 쓴다(미게시). 값 목록은 계약 `ScreenContentInset`·`screenContentInsets` |
85
86
  | `presentation` | `"dashboard"` · `"editorial"` · `"landscape"` | 가장 가까운 profile.screens.overview / 없으면 기존 골격 | [디자인 프로필](../../design-profile.md)의 제공 범위를 따른다. 명시값이 프로필을 이긴다. 헤더 배치·읽기 폭만 바꾸고 상태·입력·행동 슬롯은 유지한다 |
86
87
  | `as` (Web) | `"main"` · `"section"` | `"main"` | 제품 셸에 이미 `<main>`이 있으면 `section` |
87
88
  | `layoutStyle` | `HjmCompositionStyleProp` | 없음 | 화면 루트 배치 전용(예: 분할 화면의 `flex`·`width`). ScreenLayout 위에 만든 화면은 모두 이 prop을 루트까지 넘긴다 |
@@ -93,14 +94,18 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
93
94
  | 항목 | 값 | 근거 |
94
95
  | --- | --- | --- |
95
96
  | 크기 | 폭 100%, 최대 `screenPatternRecipe.maxWidth`(`layout.readingMaxWidth` 720), 가운데 정렬; 높이는 host가 준 남은 높이(Web `block-size: 100%`, Native `flex: 1`) | Web `.hjm-screen`, Native `ScreenLayout` |
96
- | 간격 | 바깥 padding `screenPatternRecipe.padding`(`spacing.md` 16)을 헤더 사방·notice 좌우·본문 사방·footer 사방에 준다(`contentInset="none"`이면 0); 헤더 안 `leading`·제목·`actions` 간격 `itemGap`(`spacing.sm`) 12(두 플랫폼, `contentInset="none"`이어도 유지); 상태 안내는 위아래 `sectionGap`(`spacing.xl`) 24, 안내–`stateAction` `stateGap`(`spacing.md`) 16 | Web `src/screens.tsx`(`--hjm-screen-item-gap`·`--hjm-screen-state-gap`)·`.hjm-screen__*`, Native `ScreenLayout` |
97
- | 순서·정렬 | 헤더(`leading` → 제목·설명 → `actions`) → notice → 본문 → footer; 상태 안내는 본문 세로 가운데 | 렌더 순서 |
97
+ | 간격 | 바깥 padding `screenPatternRecipe.padding`(`spacing.md` 16)을 헤더 사방·notice 좌우·본문 사방·footer 사방에 준다(`contentInset="none"`이면 0, `"body-edge"`면 본문 좌우만 0); 헤더 안 `exit`·제목·`actions` 간격 `itemGap`(`spacing.sm`) 12(두 플랫폼, `contentInset="none"`이어도 유지); 상태 안내는 위아래 `sectionGap`(`spacing.xl`) 24, 안내–`stateAction` `stateGap`(`spacing.md`) 16 | Web `src/screens.tsx`(`--hjm-screen-item-gap`·`--hjm-screen-state-gap`)·`.hjm-screen__*`, Native `ScreenLayout` |
98
+ | 제목 | 기본 가운데 `Heading` `screenHeaderRecipe.centeredTitleLevel`(level5), `headerAlignment="start"`면 `titleLevel`(level3). 추가 기능 버튼(`actions`)은 끝(오른쪽) 위, 나가기(`exit`)는 시작(왼쪽) 위. 헤더 높이는 두 플랫폼 모두 `screenHeaderRecipe`의 고정 띠 | `screenHeaderRecipe` |
99
+ | 순서·정렬 | 헤더(`exit` → 제목·설명 → `actions`) → notice → 본문 → footer; 상태 안내는 본문 세로 가운데 | 렌더 순서 |
98
100
  | 고정·스크롤 | 헤더·notice·footer 고정, `scroll="screen"`은 본문이 스크롤, `"content"`는 자식이 스크롤; footer 위 테두리 1, Web은 하단 safe area만큼 padding을 늘린다 | Web `data-scroll`·`.hjm-screen__footer`, Native `ScrollView` |
99
101
  | 좁은 폭·큰 글자 | 제목 열 최소 폭 `headerMinWidth` 120 × 글자 배율, 모자라면 `actions`가 다음 줄로 내려간다; 제목은 줄바꿈(`overflow-wrap: anywhere`)되고 자르지 않는다; Native safe area·탭바 inset은 host | `screenPatternRecipe.headerMinWidth`, Web `.hjm-screen__heading` |
100
102
 
101
103
  ## 꼭 지킬 것
102
104
 
103
- - Native에서 `headerAlignment="center"` + `titleVisibility="hidden"`이고 설명·사용자 지정 헤더가 없으면 아이콘 헤더는 최소 48pt, 세로 여백 2pt다. 2026-10-14 야잘알의 상단 버튼–탭 사이 빈 공간 피드백으로 제목용 60pt 높이를 줄였다. `notice`가 없을 때만 본문 상단 여백을 최대 4pt로 줄이며, `notice`에 둔 탭과 본문 카테고리 사이 기본 16pt 간격은 유지한다. 좌우 여백·하단 여백·터치 영역은 그대로다.
105
+ - **헤더 제목은 기본 가운데, 추가 기능 버튼은 오른쪽 위.** `headerAlignment` 기본값은 `"center"`이고 `"start"`는 큰 제목이 필요한 화면에서만 고른다(2026-10-17 사용자 지시: 가운데가 기본, 강제는 아님).
106
+ - **나가기는 `exit` 하나로 헤더 시작 쪽에 둔다(필수).** 본문·하단의 "나가기"·"닫기"·"뒤로"·"취소하고 나가기" 글자 버튼, 화면마다 다른 화살표(`←` 글자, 제품 아이콘)로 만든 뒤로 버튼을 두지 않는다. 2026-10-17 사용자 지시: `변경 저장과 이탈 확인` 예제의 "설정 나가기"가 본문 버튼으로 떠 있었고, 제품마다 뒤로 버튼 모양이 달랐다(야잘알 자체 `BackButton`, 예제의 `←`·`ChevronLeft`). 변경이 있을 때의 확인 대화상자는 제품이 `exit` 콜백에서 띄운다. 기존 navigator 헤더를 쓰는 화면은 그 헤더의 뒤로 버튼이 같은 자리를 맡는다.
107
+
108
+ - `headerAlignment="center"` + `titleVisibility="hidden"`이고 설명·사용자 지정 헤더가 없으면 아이콘 헤더는 최소 48pt, 세로 여백 2pt다(값의 정본은 `screenHeaderRecipe`). 2026-10-14 야잘알의 상단 버튼–탭 사이 빈 공간 피드백으로 제목용 높이를 줄였다. 제목이 보이는 가운데 헤더는 최소 52pt, 세로 여백 4pt다(2026-10-17 사용자 지시 "상단 여백 줄여": 60pt가 iOS 내비게이션 바보다 높았다). `notice`가 없을 때만 본문 상단 여백을 최대 4pt로 줄이며, `notice`에 둔 탭과 본문 카테고리 사이 기본 16pt 간격은 유지한다. 좌우 여백·하단 여백·터치 영역은 그대로다.
104
109
 
105
110
  - Web의 화면 스크롤 본문은 Tab으로 진입할 수 있다. `scroll="content"`의 정상 본문은 자식이 키보드 스크롤을 소유하지만, 오류·빈 상태 등 대체 안내는 화면 스크롤로 바뀌며 Tab 진입도 함께 복원된다.
106
111
  - Web에서 본문에 초점을 가진 입력·재시도 버튼이 상태 전환으로 제거되면 같은 본문으로 초점을 복구한다. `scroll="content"`의 본문은 이 복구만을 위한 `tabIndex=-1`을 가지며 일반 Tab 순서에는 추가되지 않는다. 사용자가 다른 컨트롤이나 바깥 영역으로 초점을 옮긴 경우에는 이동시키지 않는다. 2026-10-13 재시도·권한 제한 전환에서 초점이 body로 유실된 경로를 수정했다.
@@ -125,9 +130,15 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
125
130
 
126
131
  - 이미 `<main>`이 있는 제품 셸 안에서는 `as="section"`을 준다. 중첩 main이 생긴다.
127
132
 
133
+ - 본문 좌우 여백을 직접 빼려고 목록에 음수 margin이나 제품 CSS를 쓰지 않는다. 행이 자기 여백을 가지면 `contentInset="body-edge"`, host가 모든 여백을 가지면 `"none"`이다. 본문 안에 행 여백이 없는 제목·문단을 함께 두는 화면(설정 묶음 제목, 알림 그룹 제목 + ListRow)은 `default`를 유지한다. ListRow 좌우 여백은 8이라 본문 여백을 빼면 제목이 화면 끝에 붙는다.
134
+
128
135
  ### 디자인 프로필의 화면 표현
129
136
 
130
137
  `presentation` 명시값, 가장 가까운 프로필의 screens.overview, 기존 화면 골격 순으로 선택한다.
131
138
  dashboard는 가로 헤더/1200 목록 폭, editorial은 세로 헤더/720 읽기 폭, landscape는 중앙 세로 헤더/1200 폭이다.
132
139
  폭은 foundations의 contentMaxWidth/readingMaxWidth다. 프로필을 지정하지 않으면 기존 recipe 배치를 유지한다.
133
140
  같은 header/body/notice/footer 및 상태 엔진을 유지하며 입력 subtree를 테마별로 교체하지 않는다.
141
+
142
+ ## 근거
143
+
144
+ - `contentInset="body-edge"`: 2026-10-17 사용자 결정. 화면 검토 S5#9에서 대화 목록 아바타가 화면 끝에서 약 32pt 안쪽에서 시작했다. 본문 padding 16과 ConversationRow 자체 padding 16이 겹쳤다(인스타그램 DM은 약 16). 앱 약 45곳이 "host가 모든 여백을 소유한다"는 뜻으로 `none`을 쓰고 있어 `none`의 의미를 바꿀 수 없었으므로 값을 새로 추가했다. ChatScreen의 10 타임라인 여백은 `default`일 때만 공통 여백을 대신한다.
@@ -71,7 +71,7 @@ import { SearchField } from "@hjmds/react-native/inputs";
71
71
  | `onClear` | `() => void` | — | 지우기 버튼을 누른 뒤 |
72
72
  | `loading`(Web) / `busy`(Native) | `boolean` | `false` | 진행 중에는 지우기 대신 진행 표시가 뒤에 선다 |
73
73
  | `clearLabel` | 문자열 | — | 필수, 지우기 버튼 접근성 이름. Native는 `busyLabel`도 필수 |
74
- | `renderSearchIcon`(Web) · `renderLeading`(Native) | `(props) => ReactNode` — Web `{ color: "currentColor", size }`, Native `{ color, size, disabled }` | 기본 돋보기 | 제품 아이콘 adapter |
74
+ | `renderSearchIcon`(Web) · `renderLeading`(Native) | `(props) => ReactNode` — Web `{ color: "currentColor", size }`, Native `{ color, size, disabled }` | 없음 | 제품 돋보기 adapter. HJM은 아이콘을 내장하지 않으므로 넘기지 않으면 앞쪽 아이콘이 없다 |
75
75
  | `renderClearIcon` · `renderLoadingIndicator`(Web) · `renderBusyIndicator`(Native) | 위와 같은 모양 | 기본 아이콘·스피너 | — |
76
76
  | `layoutStyle` | 배치 전용 style 객체 | — | 필드 전체 배치. Web `style`은 안쪽 input에 붙는다 |
77
77
  | `label` / `aria-label`(Web) / `accessibilityLabel`(Native) | 문자열 | — | 하나는 필수. 없으면 렌더 중 `TypeError` |