@hjmds/design-contracts 1.26.1 → 1.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.17.1
7
- - 검토일: 2026-10-13
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
 
@@ -93,18 +93,18 @@ import { TextField } from "@hjmds/react-native/inputs";
93
93
  | --- | --- | --- | --- |
94
94
  | `dirty` | `boolean` | 필수 | `true`에서 닫기를 누르면 `discard` 확인창을 띄우고, 확인하면 `cancel.onAction`을 부른다. `false`면 바로 부른다 |
95
95
  | `submit` | `ScreenFlowAction`(`{ label, onAction(), disabled?, pending? }`) | 필수 | `pending`이면 loading·비활성. 그동안 닫기 버튼도 비활성. 본문 `state`가 `ready`가 아니면 저장 비활성 |
96
- | `cancel` | `ScreenFlowAction` | 필수 | `leading` 자리의 ghost 닫기 버튼 |
96
+ | `cancel` | `ScreenFlowAction` | 필수 | 헤더 시작 쪽 × 아이콘 닫기 버튼(`ScreenLayout` `exit` `close`). `label`은 보조기술 이름이다. 2026-10-17부터 글자 버튼 대신 아이콘 |
97
97
  | `discard` | AlertDialog confirm 요청에서 `onConfirm`을 뺀 것 + `fallbackErrorMessage: string` | 필수 | `onConfirm`은 EditorScreen이 채운다 |
98
98
  | `submitPlacement` | `"footer"` · `"header"` | `"footer"` | `header`면 저장이 상단 `actions` 자리로 가고 footer에는 `draftStatus`만 남는다 |
99
99
  | `draftStatus` | `ReactNode` | 없음 | 초안 저장 상태 안내. footer에 놓인다 |
100
100
  | `children` | `ReactNode` | 필수 | 편집 본문 |
101
- | 나머지 | `ScreenLayout`과 같음(`children`·`footer` 제외) | — | `leading`은 닫기 버튼으로 덮인다. `submitPlacement="footer"`일 때만 `actions`가 그대로 전달된다 |
101
+ | 나머지 | `ScreenLayout`과 같음(`children`·`footer` 제외) | — | `leading`·`exit`은 받지 않는다(닫기가 그 자리를 소유). `submitPlacement="footer"`일 때만 `actions`가 그대로 전달된다 |
102
102
 
103
103
  ## 배치
104
104
 
105
105
  | 항목 | 값 | 근거 |
106
106
  | --- | --- | --- |
107
- | 크기 | ScreenLayout 폭(최대 720) 안에 입력 본문; 저장·닫기는 `Button` 기본 크기 | `ScreenLayout`, `screen-flows.tsx` `Action` |
107
+ | 크기 | ScreenLayout 폭(최대 720) 안에 입력 본문; 저장은 `Button` 기본 크기, 닫기는 `IconButton` | `ScreenLayout`, `screen-flows.tsx` `Action` |
108
108
  | 간격 | 화면 padding `spacing.md` 16; footer 안 `draftStatus`–저장 `spacing.sm` 12(`Stack gap="sm"`); 본문 안 입력 간격은 제품(Form 등) 소유 | Web·Native `EditorScreen` |
109
109
  | 순서·정렬 | 헤더(닫기 → 제목 → [저장: header 배치]) → 본문 → footer(`draftStatus` → 저장) | `EditorScreen` 렌더 순서 |
110
110
  | 고정·스크롤 | 헤더·footer 고정, 본문 스크롤(`scroll` 기본 `"screen"`); 이탈 확인은 AlertDialog 오버레이 | `ScreenLayout`, `AlertDialog` |
@@ -115,7 +115,7 @@ import { TextField } from "@hjmds/react-native/inputs";
115
115
  - 모든 문구(`label`, `discard`의 제목·설명·버튼)는 i18n 키로 넣는다. 컴포넌트는 기본 문구를 갖지 않는다.
116
116
  - `discard`는 `fallbackErrorMessage`까지 필수 타입이다. `onConfirm`은 EditorScreen이 채우므로 넘기지 않는다.
117
117
  - 확인 후 초안을 되돌리는 일은 `cancel.onAction` 안에서 제품이 한다.
118
- - 저장 시작 또는 `cancel.disabled`/`cancel.pending` 전환은 이미 열린 초안 버리기 확인도 종료한다. 잠금이 풀렸다고 확인창이 다시 열리지는 않는다. 2026-10-13 이전 확인창으로 저장 중 이탈 잠금을 우회할 수 있던 경로를 수정했다.
118
+ - 저장 시작 또는 `cancel.disabled`/`cancel.pending` 전환은 이미 열린 초안 버리기 확인도 종료한다. 잠금이 풀렸다고 확인창이 다시 열리지는 않는다.
119
119
  - 최초 조회·오류·권한 제한 등 본문 대체 상태에서는 저장을 잠근다. 새 문서의 빈 초안은 `state="empty"`가 아니라 `ready`와 빈 입력으로 표현하고 제품 validation은 `submit.disabled`에 연결한다.
120
120
  - 라우터 뒤로가기·Android back·브라우저 이탈은 이 컴포넌트가 막지 않는다. 제품이 `dirty`로 guard를 건다.
121
121
  - 저장 성공 판정·오류 표시는 제품 상태(`notice`, Toast 등)로 한다.
@@ -126,3 +126,7 @@ import { TextField } from "@hjmds/react-native/inputs";
126
126
  | --- | --- | --- |
127
127
  | 화면 셸 전용 props | `as`, `className` | `testID`, `scrollRef`, `scrollProps` (`layoutStyle`은 양쪽 모두 받는다) |
128
128
  | 버튼 이벤트 | 내부에서 `onClick` → `onAction` | 내부에서 `onPress` → `onAction` |
129
+
130
+ ## 근거
131
+
132
+ - 저장 시작 시 열린 버리기 확인 종료: 2026-10-13 이전 확인창으로 저장 중 이탈 잠금을 우회할 수 있던 경로를 수정했다.
@@ -71,6 +71,8 @@ import { EffectSurface } from "@hjmds/react-native/effect-surface";
71
71
  | `descriptor.colors` | `ColorReference` 세 개 | `themeColor("primary")` · `themeColor("contentBrand")` · `themeColor("surfaceAccent")` | — |
72
72
 
73
73
  | `children` | `ReactNode` | — (필수) | 일반 레이아웃 내용 |
74
+ | `padding` | `none` · spacing 이름(Surface와 같은 척도) | `none` | 장식 영역 안쪽 내용 여백. 장식 레이어는 여백까지 덮는다 |
75
+ | `radius` | `none` · radius 이름(Surface와 같은 척도) | Surface 기본 `lg` 16 | 루트 모서리. 장식은 이 모서리 안에서 잘린다. 화면 전체 배경(design profile canvas)은 `none` |
74
76
  | Native `visible` | `boolean` | `true` | 화면·목록 소유자가 가려짐을 알린다. `false`면 장식이 멈춘다 |
75
77
  | Web `layoutStyle` | 배치 전용 style | — | 루트 배치 |
76
78
  | Native `style` | `StyleProp<ViewStyle>` | — | 컨테이너 `View`. 정식 motion host 프레임이라 deprecated 대상이 아니다 |
@@ -81,8 +83,8 @@ import { EffectSurface } from "@hjmds/react-native/effect-surface";
81
83
 
82
84
  | 항목 | 값 | 근거 |
83
85
  | --- | --- | --- |
84
- | 크기 | EffectSurface는 여백을 갖지 않는다. 크기는 children이 정하고 장식 레이어가 그 영역 전체(`absolute`, inset 0)를 덮는다. 넘치는 장식은 잘린다(`overflow: hidden`). 배경은 테마 `bg`다. 높이를 늘리려면 children 높이, Web `layoutStyle`·Native `style`의 높이로 정한다. 장식 레이어는 배치에 참여하지 않는다 | `react/src/effect-surface.tsx`, `react-native/src/effect-surface.tsx` |
85
- | 간격 | 내용 여백·간격은 children 쪽 [Stack](stack.md) 등으로 준다 | `react/src/effect-surface.tsx` |
86
+ | 크기 | 기본 여백은 없다(`padding="none"`). 크기는 children과 `padding`이 정하고 장식 레이어가 그 영역 전체(`absolute`, inset 0)를 덮는다. 넘치는 장식은 `radius` 모서리 안에서 잘린다(`overflow: hidden`). 안에 둔 Surface와 같은 기본 radius라 둥근 카드 모서리 밖으로 장식이 비치지 않는다. 배경은 테마 `bg`다. 높이를 늘리려면 children 높이, Web `layoutStyle`·Native `style`의 높이로 정한다. 장식 레이어는 배치에 참여하지 않는다 | `react/src/effect-surface.tsx`, `react-native/src/effect-surface.tsx` |
87
+ | 간격 | 안쪽 여백은 `padding`(예: `lg` 20), 내용 사이 간격은 children 쪽 [Stack](stack.md)으로 준다. raw 요소에 padding을 직접 주지 않는다 | `react/src/effect-surface.tsx` |
86
88
  | 순서·정렬 | children은 일반 레이아웃이다. [Stack](stack.md)으로 eyebrow → [Heading](heading.md) → 소개 → [Button](button.md)을 쌓는다(Showcase 랜딩 예는 `gap="lg"` 20, Heading `level1`) | `showcase/web/src/patterns/Landing.stories.tsx` |
87
89
  | 고정·스크롤 | 화면 위쪽 히어로 하나에만 둔다. 같은 화면에 여러 개를 겹치거나 목록 행마다 두지 않는다 | — |
88
90
  | 좁은 폭·큰 글자 | — | — |
@@ -59,7 +59,7 @@ import { EmptyState } from "@hjmds/react-native/feedback";
59
59
 
60
60
  | prop | 값 | 기본값 | 설명 |
61
61
  | --- | --- | --- | --- |
62
- | `density` | `compact` · `regular` | `regular` | `compact`는 세로 여백이 작고 Native에서 남은 공간을 채우지 않는다 |
62
+ | `density` | `compact` · `regular` | `regular` | `compact`는 세로 여백이 작고 남은 공간을 채우지 않는다 |
63
63
  | `align`(Native) | `center` · `upper` | `center` | `upper`는 `regular`일 때 내용을 위쪽(1:3 여백)에 둔다 |
64
64
  | `announcement`(Native) | `none` · `polite` · `assertive` | `none` | 상태가 바뀌어 빈 화면이 새로 나타날 때만 켠다 |
65
65
  | `titleRole`(Native) | 접근성 role | `"header"` | Web은 항상 `role="status"`로 그린다 |
@@ -80,7 +80,7 @@ EmptyState 자체에는 콜백 prop이 없다. 2026-10-07 기존 예제의 생
80
80
  | 크기 | Native 그림 슬롯은 최소 `glyph.lg`(28) 폭·높이를 확보하고 큰 일러스트의 자체 크기를 존중한다. Web 그림 슬롯에는 고정 크기가 없다. 세로 여백: `compact` `spacing.xl`(24). `regular` `spacing.xxxl`(40, 두 플랫폼) | `component-recipes.ts` `emptyStateRecipe`, `styles.css` `.hjm-empty-state` |
81
81
  | 간격 | 항목 사이 간격 `spacing.xs`(8), 좌우 여백 `spacing.xl`(24) | `component-recipes.ts` `emptyStateRecipe` |
82
82
  | 순서·정렬 | 위→아래 순서는 그림 → 제목 → 설명 → 행동이고 가운데 정렬이다. 행동은 Button 하나(`tone="secondary"` 또는 `primary`)를 내용 폭만큼 둔다 | `react-native/src/feedback.tsx` `EmptyState` |
83
- | 고정·스크롤 | 비어 있는 목록·검색 결과 영역 **안**에 둔다. 위 TopBar·검색칸·필터는 그대로 남기고 그 아래 내용 영역만 EmptyState로 바꾼다. Native `regular`는 남은 높이를 채우고(`flexGrow: 1`) 세로 가운데에 놓는다. `align="upper"`는 위:아래 빈 공간을 1:3으로 나눠 내용이 위쪽에 온다. 화면 하단 고정 행동이 필요하면 [BottomCTA](bottom-cta.md)로 따로 둔다 | `react-native/src/feedback.tsx` `EmptyState` |
83
+ | 고정·스크롤 | 비어 있는 목록·검색 결과 영역 **안**에 둔다. 위 TopBar·검색칸·필터는 그대로 남기고 그 아래 내용 영역만 EmptyState로 바꾼다. `regular`는 flex 부모의 남은 높이를 채우고(Web `flex-grow: 1`, Native `flexGrow: 1`) 세로 가운데에 놓는다. 높이가 내용만큼인 부모에서는 늘어나지 않는다. `align="upper"`는 위:아래 빈 공간을 1:3으로 나눠 내용이 위쪽에 온다. 화면 하단 고정 행동이 필요하면 [BottomCTA](bottom-cta.md)로 따로 둔다 | `react/src/styles.css` `.hjm-empty-state`, `react-native/src/feedback.tsx` `EmptyState` |
84
84
  | 좁은 폭·큰 글자 | 카드·시트 안처럼 높이가 작은 자리는 `compact`(채우지 않음)를 쓴다 | `react-native/src/feedback.tsx` `EmptyState` |
85
85
 
86
86
  ```text
@@ -0,0 +1,104 @@
1
+ # EventAttendanceCard
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: `src/social-compositions.ts`의 `resolveAttendanceProposal`·`resolveAttendanceChoice`, 양 renderer `src/social-compositions.tsx`, [소셜 구성](../compositions/social-compositions.md)
9
+ - 스토리북: `배포/구성/선택과 필터/행사 참석 응답`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 행사·모임 카드에서 일정, 참석자 수, 참석자 목록을 보여 주고 참석 응답(참석·미정·불참 등)과 캘린더 추가를
14
+ 실제 요청으로 처리할 때 쓴다. 응답 요청 중에는 제안한 응답과 참석자 수를 먼저 보여 주고, 실패하면 제품이
15
+ 확인한 `state`로 되돌린다. 네트워크·인증·저장은 제품이 소유한다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 프로필 팔로우 | `FollowProfileCard`([소셜 구성](../compositions/social-compositions.md)) |
22
+ | 요청 없이 선택만 하는 응답 | [SegmentedControl](segmented-control.md) 또는 [RadioGroup](radio-group.md) |
23
+ | 일반 정보 카드 | [Card](card.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `EventAttendanceCard` | 행사 참석 응답·캘린더 추가 카드 | `@hjmds/react/social-compositions` | `@hjmds/react-native/social-compositions` |
30
+
31
+ `AttendanceChoice`·`EventAttendanceState` 타입과 `resolveAttendanceProposal`은 `@hjmds/design-contracts/social-compositions`에서 import한다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { EventAttendanceCard } from "@hjmds/react/social-compositions";
38
+
39
+ <EventAttendanceCard eventId={event.id} title={event.title} dateLabel={formatEventDate(event)}
40
+ locale={locale} state={attendance} choices={choices} participants={participants}
41
+ labels={{ response: t("rsvp.title"), pending: t("common.pending"), count: t("rsvp.count"),
42
+ calendar: t("rsvp.calendar"), calendarAdded: t("rsvp.calendarAdded"), error: t("rsvp.error") }}
43
+ onResponseChange={async id => setAttendance(await saveResponse(event.id, id))}
44
+ onAddToCalendar={() => addEventToCalendar(event)} />
45
+ ```
46
+
47
+ ```tsx
48
+ // Native
49
+ import { EventAttendanceCard } from "@hjmds/react-native/social-compositions";
50
+
51
+ <EventAttendanceCard eventId={event.id} title={event.title} dateLabel={formatEventDate(event)}
52
+ locale={locale} state={attendance} choices={choices} participants={participants}
53
+ labels={{ response: t("rsvp.title"), pending: t("common.pending"), count: t("rsvp.count"),
54
+ calendar: t("rsvp.calendar"), calendarAdded: t("rsvp.calendarAdded"), error: t("rsvp.error") }}
55
+ onResponseChange={async id => setAttendance(await saveResponse(event.id, id))}
56
+ onAddToCalendar={() => addEventToCalendar(event)} />
57
+ ```
58
+
59
+ ## 축과 기본값
60
+
61
+ | prop | 값 | 기본값 | 설명 |
62
+ | --- | --- | --- | --- |
63
+ | eventId | string | 필수 | 비어 있으면 오류. 바뀌면 내부 요청 상태를 새로 시작한다 |
64
+ | title / description | string / ReactNode | title 필수 | Card 제목·설명 |
65
+ | dateLabel | string | 필수 | 제품이 현지화한 일정 문구 |
66
+ | state | `{ responseId: string \| null; count: number }` | 필수 | 서버가 확인한 응답과 참석자 수. 참석 응답인데 수가 0이면 오류 |
67
+ | choices | `readonly AttendanceChoice[]`(`id`·`label`·`attending`·`disabled?`) | 필수 | 비어 있거나 id 중복이면 오류 |
68
+ | participants | `{ id; name; avatar? }[]` | 필수 | 아바타가 없으면 이름 Avatar를 장식으로 표시 |
69
+ | locale | string | 필수 | 참석자 수 `AnimatedStatistic` 숫자 형식 |
70
+ | labels | `response`·`pending`·`count`·`calendar`·`calendarAdded`·`error` | 필수 | 문구 |
71
+ | onResponseChange | `(responseId) => void \| Promise<void>` | 필수 | 실제 응답 저장. 끝나면 `state` 갱신 |
72
+ | onAddToCalendar | `() => void \| Promise<void>` | 필수 | 기기 캘린더·권한 어댑터. reject하면 재시도 가능 |
73
+ | disabled | boolean | false | 모든 행동 비활성 |
74
+
75
+ ## 배치
76
+
77
+ | 항목 | 값 | 근거 |
78
+ | --- | --- | --- |
79
+ | 크기 | 부모 폭을 채우는 Card, 높이는 내용 | [Card](card.md) |
80
+ | 간격 | 본문 요소 사이 `spacing.md` 16, 참석자 사이·아바타–이름·응답 버튼 사이 `spacing.xs` 8 | `Stack gap="md"`, `Stack axis="inline" gap="xs"` |
81
+ | 순서·정렬 | Card 제목·설명 → 일정 → 참석자 수 → 참석자 목록 → 응답 제목(strong) → 응답 버튼 행 → 캘린더 버튼(secondary) → 실패 문구(중앙) | 양 renderer |
82
+ | 고정·스크롤 | 자체 스크롤 없음. 카드 목록 스크롤은 부모 | [Card](card.md) |
83
+ | 좁은 폭·큰 글자 | 참석자 목록과 응답 버튼 행은 줄바꿈(`wrap`) | `Stack wrap` |
84
+
85
+ ## 꼭 지킬 것
86
+
87
+ - `onResponseChange`는 실제 저장이 끝난 뒤 resolve하고, 서버가 확인한 `state`를 다시 넘긴다. 참석자 수를 제품에서 따로 더하지 않는다(제안 수는 요청 시작 때 한 번 계산한다).
88
+ - 캘린더 추가는 실제 기기·계정 캘린더 어댑터를 연결한다. 성공하면 버튼이 `calendarAdded` 문구로 바뀌고 비활성된다.
89
+ - 참석자 목록·권한·알림은 제품 데이터다. 카드 안 배치를 제품 레이아웃으로 다시 조립하지 않는다.
90
+
91
+ ## 상태와 상호작용
92
+
93
+ | 상태 | 모습 | 동작 |
94
+ | --- | --- | --- |
95
+ | 기본 | 현재 응답 버튼 `selected` | 같은 응답·비활성 응답 누름은 무시 |
96
+ | 진행 중 | 제안 응답 선택·수 반영, 해당 버튼 로딩, 모든 행동 잠금 | 캘린더 요청 중에는 캘린더 버튼만 로딩, 응답·수는 `state` 그대로 |
97
+ | 실패 | `state`로 복귀, `labels.error` 중앙 표시 | Web `role="alert"`, Native `accessibilityRole="alert"`·assertive. 같은 행동으로 재시도 |
98
+
99
+ ## 플랫폼 차이
100
+
101
+ | 항목 | Web | Native |
102
+ | --- | --- | --- |
103
+ | 진행 중 이름 | `aria-label={labels.pending}` | `loadingLabel={labels.pending}` |
104
+ | 기본 아바타 | `Avatar aria-hidden` | `Avatar decorative` |
@@ -0,0 +1,105 @@
1
+ # ExpandableSearch
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [검색·메시지 연결 계약](../../communication.md), `src/communication.ts`의 `SearchResponse`, 양 renderer `src/communication.tsx`
9
+ - 스토리북: `배포/구성/선택과 필터/펼치는 상단 검색`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 평소에는 검색 열기 버튼 하나만 두고, 누르면 그 자리에서 검색 입력·제안·최근 검색·결과를 펼칠 때 쓴다.
14
+ Lucian 027의 펼침 검색을 기존 `SearchScreen`의 두 단계 검색·debounce·AbortSignal에 연결한 구성이다.
15
+ 펼침 상태·검색어·최근 검색은 제품이 소유하고, 현재 검색어와 맞는 최신 응답만 표시한다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 검색이 화면의 본문 전체 | `SearchScreen` |
22
+ | 입력란 하나만 필요 | `SearchField` |
23
+ | 명령 실행 팔레트 | `CommandPalette` |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `ExpandableSearch` | 열기 버튼 ↔ 펼친 검색(입력·제안·결과) | `@hjmds/react/communication` | `@hjmds/react-native/communication` |
30
+
31
+ `SearchResponse<T>` 타입은 `@hjmds/design-contracts/communication`에 있다. root barrel에는 없다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { ExpandableSearch } from "@hjmds/react/communication";
38
+
39
+ <ExpandableSearch<Doc[]> expanded={expanded} onExpandedChange={setExpanded} query={query} onQueryChange={setQuery}
40
+ onSearch={(value, { signal }) => api.searchDocs(value, { signal })} // { result, suggestions }
41
+ renderResult={docs => <DocList docs={docs} />}
42
+ onSubmit={value => saveRecent(value)} recentQueries={recentQueries}
43
+ labels={{ title: t("search.title"), open: t("search.open"), cancel: t("search.close"), query: t("search.query"),
44
+ clear: t("search.clear"), searching: t("search.searching"), error: t("search.failed"),
45
+ commit: value => t("search.commit", { value }), suggestionCount: count => t("search.suggestions", { count }) }} />
46
+ ```
47
+
48
+ ```tsx
49
+ // Native
50
+ import { ExpandableSearch } from "@hjmds/react-native/communication";
51
+
52
+ <ExpandableSearch<Doc[]> expanded={expanded} onExpandedChange={setExpanded} query={query} onQueryChange={setQuery}
53
+ onSearch={(value, { signal }) => api.searchDocs(value, { signal })}
54
+ renderResult={docs => <DocList docs={docs} />}
55
+ onSubmit={value => saveRecent(value)} recentQueries={recentQueries}
56
+ labels={searchLabels} />
57
+ ```
58
+
59
+ ## 축과 기본값
60
+
61
+ | prop | 값 | 기본값 | 설명 |
62
+ | --- | --- | --- | --- |
63
+ | expanded / onExpandedChange | boolean / fn | 필수 | 펼침 상태(제품 소유) |
64
+ | query / onQueryChange | string / fn | 필수 | 검색어(제품 소유) |
65
+ | onSearch | (query, { signal }) => Promise\<SearchResponse\<T\>\> | 필수 | 실제 결과와 `suggestions`. 빈 입력은 호출하지 않음 |
66
+ | renderResult | (result: T) => ReactNode | 필수 | 현재 검색어의 결과 렌더 |
67
+ | onSubmit | (query) => void | 없음 | Enter·제안·최근 검색 확정 시 trim된 값 |
68
+ | recentQueries | SearchRecentQueries | 없음 | SearchScreen 최근 검색 슬롯 |
69
+ | debounceMs | number | 300 | SearchScreen `searchScreenRecipe.debounceMs` |
70
+ | disabled | boolean | false | 열기 버튼 비활성 |
71
+ | labels | object | 필수 | title·open·cancel·query·clear·searching·error·commit·suggestionCount |
72
+
73
+ ## 배치
74
+
75
+ | 항목 | 값 | 근거 |
76
+ | --- | --- | --- |
77
+ | 크기 | 접힘: Button(primary) 하나. 펼침: SearchScreen 규격 | 양 renderer |
78
+ | 간격 | 세로 sm | 양 renderer |
79
+ | 순서·정렬 | 접힘: 열기 버튼. 펼침: 닫기(ghost) → SearchScreen(제목 숨김, 입력·제안·결과) → 오류(가로 중앙) | 양 renderer |
80
+ | 고정·스크롤 | 결과 스크롤은 SearchScreen·바깥 host 소유 | 별도 scroller 없음 |
81
+ | 좁은 폭·큰 글자 | SearchScreen 입력·제안 규칙을 그대로 따름 | SearchScreen |
82
+
83
+ ## 꼭 지킬 것
84
+
85
+ - `onSearch`는 `signal`을 실제 요청에 전달한다. 신호를 무시해도 늦은 응답은 표시되지 않지만 서버 부하는 남는다.
86
+ - 최근 검색 저장·분석은 `onSubmit`에서만 한다. 타이핑 중 자동 기록하지 않는다.
87
+ - 닫아도 검색어는 지우지 않는다. 지울지는 제품이 `query`로 정한다.
88
+
89
+ ## 상태와 상호작용
90
+
91
+ | 상태 | 모습 |
92
+ | --- | --- |
93
+ | 접힘 | 열기 버튼만 표시. 진행 중 요청은 abort |
94
+ | 펼침 | 검색 입력으로 포커스. 입력 변경마다 debounce 뒤 검색 |
95
+ | 검색 중 | SearchScreen `searching` 표시 |
96
+ | 실패 | 입력 유지, 아래 중앙 오류. 편집·재검색으로 재시도 |
97
+ | 닫기 | 요청 abort 후 `onExpandedChange(false)` |
98
+
99
+ ## 플랫폼 차이
100
+
101
+ | 항목 | Web | Native |
102
+ | --- | --- | --- |
103
+ | 닫은 뒤 | 열기 버튼으로 포커스 복원 | 키보드를 내림 |
104
+ | 입력 | SearchScreen 기본 입력 | `SearchField`(busy 표시, return 키 `search`로 확정) |
105
+ | 실패 알림 | `role="alert"` | `accessibilityRole="alert"`·assertive |
@@ -67,6 +67,7 @@ import { FilePicker } from "@hjmds/react-native/file-picker";
67
67
  | `existingCount` | 0 이상 정수 | `0` | 여러 번 나눠 고르는 흐름은 이미 고른 개수를 넘겨야 `maxCount`가 누적으로 판정된다 |
68
68
  | `descriptor` | `{ mode?, accept?: readonly string[], maxSizeBytes?, maxCount? }` | — | `accept`는 MIME 패턴(`image/*`) 또는 확장자(`.pdf`). 비우면 제한 없음 |
69
69
  | `onSelect` | `(result: { accepted: readonly FilePickerCandidate[]; rejected: readonly FilePickerRejection[] }) => void` | 필수 | 받아들인 것과 거부한 것을 한 번에 준다 |
70
+ | Web `onSelect` 두 번째 인자 `files` | `ReadonlyMap<string, File>` | — | 받아들인 후보 id → 브라우저 `File`. 제품이 내용을 읽거나 올릴 때 쓴다. 거부된 파일은 들어 있지 않다. 미게시(1.18.0 이후). Native는 `onPick` adapter가 파일 참조를 소유한다 |
70
71
  | Native `onPick` | `() => Promise<readonly FilePickerCandidate[] \| null>` | 필수 | 제품 adapter. 취소면 `null` |
71
72
  | Native `onPickError` | `(error: unknown) => void` | 필수 | `onPick`이 reject하면 이쪽으로만 온다 |
72
73
  | Web `getCandidateId` | `(file: File, index: number) => string` | `name:size:lastModified:index` | 후보 id 규칙 |
@@ -121,3 +122,7 @@ import { FilePicker } from "@hjmds/react-native/file-picker";
121
122
  2026-10-07 소비 감사에서 Native 선택 버튼의 모서리 12가 프로필을 우회했다. Web 버튼의 기존 `radius.md`와 같은 역할을 Native도 가까운 프로필에서 읽으며, 프로필이 없으면 기존 12를 유지한다. 선택 어댑터·비활성/진행 상태·파일 검증은 바꾸지 않는다.
122
123
 
123
124
  프로필 연결은 1.16.0 변경이며, 기본 배포 계약과 실제 Native 기기 검증은 구분한다.
125
+
126
+ ## 근거
127
+
128
+ - Web `onSelect`의 `files` 인자: 2026-10-17 목업 편집 화면 검토 — 제품이 고른 `File`을 받을 방법이 없어 화면이 브라우저 기본 `<input type="file">`(영어 "Choose File")로 조립돼 있었다.
@@ -0,0 +1,97 @@
1
+ # FilterRecovery
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [목록 편집 계약](../../collection-editing.md), `src/collection-editing.ts`의 `recoverableFilters`, 양 renderer `src/collection-editing.tsx`
9
+ - 스토리북: `배포/구성/피드백과 복구/검색 필터 완화`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 검색·필터 결과가 비었을 때 실제로 결과가 생기는 필터 완화 제안을 보여 주고, 사용자가 고른 하나를 명시적으로 적용할 때 쓴다.
14
+ `resultCount`가 0인 제안은 숨긴다. 적용 실패 시 현재 필터를 유지하고 같은 버튼으로 재시도한다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 완화 제안 없는 단순 빈 결과 | `EmptyState` |
21
+ | 검색 화면 전체 틀과 결과 없음 이유 | `SearchScreen` |
22
+ | 적용된 필터 목록 해제 | `SearchScreen`의 `appliedFilters` |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `FilterRecovery` | 결과가 있는 필터 완화 제안과 적용 | `@hjmds/react/collection-editing` | `@hjmds/react-native/collection-editing` |
29
+
30
+ `FilterRecoveryOption` 타입과 `recoverableFilters`는 `@hjmds/design-contracts/collection-editing`에 있다.
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { FilterRecovery } from "@hjmds/react/collection-editing";
37
+
38
+ {results.length === 0 ? <FilterRecovery title={t("search.noMatch")} description={t("search.relaxHint")}
39
+ options={relaxOptions /* 서버가 센 resultCount */}
40
+ optionLabel={option => t("search.relaxOption", { label: option.label, count: option.resultCount })}
41
+ errorLabel={t("search.applyFailed")} onApply={async id => { await applyRelaxation(id); }} /> : resultList}
42
+ ```
43
+
44
+ ```tsx
45
+ // Native
46
+ import { FilterRecovery } from "@hjmds/react-native/collection-editing";
47
+
48
+ {results.length === 0 ? <FilterRecovery title={t("search.noMatch")} description={t("search.relaxHint")}
49
+ options={relaxOptions}
50
+ optionLabel={option => t("search.relaxOption", { label: option.label, count: option.resultCount })}
51
+ errorLabel={t("search.applyFailed")} onApply={async id => { await applyRelaxation(id); }} /> : resultList}
52
+ ```
53
+
54
+ ## 축과 기본값
55
+
56
+ | prop | 값 | 기본값 | 설명 |
57
+ | --- | --- | --- | --- |
58
+ | title | string | 필수 | 결과 없음 제목(강조) |
59
+ | description | string | 없음 | 보조 설명(muted) |
60
+ | options | FilterRecoveryOption[] | 필수 | `{ id, label, resultCount }`. id는 서로 다르고 비지 않아야 함 |
61
+ | optionLabel | (option) => string | 필수 | 제안 버튼 문구 |
62
+ | onApply | (id) => void \| Promise | 필수 | 실제 필터 변경. 실패하면 reject |
63
+ | errorLabel | string | 필수 | 적용 실패 안내 |
64
+ | disabled | boolean | false | 모든 제안 비활성 |
65
+
66
+ ## 배치
67
+
68
+ | 항목 | 값 | 근거 |
69
+ | --- | --- | --- |
70
+ | 크기 | 기존 Button(secondary)·Text 규격 | 양 renderer |
71
+ | 간격 | 세로 sm | 양 renderer |
72
+ | 순서·정렬 | 제목 → 설명 → 제안 버튼들(`options` 순서) → 오류, 모두 가로·세로 중앙 | 양 renderer, VQ-09 |
73
+ | 고정·스크롤 | 결과 영역 host 소유 | 별도 scroller 없음 |
74
+ | 좁은 폭·큰 글자 | 버튼을 세로로 쌓아 각 행 전체 문구 유지 | 양 renderer |
75
+
76
+ 빈 결과 영역 안에 그대로 넣는다. 앱이 임의 상단 여백으로 위치를 맞추지 않는다.
77
+
78
+ ## 꼭 지킬 것
79
+
80
+ - `resultCount`는 제품 데이터에서 센 음이 아닌 안전 정수다. 어기면 렌더 중 `RangeError`가 난다. 추정치를 넣지 않는다.
81
+ - 중복·빈 `id`는 `TypeError`다.
82
+ - 적용 결과로 생긴 실제 목록은 제품이 다시 그린다. 이 구성은 결과를 만들지 않는다.
83
+
84
+ ## 상태와 상호작용
85
+
86
+ | 상태 | 모습 |
87
+ | --- | --- |
88
+ | 기본 | 결과가 있는 제안만 secondary 버튼으로 표시 |
89
+ | 적용 중 | 누른 버튼 로딩, 다른 제안 비활성 |
90
+ | 실패 | 필터 유지, 중앙 오류, 같은 버튼으로 재시도 |
91
+
92
+ ## 플랫폼 차이
93
+
94
+ | 항목 | Web | Native |
95
+ | --- | --- | --- |
96
+ | 그룹 | `role="group"`, 이름은 `title` | 접근성 그룹 없음 |
97
+ | 실패 알림 | `role="alert"` | `accessibilityRole="alert"`·assertive, iOS는 announce |
@@ -0,0 +1,100 @@
1
+ # FollowProfileCard
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [팔로우 프로필과 행사 참석 계약](../../social-compositions.md), `src/social-compositions.ts`의 `resolveFollowProposal`, 양 renderer `src/social-compositions.tsx`
9
+ - 스토리북: `배포/구성/정보 표시/팔로우 프로필`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 프로필 카드에서 팔로우·팔로잉 전환과 팔로워 수를 실제 mutation 결과와 함께 보여 줄 때 쓴다.
14
+ Lucian 054를 소비 앱마다 다시 조립하지 않도록 Card·Avatar·Button·AnimatedStatistic과 action-session을 합성했다.
15
+ 요청 중에는 제안 상태와 수치를 한 번 캡처해 낙관적으로 보여 주고, 실패하면 확정 상태로 돌아간다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 팔로우 행동이 없는 프로필 정보 | `Card` + `Avatar` 또는 `ProfileSummary` |
22
+ | 프로필 화면 전체 틀 | `ProfileScreen`(이 카드를 본문에 넣을 수 있음) |
23
+ | 행사 참석 응답 | `EventAttendanceCard` |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `FollowProfileCard` | 프로필·팔로워 수·팔로우 전환 | `@hjmds/react/social-compositions` | `@hjmds/react-native/social-compositions` |
30
+
31
+ `FollowProfileState`와 `resolveFollowProposal`은 `@hjmds/design-contracts/social-compositions`에 있다. root barrel에는 없다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { FollowProfileCard } from "@hjmds/react/social-compositions";
38
+
39
+ <FollowProfileCard profileId={profile.id} name={profile.name} description={profile.bio}
40
+ avatar={<ProfileImage src={profile.avatarUrl} />} locale={locale} state={{ following, count }}
41
+ labels={{ follow: t("follow"), following: t("following"), pending: t("followPending"),
42
+ count: t("followers"), error: t("followFailed") }}
43
+ onFollowChange={async next => { const confirmed = await api.setFollow(profile.id, next); setFollowState(confirmed); }} />
44
+ ```
45
+
46
+ ```tsx
47
+ // Native
48
+ import { FollowProfileCard } from "@hjmds/react-native/social-compositions";
49
+
50
+ <FollowProfileCard profileId={profile.id} name={profile.name} description={profile.bio}
51
+ avatar={<ProfileImage source={{ uri: profile.avatarUrl }} />} locale={locale} state={{ following, count }}
52
+ labels={followLabels}
53
+ onFollowChange={async next => { const confirmed = await api.setFollow(profile.id, next); setFollowState(confirmed); }} />
54
+ ```
55
+
56
+ ## 축과 기본값
57
+
58
+ | prop | 값 | 기본값 | 설명 |
59
+ | --- | --- | --- | --- |
60
+ | profileId | string | 필수 | 비면 `TypeError`. 바뀌면 작업 범위를 remount |
61
+ | name | string | 필수 | Card 제목, 기본 Avatar 이름 |
62
+ | description | ReactNode | 없음 | Card 설명 |
63
+ | avatar | ReactNode | 이름 기반 `Avatar`(장식) | 제품 이미지 host |
64
+ | state | `{ following, count }` | 필수 | 서버가 확정한 상태. count는 현재 사용자 포함 |
65
+ | locale | string | 필수 | 수치 형식 |
66
+ | labels | follow·following·pending·count·error | 필수 | 현지화 문구 |
67
+ | onFollowChange | (following) => void \| Promise | 필수 | 실제 요청. 성공 전에 확정 state로 갱신, 실패는 reject |
68
+ | disabled | boolean | false | 버튼 비활성 |
69
+
70
+ ## 배치
71
+
72
+ | 항목 | 값 | 근거 |
73
+ | --- | --- | --- |
74
+ | 크기 | Card·Avatar·Button 기본 규격 | 양 renderer |
75
+ | 간격 | 카드 본문 세로 md | 양 renderer |
76
+ | 순서·정렬 | Card(leading 아바타, 제목, 설명) → 팔로워 수(AnimatedStatistic) → 팔로우 버튼 → 오류(가로 중앙) | 양 renderer |
77
+ | 고정·스크롤 | 바깥 화면 host 소유 | 별도 scroller 없음 |
78
+ | 좁은 폭·큰 글자 | Card 기본 배치를 따름 | Card |
79
+
80
+ ## 꼭 지킬 것
81
+
82
+ - `state`는 서버 확정값만 전달한다. `following: true`인데 `count: 0`이면 `RangeError`다.
83
+ - 성공 callback 안에서 서버 snapshot으로 state를 갱신한 뒤 resolve한다. HJM이 성공 수치를 만들지 않는다.
84
+ - 다른 프로필로 바뀌면 `profileId`를 바꾼다. 이전 프로필의 늦은 응답은 새 카드의 오류를 바꾸지 않지만, 제품 캐시 기록은 제품이 맞춘다.
85
+
86
+ ## 상태와 상호작용
87
+
88
+ | 상태 | 모습 |
89
+ | --- | --- |
90
+ | 기본 | 확정 수치, `following`이면 selected 버튼과 following 문구 |
91
+ | 진행 중 | 제안 상태·수치(±1)를 한 번 캡처해 표시, 버튼 로딩. 같은 요청 중복은 session이 직렬화 |
92
+ | 실패 | 확정 props로 복원, 중앙 오류, 같은 버튼으로 재시도 |
93
+
94
+ ## 플랫폼 차이
95
+
96
+ | 항목 | Web | Native |
97
+ | --- | --- | --- |
98
+ | 진행 중 이름 | 버튼 `aria-label`=pending | Button `loadingLabel`=pending |
99
+ | 기본 아바타 | `Avatar aria-hidden` | `Avatar decorative` |
100
+ | 수치 전환 의존성 | `statistic-motion`의 `@number-flow/react` peer 필요 | 기존 ContentTransition |
@@ -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
  - 근거: [Heading](../../heading.md), recipe `headingRecipe`(`src/heading.ts`)
9
9
  - 스토리북: `배포/컴포넌트/글자와 아이콘/제목`
10
10
 
@@ -48,7 +48,7 @@ import { Heading } from "@hjmds/react-native/heading";
48
48
 
49
49
  | prop | 값 | 기본값 | 설명 |
50
50
  | --- | --- | --- | --- |
51
- | `level`(필수) | `level1` 40px · `level2` 32px · `level3` 24px · `level4` 20px · `level5` 18px | — | recipe 기본은 `level3`이지만 두 renderer 모두 prop을 필수로 받는다 |
51
+ | `level`(필수) | `level1` 40px/700 · `level2` 32px/700 · `level3` 24px/600 · `level4` 20px/600 · `level5` 18px/600 | — | recipe 기본은 `level3`이지만 두 renderer 모두 prop을 필수로 받는다 |
52
52
  | `semanticLevel` | 1~6 | `level`의 숫자 | 크게 보이지만 구조상 h4인 카드 제목처럼 시각 크기와 문서 단계가 다르면 둘을 따로 적는다 |
53
53
  | `layoutStyle` | 배치 key(margin·폭·정렬 등) | — | 바깥 배치만. 크기·줄 높이·굵기·색은 받지 않는다 |
54
54
 
@@ -86,6 +86,10 @@ import { Heading } from "@hjmds/react-native/heading";
86
86
  | 배치 | `layoutStyle`. `style`도 합쳐진다(크기 변수는 덮이지 않음) | `layoutStyle`. `style`(TextStyle)은 제거된 속성 — `layoutStyle` 또는 `level` |
87
87
  | 글자 배율 | `--hjm-text-scale`로 확대 | Native `Text`의 글자 배율 |
88
88
 
89
+ ## 근거
90
+
91
+ 2026-10-17 사용자 결정: `level1`·`level2`는 Pretendard 800이 큰 크기에서 무겁고 거칠어 700(bold)으로 낮췄다. 같은 날 600으로 낮춘 `level3`~`level5`보다 한 단계 굵게 유지한다. Native는 `fonts="pretendard"`일 때 `Pretendard-Bold` face로 그린다. `Top size="large"` 제목도 `heading.level2`라 함께 700이 된다. 브루탈리즘 프리셋은 level1·2를 800으로 명시한다.
92
+
89
93
  ## 함정
90
94
 
91
95
  - Web `style`은 이제 버려지지 않고 합쳐진다. 다만 크기·줄 높이·굵기 변수(`--hjm-heading-*`)는 recipe가 마지막에 덮으므로
@@ -0,0 +1,89 @@
1
+ # HistogramExplorer
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-16
8
+ - 근거: [데이터 해석과 가져오기 계약](../../data-workflows.md), `src/data-workflows.ts`의 `binHistogram`, 양 renderer `src/data-workflows.tsx`
9
+ - 스토리북: `배포/구성/정보 표시/분포 구간 탐색`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 실제 관측값 배열의 분포를 동폭 구간 막대로 보여 주고, 사용자가 구간 수를 바꿔 볼 때 쓴다.
14
+ Lucian 130·160–164 검토에서 별도 차트 엔진 대신 BarChart를 합성하기로 한 데이터 구성의 하나다.
15
+ 구간 수를 바꿔도 모든 선택지의 최대 count로 y축을 고정해 막대 높이의 의미가 바뀌지 않는다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 이미 집계된 범주 값 | `BarChart` |
22
+ | 시간에 따른 추이 | `LineChart` |
23
+ | 단계별 감소 | `ConversionFunnel` |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `HistogramExplorer` | 구간 수 선택 + 분포 막대 + 요약 | `@hjmds/react/data-workflows` | `@hjmds/react-native/data-workflows` |
30
+
31
+ `binHistogram`·`HistogramBin`은 `@hjmds/design-contracts/data-workflows`에 있다. root barrel에는 없다.
32
+
33
+ ## 최소 사용 예
34
+
35
+ ```tsx
36
+ // Web
37
+ import { HistogramExplorer } from "@hjmds/react/data-workflows";
38
+
39
+ <HistogramExplorer label={t("stats.duration")} samples={durations /* null = 미관측 */} domain={[0, 10]}
40
+ binCounts={[2, 4, 8]} binCount={binCount} onBinCountChange={setBinCount}
41
+ binLabel={bin => `${format(bin.lower)}–${format(bin.upper)}`} binCountLabel={count => t("stats.bins", { count })}
42
+ formatCount={String} missingLabel={t("stats.unobserved")}
43
+ summaryLabel={h => t("stats.summary", { observed: h.observed, missing: h.missing, outside: h.underflow + h.overflow })} />
44
+ ```
45
+
46
+ ```tsx
47
+ // Native
48
+ import { HistogramExplorer } from "@hjmds/react-native/data-workflows";
49
+
50
+ <HistogramExplorer label={t("stats.duration")} samples={durations} domain={[0, 10]}
51
+ binCounts={[2, 4, 8]} binCount={binCount} onBinCountChange={setBinCount}
52
+ binLabel={binLabel} binCountLabel={binCountLabel} formatCount={String}
53
+ missingLabel={t("stats.unobserved")} summaryLabel={summaryLabel} />
54
+ ```
55
+
56
+ ## 축과 기본값
57
+
58
+ | prop | 값 | 기본값 | 설명 |
59
+ | --- | --- | --- | --- |
60
+ | label | string | 필수 | 제목, 차트 접근성 이름 |
61
+ | samples | (number \| null)[] | 필수 | 유한 값 또는 null(미관측) |
62
+ | binCounts | number[] | 필수 | 서로 다른 구간 수 선택지(각 1–100), `binCount` 포함 |
63
+ | binCount / onBinCountChange | number / fn | 필수 | 선택한 구간 수(제품 소유) |
64
+ | domain | [number, number] | 관측 최소·최대 | 지정하면 범위 밖 값을 underflow/overflow로 셈 |
65
+ | binLabel / binCountLabel / formatCount | fn | 필수 | 구간·선택지·count 문구 |
66
+ | missingLabel | string | 필수 | 관측값이 없을 때와 차트 결측 문구 |
67
+ | summaryLabel | (histogram) => string | 필수 | observed·missing·underflow·overflow 요약 |
68
+
69
+ ## 배치
70
+
71
+ | 항목 | 값 | 근거 |
72
+ | --- | --- | --- |
73
+ | 크기 | BarChart 기본 규격, 값 표시(`showValues`) | 양 renderer |
74
+ | 간격 | 세로 md, 구간 선택 버튼 sm | 양 renderer |
75
+ | 순서·정렬 | 제목 → 구간 수 버튼(secondary, 선택 강조) → 막대 → 요약 | 양 renderer |
76
+ | 고정·스크롤 | 바깥 화면 host 소유 | 별도 scroller 없음 |
77
+ | 좁은 폭·큰 글자 | 구간 수 버튼 `wrap` 줄바꿈 | 양 renderer |
78
+
79
+ 관측값이 하나도 없으면 막대 대신 `missingLabel`을 가로 중앙에 둔다.
80
+
81
+ ## 꼭 지킬 것
82
+
83
+ - `binCounts`가 비었거나, `binCount`를 포함하지 않거나, 중복이 있으면 `RangeError`다.
84
+ - 구간은 동폭이며 마지막 구간만 상한을 포함한다. 무한대·NaN 샘플은 `RangeError`다.
85
+ - y축은 모든 선택지의 최대 count로 고정된다. 앱이 구간마다 다시 정규화하지 않는다.
86
+
87
+ ## 상태와 상호작용
88
+
89
+ 구간 수 버튼은 `onBinCountChange`만 호출한다. 요약은 Web `role="status"`, Native polite live region으로 갱신을 알린다.