@hjmds/design-contracts 1.12.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/dist/avatar-fallback.d.ts +11 -0
  2. package/dist/avatar-fallback.d.ts.map +1 -1
  3. package/dist/avatar-fallback.js +21 -0
  4. package/dist/avatar-fallback.js.map +1 -1
  5. package/dist/base-recipes.d.ts +17 -0
  6. package/dist/base-recipes.d.ts.map +1 -1
  7. package/dist/base-recipes.js +17 -0
  8. package/dist/base-recipes.js.map +1 -1
  9. package/dist/catalog.d.ts +25 -0
  10. package/dist/catalog.d.ts.map +1 -1
  11. package/dist/command-palette.d.ts +14 -9
  12. package/dist/command-palette.d.ts.map +1 -1
  13. package/dist/command-palette.js +8 -9
  14. package/dist/command-palette.js.map +1 -1
  15. package/dist/component-recipes.d.ts +17 -0
  16. package/dist/component-recipes.d.ts.map +1 -1
  17. package/dist/component-recipes.js +5 -0
  18. package/dist/component-recipes.js.map +1 -1
  19. package/dist/provider-button.d.ts.map +1 -1
  20. package/dist/provider-button.js +3 -0
  21. package/dist/provider-button.js.map +1 -1
  22. package/dist/reactions.d.ts +10 -0
  23. package/dist/reactions.d.ts.map +1 -1
  24. package/dist/reactions.js +7 -0
  25. package/dist/reactions.js.map +1 -1
  26. package/dist/screen-patterns.d.ts +147 -0
  27. package/dist/screen-patterns.d.ts.map +1 -0
  28. package/dist/screen-patterns.js +149 -0
  29. package/dist/screen-patterns.js.map +1 -0
  30. package/dist/slider.d.ts +8 -0
  31. package/dist/slider.d.ts.map +1 -1
  32. package/dist/slider.js +6 -1
  33. package/dist/slider.js.map +1 -1
  34. package/dist/upload-item.d.ts +5 -0
  35. package/dist/upload-item.d.ts.map +1 -1
  36. package/dist/upload-item.js +5 -0
  37. package/dist/upload-item.js.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/dist/version.js.map +1 -1
  41. package/docs/action-session.md +3 -3
  42. package/docs/agreement.md +5 -0
  43. package/docs/avatar-fallback.md +7 -0
  44. package/docs/bottom-navigation.md +6 -0
  45. package/docs/brand-boundary.md +1 -1
  46. package/docs/button-label.md +6 -0
  47. package/docs/clipboard.md +3 -0
  48. package/docs/command-palette.md +45 -2
  49. package/docs/consumer-policy.md +5 -1
  50. package/docs/data-table.md +6 -4
  51. package/docs/dialog.md +8 -2
  52. package/docs/form.md +51 -0
  53. package/docs/generated/component-maturity.md +1 -1
  54. package/docs/generated/renderer-evidence.json +3 -3
  55. package/docs/generated/renderer-evidence.md +1 -1
  56. package/docs/generated/showcase-manifest.json +1 -1
  57. package/docs/link.md +8 -0
  58. package/docs/migration-native-legacy-removal.md +45 -1
  59. package/docs/optional-adapters.md +1 -1
  60. package/docs/password-field.md +5 -0
  61. package/docs/product-composition-adoption.md +40 -0
  62. package/docs/progress.md +19 -1
  63. package/docs/provider-button.md +13 -0
  64. package/docs/result.md +3 -0
  65. package/docs/screen-chrome.md +10 -0
  66. package/docs/screen-patterns.md +376 -0
  67. package/docs/sheet.md +12 -0
  68. package/docs/splitter.md +8 -2
  69. package/docs/theming.md +36 -29
  70. package/docs/toggle-group.md +7 -0
  71. package/docs/tour.md +7 -1
  72. package/docs/tree.md +5 -2
  73. package/docs/upload-item.md +7 -0
  74. package/docs/usage/README.md +236 -0
  75. package/docs/usage/STANDARD.md +108 -0
  76. package/docs/usage/components/accordion.md +107 -0
  77. package/docs/usage/components/activity-heatmap.md +104 -0
  78. package/docs/usage/components/affix.md +86 -0
  79. package/docs/usage/components/agreement.md +129 -0
  80. package/docs/usage/components/alert-dialog.md +130 -0
  81. package/docs/usage/components/anchor.md +96 -0
  82. package/docs/usage/components/aspect-ratio.md +89 -0
  83. package/docs/usage/components/asset.md +126 -0
  84. package/docs/usage/components/auth-provider-button.md +116 -0
  85. package/docs/usage/components/auth-screen-layout.md +129 -0
  86. package/docs/usage/components/avatar.md +114 -0
  87. package/docs/usage/components/badge.md +84 -0
  88. package/docs/usage/components/bottom-cta.md +125 -0
  89. package/docs/usage/components/bottom-info.md +99 -0
  90. package/docs/usage/components/bottom-navigation.md +136 -0
  91. package/docs/usage/components/breadcrumb.md +81 -0
  92. package/docs/usage/components/button.md +118 -0
  93. package/docs/usage/components/calendar.md +122 -0
  94. package/docs/usage/components/card.md +110 -0
  95. package/docs/usage/components/carousel.md +113 -0
  96. package/docs/usage/components/celebration.md +96 -0
  97. package/docs/usage/components/chat-message.md +122 -0
  98. package/docs/usage/components/chat-screen.md +112 -0
  99. package/docs/usage/components/checkbox-group.md +104 -0
  100. package/docs/usage/components/checkbox.md +103 -0
  101. package/docs/usage/components/chip.md +104 -0
  102. package/docs/usage/components/code-block.md +111 -0
  103. package/docs/usage/components/collapsible.md +112 -0
  104. package/docs/usage/components/color-picker.md +86 -0
  105. package/docs/usage/components/combobox.md +137 -0
  106. package/docs/usage/components/command-palette.md +125 -0
  107. package/docs/usage/components/comment-thread-screen.md +125 -0
  108. package/docs/usage/components/container.md +98 -0
  109. package/docs/usage/components/content-transition.md +101 -0
  110. package/docs/usage/components/context-menu.md +136 -0
  111. package/docs/usage/components/counter-badge.md +107 -0
  112. package/docs/usage/components/data-table.md +122 -0
  113. package/docs/usage/components/date-picker.md +142 -0
  114. package/docs/usage/components/date-range-picker.md +111 -0
  115. package/docs/usage/components/description-list.md +103 -0
  116. package/docs/usage/components/design-system-provider.md +124 -0
  117. package/docs/usage/components/dialog.md +176 -0
  118. package/docs/usage/components/divider.md +89 -0
  119. package/docs/usage/components/editor-screen.md +126 -0
  120. package/docs/usage/components/effect-surface.md +120 -0
  121. package/docs/usage/components/empty-state.md +114 -0
  122. package/docs/usage/components/field.md +129 -0
  123. package/docs/usage/components/file-picker.md +114 -0
  124. package/docs/usage/components/floating-action-button.md +138 -0
  125. package/docs/usage/components/form.md +162 -0
  126. package/docs/usage/components/grid.md +99 -0
  127. package/docs/usage/components/heading.md +87 -0
  128. package/docs/usage/components/icon-button.md +126 -0
  129. package/docs/usage/components/icon.md +105 -0
  130. package/docs/usage/components/image.md +122 -0
  131. package/docs/usage/components/keyboard-avoiding.md +93 -0
  132. package/docs/usage/components/keyboard-dock.md +110 -0
  133. package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
  134. package/docs/usage/components/keyboard-motion-provider.md +86 -0
  135. package/docs/usage/components/layout.md +117 -0
  136. package/docs/usage/components/link.md +121 -0
  137. package/docs/usage/components/list-detail-screen.md +103 -0
  138. package/docs/usage/components/list-row.md +124 -0
  139. package/docs/usage/components/list.md +119 -0
  140. package/docs/usage/components/load-more.md +115 -0
  141. package/docs/usage/components/masonry.md +109 -0
  142. package/docs/usage/components/media-selection-screen.md +119 -0
  143. package/docs/usage/components/mentions.md +119 -0
  144. package/docs/usage/components/menu.md +129 -0
  145. package/docs/usage/components/menubar.md +93 -0
  146. package/docs/usage/components/message-composer.md +124 -0
  147. package/docs/usage/components/moderation-screen.md +113 -0
  148. package/docs/usage/components/notice.md +106 -0
  149. package/docs/usage/components/notification-inbox-screen.md +97 -0
  150. package/docs/usage/components/notification-item.md +98 -0
  151. package/docs/usage/components/number-field.md +131 -0
  152. package/docs/usage/components/onboarding-screen.md +106 -0
  153. package/docs/usage/components/otp-field.md +101 -0
  154. package/docs/usage/components/pagination.md +82 -0
  155. package/docs/usage/components/password-field.md +137 -0
  156. package/docs/usage/components/permission-screen.md +107 -0
  157. package/docs/usage/components/photo-source-sheet.md +119 -0
  158. package/docs/usage/components/popover.md +108 -0
  159. package/docs/usage/components/profile-screen.md +89 -0
  160. package/docs/usage/components/progress.md +122 -0
  161. package/docs/usage/components/qr-code.md +122 -0
  162. package/docs/usage/components/radio-group.md +124 -0
  163. package/docs/usage/components/radio.md +104 -0
  164. package/docs/usage/components/result.md +116 -0
  165. package/docs/usage/components/saved-items-screen.md +126 -0
  166. package/docs/usage/components/screen-layout.md +119 -0
  167. package/docs/usage/components/search-field.md +120 -0
  168. package/docs/usage/components/search-screen.md +215 -0
  169. package/docs/usage/components/section.md +111 -0
  170. package/docs/usage/components/segmented-control.md +138 -0
  171. package/docs/usage/components/select.md +142 -0
  172. package/docs/usage/components/settings-screen.md +126 -0
  173. package/docs/usage/components/shared-transition-element.md +111 -0
  174. package/docs/usage/components/shared-transition-screen.md +86 -0
  175. package/docs/usage/components/sheet.md +151 -0
  176. package/docs/usage/components/side-panel.md +104 -0
  177. package/docs/usage/components/sidebar.md +107 -0
  178. package/docs/usage/components/skeleton.md +105 -0
  179. package/docs/usage/components/skip-nav.md +76 -0
  180. package/docs/usage/components/slider.md +121 -0
  181. package/docs/usage/components/sortable-collection.md +127 -0
  182. package/docs/usage/components/spinner.md +86 -0
  183. package/docs/usage/components/splitter.md +103 -0
  184. package/docs/usage/components/stack.md +93 -0
  185. package/docs/usage/components/statistic.md +123 -0
  186. package/docs/usage/components/steps.md +110 -0
  187. package/docs/usage/components/surface.md +91 -0
  188. package/docs/usage/components/swipe-actions.md +124 -0
  189. package/docs/usage/components/switch.md +120 -0
  190. package/docs/usage/components/tabs.md +134 -0
  191. package/docs/usage/components/tag.md +84 -0
  192. package/docs/usage/components/tags-input.md +111 -0
  193. package/docs/usage/components/text-area.md +112 -0
  194. package/docs/usage/components/text-format.md +75 -0
  195. package/docs/usage/components/text-transition.md +104 -0
  196. package/docs/usage/components/text.md +101 -0
  197. package/docs/usage/components/thinking-orb.md +105 -0
  198. package/docs/usage/components/timeline.md +105 -0
  199. package/docs/usage/components/toast.md +145 -0
  200. package/docs/usage/components/toggle-group.md +95 -0
  201. package/docs/usage/components/tooltip.md +103 -0
  202. package/docs/usage/components/top-bar.md +124 -0
  203. package/docs/usage/components/top.md +89 -0
  204. package/docs/usage/components/tour.md +118 -0
  205. package/docs/usage/components/transfer-list.md +115 -0
  206. package/docs/usage/components/tree.md +91 -0
  207. package/docs/usage/components/upload-item.md +99 -0
  208. package/docs/usage/components/virtual-list.md +105 -0
  209. package/docs/usage/components/visually-hidden.md +72 -0
  210. package/docs/usage/components/watermark.md +78 -0
  211. package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
  212. package/docs/usage/compositions/action-recovery-save.md +235 -0
  213. package/docs/usage/compositions/action-recovery-undo.md +193 -0
  214. package/docs/usage/compositions/common-message.md +132 -0
  215. package/docs/usage/compositions/common-notification.md +101 -0
  216. package/docs/usage/compositions/compound-controls.md +186 -0
  217. package/docs/usage/compositions/data-layouts.md +157 -0
  218. package/docs/usage/compositions/disclosure.md +144 -0
  219. package/docs/usage/compositions/environment-matrix.md +139 -0
  220. package/docs/usage/compositions/expo-interactions.md +149 -0
  221. package/docs/usage/compositions/family-drawer.md +201 -0
  222. package/docs/usage/compositions/floating-action-button.md +197 -0
  223. package/docs/usage/compositions/input-sheet.md +148 -0
  224. package/docs/usage/compositions/interaction-adapters.md +190 -0
  225. package/docs/usage/compositions/interaction-flow-apply.md +205 -0
  226. package/docs/usage/compositions/interaction-flow-draft.md +188 -0
  227. package/docs/usage/compositions/interaction-flow-search.md +171 -0
  228. package/docs/usage/compositions/native-renderers.md +106 -0
  229. package/docs/usage/compositions/navigation-bar-collection.md +164 -0
  230. package/docs/usage/compositions/optional-adapters.md +169 -0
  231. package/docs/usage/compositions/optional-motion.md +109 -0
  232. package/docs/usage/compositions/photo-source.md +104 -0
  233. package/docs/usage/compositions/purpose-input-comment.md +110 -0
  234. package/docs/usage/compositions/purpose-input-message.md +119 -0
  235. package/docs/usage/compositions/reference-first.md +96 -0
  236. package/docs/usage/compositions/reference-review.md +107 -0
  237. package/docs/usage/compositions/reference-settings.md +107 -0
  238. package/docs/usage/compositions/selection-scope.md +174 -0
  239. package/docs/usage/compositions/stea-event-ticket.md +166 -0
  240. package/docs/usage/compositions/stea-flip-card.md +162 -0
  241. package/docs/usage/compositions/stea-order-progress.md +184 -0
  242. package/docs/usage/compositions/stea-otp-verify.md +215 -0
  243. package/docs/usage/compositions/stea-pixel-empty.md +140 -0
  244. package/docs/usage/compositions/stea-schedule-card.md +169 -0
  245. package/docs/usage/compositions/stea-stat-summary.md +154 -0
  246. package/docs/usage/compositions/time-selection.md +174 -0
  247. package/docs/usage/compositions/toast-layout.md +128 -0
  248. package/docs/usage/compositions/visual-foundations.md +185 -0
  249. package/docs/usage/compositions/web-additions.md +146 -0
  250. package/docs/usage/compositions/web-navigation.md +143 -0
  251. package/docs/usage/screens/common-chat.md +127 -0
  252. package/docs/usage/screens/common-comments.md +108 -0
  253. package/docs/usage/screens/common-inbox.md +110 -0
  254. package/docs/usage/screens/common-login.md +98 -0
  255. package/docs/usage/screens/common-profile.md +221 -0
  256. package/docs/usage/screens/common-saved.md +127 -0
  257. package/docs/usage/screens/common-search.md +274 -0
  258. package/docs/usage/screens/common-settings.md +126 -0
  259. package/docs/usage/screens/common-shell.md +108 -0
  260. package/docs/usage/screens/dashboard.md +245 -0
  261. package/docs/usage/screens/discovery-gallery.md +306 -0
  262. package/docs/usage/screens/flow-collection.md +96 -0
  263. package/docs/usage/screens/flow-editor.md +120 -0
  264. package/docs/usage/screens/flow-media.md +111 -0
  265. package/docs/usage/screens/flow-moderation.md +120 -0
  266. package/docs/usage/screens/flow-onboarding.md +193 -0
  267. package/docs/usage/screens/flow-permission.md +103 -0
  268. package/docs/usage/screens/landing.md +347 -0
  269. package/docs/usage/screens/mockup-studio.md +190 -0
  270. package/docs/usage/screens/notification-settings.md +206 -0
  271. package/docs/usage/screens/reference-comparison.md +159 -0
  272. package/docs/usage/templates/component.md +61 -0
  273. package/docs/usage/templates/composition.md +47 -0
  274. package/docs/usage/templates/screen.md +56 -0
  275. package/docs/usage/templates/token.md +32 -0
  276. package/docs/usage/tokens/color.md +142 -0
  277. package/docs/usage/tokens/elevation-opacity.md +86 -0
  278. package/docs/usage/tokens/layers.md +98 -0
  279. package/docs/usage/tokens/layout.md +114 -0
  280. package/docs/usage/tokens/motion.md +88 -0
  281. package/docs/usage/tokens/radius.md +53 -0
  282. package/docs/usage/tokens/size.md +74 -0
  283. package/docs/usage/tokens/spacing.md +73 -0
  284. package/docs/usage/tokens/stroke.md +50 -0
  285. package/docs/usage/tokens/theme-studio.md +70 -0
  286. package/docs/usage/tokens/typography-studio.md +70 -0
  287. package/docs/usage/tokens/typography.md +89 -0
  288. package/package.json +7 -1
@@ -0,0 +1,101 @@
1
+ # ContentTransition
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: contract `src/content-transition.ts`(`resolveContentTransition`)
9
+ - 스토리북: `배포/컴포넌트/시각 효과/내용 전환`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 같은 자리의 내용이 상태에 따라 바뀔 때(필터 결과 패널, 단계별 본문) 새 내용이 짧게 나타나도록 감싼다.
14
+ `stateKey`가 바뀔 때만 움직이고, 화면에는 현재 내용 하나만 남는다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 바뀌는 것이 텍스트 한 줄 | [TextTransition](text-transition.md) |
21
+ | 화면 사이 이동 | [SharedTransitionScreen](shared-transition-screen.md) |
22
+ | 내용이 아직 로딩 중 | [Skeleton](skeleton.md) |
23
+ | 내용을 펼치고 접기 | [Collapsible](collapsible.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `ContentTransition` | 보조 — 별도 보조 기능(supplemental) | `/content-transition` | `/content-transition` |
30
+ | `TextTransition` | 동반 — 텍스트 전용, [별도 지침](text-transition.md) | `/content-transition` | `/content-transition` |
31
+
32
+ root에서 export되지 않고 granular subpath로만 import 된다. Web은 optional peer `framer-motion`이 필요하다.
33
+ Native는 React Native `Animated`만 써서 추가 peer가 없다.
34
+
35
+ ## 최소 사용 예
36
+
37
+ ```tsx
38
+ // Web
39
+ import { ContentTransition } from "@hjmds/react/content-transition";
40
+ import { useRef } from "react";
41
+
42
+ // 상태 → 키 상수 표. 키를 템플릿 문자열로 만들지 않는다.
43
+ const titleKey = { all: "results.all.title", unread: "results.unread.title" } as const;
44
+
45
+ function Results({ filter }: { filter: keyof typeof titleKey }) {
46
+ const heading = useRef<HTMLHeadingElement>(null);
47
+ return (
48
+ <ContentTransition stateKey={filter} preset="rise" focusTarget={heading}>
49
+ <h2 ref={heading} tabIndex={-1}>{t(titleKey[filter])}</h2>
50
+ <ResultList filter={filter} />
51
+ </ContentTransition>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ```tsx
57
+ // Native
58
+ import { ContentTransition } from "@hjmds/react-native/content-transition";
59
+
60
+ <ContentTransition stateKey={step} preset="slide">
61
+ <StepBody step={step} />
62
+ </ContentTransition>
63
+ ```
64
+
65
+ ## 축과 기본값
66
+
67
+ | prop | 값 | 기본값 | 설명 |
68
+ | --- | --- | --- | --- |
69
+ | `preset` | `fade` · `rise` · `slide` · `scale` | `fade` | `rise`는 아래 12에서, `slide`는 가로 16(RTL이면 반대), `scale`은 0.96에서 시작 |
70
+ | `motion` | `system` · `none` | `system` | `system`은 reduced motion을 따르고 `none`은 항상 즉시 교체 |
71
+ | `stateKey` | `string` | — (필수) | 바뀔 때만 새 내용이 나타난다 |
72
+ | Web `focusTarget` | `RefObject<HTMLElement \| null>` | — | 바뀌기 전 포커스가 안에 있었으면 전환 뒤 이 요소로 옮긴다 |
73
+ | Web `layoutStyle` | 배치 전용 style | — | 바깥 고정 wrapper에 붙는다(키가 바뀌는 안쪽 패널이 아님) |
74
+
75
+ 콜백 prop은 없다. `TextTransition`은 `text: string`을 `stateKey`로 쓴다.
76
+
77
+ - 첫 렌더는 움직이지 않는다. 시간은 `motion.normal`, 곡선은 `easing.enter` 토큰이다.
78
+
79
+ ## 배치
80
+
81
+ | 항목 | 값 | 근거 |
82
+ | --- | --- | --- |
83
+ | 크기 | 자체 크기·여백이 없다. Web은 `div` 두 겹(블록), Native는 `Animated.View` 하나로 감싼다 | `packages/react/src/content-transition.tsx`, `packages/react-native/src/content-transition.tsx` |
84
+ | 간격 | 자체 간격이 없다. 위아래 간격은 감싸는 [Stack](stack.md) 등이 정한다. 움직임 폭(세로 12·가로 16·0.96배)만큼 래퍼 밖으로 잠깐 밀려 나오므로 바로 옆 요소와 간격을 둔다 | `src/content-transition.ts` |
85
+ | 순서·정렬 | 바뀌는 영역 하나만 감싼다(결과 패널, 단계 본문). 필터 막대·탭·제목처럼 그대로 남는 부분은 바깥에 둔다 | — |
86
+ | 고정·스크롤 | Native 래퍼에는 `flex`가 없어 남은 높이를 채우지 않는다. 화면 높이를 채워야 하는 내용이면 바깥 View가 높이를 정한다 | `packages/react-native/src/content-transition.tsx` |
87
+ | 좁은 폭·큰 글자 | — | — |
88
+
89
+ ## 꼭 지킬 것
90
+
91
+ - `stateKey`는 내용의 의미가 바뀔 때만 바꾼다. 매 렌더 새 값을 주면 계속 다시 나타난다.
92
+ - 사라지는 내용의 복사본을 남기지 않는 계약이다. 교차 페이드를 직접 만들려고 두 겹을 겹치지 않는다.
93
+ - Web 배치는 `layoutStyle`(바깥 wrapper)로 한다. Native는 배치 prop(`style`·`layoutStyle`)이 없어 바깥 View가 배치한다.
94
+
95
+ ## 플랫폼 차이
96
+
97
+ | 항목 | Web | Native |
98
+ | --- | --- | --- |
99
+ | 포커스 복원 | `focusTarget`: 바뀌기 전 포커스가 안에 있었으면 그 요소로 옮긴다 | 없음 |
100
+ | 앱이 백그라운드로 감 | 해당 없음 | 진행 중 전환을 멈추고 바로 표시 |
101
+ | 배치 prop | `layoutStyle`(바깥 wrapper) | 없음 |
@@ -0,0 +1,136 @@
1
+ # ContextMenu
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [ContextMenu](../../context-menu.md), [선택 어댑터](../../optional-adapters.md), recipe `menuRecipe`
9
+ - 스토리북: `배포/컴포넌트/탐색/상황별 메뉴`
10
+
11
+ ## 언제 쓰나
12
+
13
+ Web에서 제품이 소유한 영역(카드·목록 행·캔버스)의 우클릭·길게 누르기·Shift+F10에 명령 목록을
14
+ 띄울 때 쓴다. 같은 명령은 화면의 다른 경로(버튼·Menu)에도 있어야 한다. Native canonical 구현은 없고,
15
+ OS 길게 누르기 메뉴가 필요하면 선택 어댑터 `NativeContextMenu`를 쓴다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 버튼을 눌러 여는 명령 목록 | [Menu](menu.md) |
22
+ | 앱 상단 메뉴 막대(Web) | [Menubar](menubar.md) |
23
+ | 목록 행을 밀어 나오는 행동 | [SwipeActions](swipe-actions.md) |
24
+ | 텍스트 선택·링크·이미지 위의 브라우저 기본 메뉴 대체 | 쓰지 않는다([계약](../../context-menu.md)) |
25
+ | Native에서 추가 의존성 없이 행동 목록 | [Menu](menu.md), [Sheet](sheet.md) |
26
+
27
+ ## 공개 이름과 import
28
+
29
+ | 이름 | 역할 | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `ContextMenu` | 기본 — Web 전용 | `@hjmds/react`, `/context-menu` | 없음 |
32
+ | `NativeContextMenu` | 확장 — OS 길게 누르기 메뉴 선택 어댑터 | 없음 | `/context-menu-native` |
33
+
34
+ `NativeContextMenu`는 root에서 재노출되지 않는다. `@hjmds/react-native/context-menu-native`로만 import한다.
35
+
36
+ ## 최소 사용 예
37
+
38
+ ```tsx
39
+ // Web
40
+ import { ContextMenu } from "@hjmds/react/context-menu";
41
+
42
+ <ContextMenu
43
+ accessibilityLabel={t("post.actions")}
44
+ items={[
45
+ { id: "share", label: t("post.share"), textValue: t("post.share") },
46
+ { id: "delete", label: t("post.delete"), textValue: t("post.delete"), tone: "danger" },
47
+ ]}
48
+ onAction={(id) => runPostAction(post, id)}
49
+ >
50
+ <PostCard post={post} />
51
+ </ContextMenu>
52
+ ```
53
+
54
+ ```tsx
55
+ // Native (선택 어댑터)
56
+ import { NativeContextMenu } from "@hjmds/react-native/context-menu-native";
57
+ import { Pressable } from "react-native";
58
+
59
+ <NativeContextMenu
60
+ items={[
61
+ { id: "share", label: t("post.share") },
62
+ { id: "delete", label: t("post.delete"), tone: "danger" },
63
+ ]}
64
+ onAction={(id) => runPostAction(post, id)}
65
+ >
66
+ <Pressable accessibilityRole="button" accessibilityLabel={t("post.open")}>
67
+ <PostCardBody post={post} />
68
+ </Pressable>
69
+ </NativeContextMenu>
70
+ ```
71
+
72
+ ## 축과 기본값
73
+
74
+ | prop | 값 | 기본값 | 설명 |
75
+ | --- | --- | --- | --- |
76
+ | Web `items` | `{ id, label, textValue, shortcut?, disabled?, tone?: "default" \| "danger" }[]` | — (필수) | `MenuItemDescriptor`. `description`은 그리지 않는다 |
77
+ | Native `items` | `{ id: string, label: string, disabled?, tone?: "default" \| "danger" }[]` | — (필수) | 비거나 id 중복·빈 라벨이면 `TypeError` |
78
+ | `onAction` | Web `(id: Key) => void` · Native `(id: string) => void` | — (필수) | 고른 항목 id. 메뉴는 닫힌다 |
79
+ | Web `accessibilityLabel` | `string` | — (필수) | 메뉴 이름 |
80
+ | Native `onOpenChange` | `(open: boolean) => void` | — | OS 메뉴가 열리고 닫힐 때 |
81
+ | Web `layoutStyle` | 배치 전용 style | — | 영역 래퍼(흐름 안)에 붙는다. 떠 있는 메뉴는 배치하지 않는다 |
82
+ | Web `className` | `string` | — | 영역 래퍼 |
83
+
84
+ ## 배치
85
+
86
+ | 항목 | 값 | 근거 |
87
+ | --- | --- | --- |
88
+ | 크기 | 메뉴(Web): 최대 폭 `min(24rem, 90vw)`, 최대 높이 뷰포트 높이 − 2×`spacing.md`(16). 항목 최소 높이 `control.minTouchTarget` 44. 감싼 영역 래퍼(`.hjm-context-menu-host`)는 `min-inline-size: 0`만 가지며 크기·여백을 더하지 않는다 | `packages/react/src/styles.css` `.hjm-context-menu*` |
89
+ | 간격 | 메뉴 안쪽 여백 `spacing.xs` 8(`menuRecipe.surface.padding`, Menu와 같다. 2026-10-06까지 4, 1.12.1 이후 미게시), 항목 여백 `spacing.xs` 8 · `spacing.sm` 12, 라벨–단축키 간격 `spacing.sm` 12 | `packages/react/src/styles.css` `.hjm-context-menu__item` |
90
+ | 순서·정렬 | 항목은 `items` 배열 순서대로 위에서 아래로 그린다. 단축키는 끝에 붙고 줄바꿈하지 않는다. 우클릭·길게 누르기는 누른 좌표, Shift+F10·메뉴 키는 영역의 시작 쪽 아래 모서리에 메뉴를 연다 | `packages/react/src/context-menu.tsx`, `src/context-menu.ts` |
91
+ | 고정·스크롤 | `position: fixed`로 뜨고 뷰포트 밖으로 넘치면 안쪽으로 밀어 넣는다. 최대 높이를 넘으면 메뉴 안에서 스크롤한다 | `packages/react/src/context-menu.tsx` |
92
+ | 좁은 폭·큰 글자 | 좁은 폭에서는 최대 폭이 `90vw`로 줄고 항목 라벨은 줄바꿈한다. Native `NativeContextMenu`의 위치·크기·미리보기는 OS가 정한다 | `packages/react/src/styles.css` |
93
+
94
+ ```text
95
+ ┌ 카드(ContextMenu 영역) ─────────────────┐
96
+ │ ● 우클릭·길게 누른 지점 │
97
+ │ ┌──────────────────────┐ │
98
+ │ │ 공유 ⌘S │ │ 항목 최소 높이 44
99
+ │ │ 삭제(danger) │ │
100
+ │ └──────────────────────┘ │
101
+ └──────────────────────────────────────────┘
102
+ 키보드로 열면 메뉴 왼쪽 위가 영역의 왼쪽 아래 모서리에 붙는다(RTL은 시작 쪽).
103
+ ```
104
+
105
+ ## 꼭 지킬 것
106
+
107
+ - 메뉴 이름·항목 라벨은 i18n 키로 넣는다. 위험한 행동은 `tone: "danger"`로 표시하고 실행 전 확인은 제품이 한다.
108
+ - Web 항목은 `MenuItemDescriptor`(`id`·`label`·`textValue` 필수, `shortcut`·`disabled`·`tone` 선택)다.
109
+ `description`은 타입에 있지만 ContextMenu는 그리지 않는다.
110
+ - Web 영역은 `tabIndex=0` 래퍼가 되어 키보드로 열 수 있다. 같은 명령을 다른 보이는 경로에도 둔다.
111
+ - `NativeContextMenu`의 `children`은 접근 가능한 native 요소 하나다(`asChild`로 트리거가 된다).
112
+ 항목이 비거나, id가 중복되거나, id·라벨이 비면 실행 중 `TypeError`다.
113
+ - 메뉴 색·모양은 OS(Native)와 `menuRecipe`(Web)가 소유한다. 임의 색은 지원하지 않는다.
114
+
115
+ ### Native 선택 어댑터 설치 조건
116
+
117
+ - `zeego` 3.0.6이 optional peer다. zeego가 요구하는 `@react-native-menu/menu` 1.2.2,
118
+ `react-native-ios-context-menu` 3.2.1, `react-native-ios-utilities` 5.2.0도 package.json의 optional peer로
119
+ 고정돼 있다. 이 subpath를 쓰는 앱만 설치한다. 기본 entry는 이 peer 없이 동작한다.
120
+ - native module이라 개발 클라이언트를 다시 빌드해야 하고 Expo Go로는 확인할 수 없다.
121
+ - 배포된 HJM은 workspace patch를 적용해 주지 않는다. `@hjmds/react-native/docs/patches/`의 menu·
122
+ ios-context-menu·ios-utilities patch를 앱에 복사해 등록한 뒤 빌드한다([선택 어댑터](../../optional-adapters.md)).
123
+ - 2026-10에 optional native peer가 없는 앱에서 다른 선택 subpath(celebration·effect-surface·qr-code·
124
+ thinking-orb·toast-liquid)가 tsc·테스트는 통과하고 기기 Metro에서 크래시를 냈다. 이 subpath도
125
+ peer를 import하므로 기기에서 실행해 확인한다.
126
+
127
+ ## 플랫폼 차이
128
+
129
+ | 항목 | Web | Native |
130
+ | --- | --- | --- |
131
+ | 구현 | `ContextMenu` | `NativeContextMenu` |
132
+ | 여는 방법 | 우클릭, 터치 길게 누르기(500ms), Shift+F10·메뉴 키 | OS 길게 누르기 |
133
+ | 메뉴 접근성 이름 | `accessibilityLabel`(필수) | 없음(자식 요소가 이름을 가짐) |
134
+ | 항목 `textValue`·`shortcut` | 있음 | 없음 |
135
+ | 열림 알림 | 없음 | `onOpenChange(open)` |
136
+ | 성숙도 | stable | 실험적 어댑터(기기 증거 전까지 canonical unsupported) |
@@ -0,0 +1,107 @@
1
+ # CounterBadge
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: recipe `counterBadgeRecipe`(`src/counter-badge-recipe.ts`), 숫자 규칙 `formatCounterBadgeCount`(`src/counter-badge.ts`)
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/숫자 배지`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 읽지 않은 알림·메시지·장바구니 수처럼 **셀 수 있는 개수**를 아이콘·행 옆에 작게 보일 때 쓴다.
14
+ 0이면 아무것도 그리지 않고, `max`를 넘으면 `99+`처럼 줄인다. Web은 숫자 없이 "새것이 있음"만
15
+ 알리는 점(`dot`)도 그린다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 상태·분류 텍스트(신규, 완료, 오류) | [Badge](badge.md), [Tag](tag.md) |
22
+ | 하단 탭의 개수 | [BottomNavigation](bottom-navigation.md) 항목의 `badge` |
23
+ | 큰 숫자 지표 | [Statistic](statistic.md) |
24
+ | 진행률 | [Progress](progress.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `CounterBadge` | 기본 | `@hjmds/react`, `/display` | `@hjmds/react-native`, `/data-display` |
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { CounterBadge } from "@hjmds/react/display";
37
+
38
+ <CounterBadge
39
+ count={unread}
40
+ accessibilityLabel={t("inbox.unreadCount", { count: unread })}
41
+ />
42
+ ```
43
+
44
+ ```tsx
45
+ // Native
46
+ import { CounterBadge } from "@hjmds/react-native/data-display";
47
+
48
+ <CounterBadge
49
+ count={unread}
50
+ accessibilityLabel={t("inbox.unreadCount", { count: unread })}
51
+ />
52
+ ```
53
+
54
+ ## 축과 기본값
55
+
56
+ | prop | 값 | 기본값 | 설명 |
57
+ | --- | --- | --- | --- |
58
+ | `tone` | `danger` · `brand` · `neutral` | `danger` | — |
59
+ | `size` | `small` · `medium` | `medium` | — |
60
+ | `variant` | `inline` · `floating` | `inline` | `floating`은 아이콘 모서리에 겹칠 때 쓰는 테두리 있는 판 |
61
+ | `max` | 숫자 | `99` | 소수는 버리고 음수·`NaN`은 0으로 본다 |
62
+ | `dot` | `true` · `false` | `false` | Web만. 숫자 대신 8px 점을 그린다 |
63
+ | `count` | `number` | — (필수) | 0 이하면 아무것도 그리지 않는다 |
64
+ | `accessibilityLabel` | `string` | — | 없으면 보조기술에서 숨는다. 빈 문자열은 `TypeError` |
65
+ | `layoutStyle` | 배치 전용 style | — | 위치·여백만. Native `style`은 deprecated |
66
+
67
+ 콜백 prop은 없다.
68
+
69
+ ## 배치
70
+
71
+ | 항목 | 값 | 근거 |
72
+ | --- | --- | --- |
73
+ | 크기 | `medium` 높이·최소 폭 20, `small` 높이·최소 폭 16, `dot` 8×8, 모두 `radius.full`. 자릿수가 늘면 폭만 늘어난다(`99+`). 터치 대상이 아니므로 누르는 것은 부모다 | `src/counter-badge-recipe.ts`, `packages/react/src/styles.css` `.hjm-counter-badge` |
74
+ | 간격 | 좌우 여백 `medium` `spacing.xs` 8, `small` `spacing.xxs` 4. `floating` 테두리 `stroke.strong` 2가 아이콘과 배지를 떼어 보인다 | `src/counter-badge-recipe.ts` |
75
+ | 순서·정렬 | `inline`: 탭 라벨·목록 행의 끝 쪽에 텍스트와 나란히 둔다. `floating`: 컴포넌트가 스스로 위치를 잡지 않으므로 부모를 relative로 두고 배지를 위·끝 모서리(`top: 0`, 끝 0)에 absolute로 놓는다. 공개된 `NotificationBell`(`/notification-bell`)이 IconButton 위에 이 배치를 그대로 쓴다 | `packages/react/src/notification-bell.tsx`, `packages/react-native/src/notification-bell.tsx` |
76
+ | 고정·스크롤 | — | — |
77
+ | 좁은 폭·큰 글자 | Web은 `flex-shrink: 0`이라 좁아져도 줄지 않는다 | `packages/react/src/styles.css` `.hjm-counter-badge` |
78
+
79
+ ```text
80
+ inline (행 끝) floating (아이콘 모서리)
81
+ ┌─────────────────────────────┐ ┌──────┐(3) ← top 0, 끝 0
82
+ │ 받은 편지함 (12) │ │ 🔔 │
83
+ └─────────────────────────────┘ └──────┘ IconButton 44×44
84
+ ```
85
+
86
+ ## 꼭 지킬 것
87
+
88
+ - 이름을 붙이지 않으면 배지는 보조기술에서 숨겨진다. 부모(아이콘 버튼·행)의 접근성 이름이 개수를
89
+ 이미 말할 때만 `accessibilityLabel`을 생략한다. 빈 문자열을 넘기면 `TypeError`가 난다.
90
+ - `dot`에는 `accessibilityLabel`이 필수다(없으면 `TypeError`).
91
+ - 개수 표기·`+` 처리는 컴포넌트에 맡기고 `"99+"` 같은 문자열을 앱에서 만들지 않는다. 배지가 아닌
92
+ 자리에서 같은 표기가 필요하면 `formatCounterBadgeCount`(`@hjmds/design-contracts`, `/recipes`)를 쓴다.
93
+ - 색은 `tone`과 제품 테마 토큰으로 바꾼다. 배지 색을 직접 칠하지 않는다.
94
+ - 배치는 `layoutStyle`로 한다(Web·Native). Native `style`은 deprecated — 배치는 `layoutStyle`, 외형은 `tone`·`size`·`variant`로 옮긴다(개발 모드 1회 경고, 다음 major 제거).
95
+
96
+ ## 플랫폼 차이
97
+
98
+ | 항목 | Web | Native |
99
+ | --- | --- | --- |
100
+ | `dot` | 있음 | 없음 |
101
+ | 배치 | `layoutStyle`(그 밖에 `span` 속성 전달, `style`과 합친다) | `layoutStyle`(`style`은 deprecated) |
102
+ | ref | `HTMLSpanElement` | 없음 |
103
+
104
+ ## 함정
105
+
106
+ - `dot`이어도 `count`가 0이면 아무것도 그리지 않는다(숫자 계산이 먼저 `null`을 돌려준다).
107
+ 점을 보이려면 `count`를 1 이상으로 넘긴다.
@@ -0,0 +1,122 @@
1
+ # DataTable
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [DataTable](../../data-table.md), `src/data-table.ts`(`dataTableRecipe`, `getNextDataTableSortState`)
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/데이터 표`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 여러 행의 데이터를 열로 맞춰 훑고, 열 기준으로 정렬하거나 행을 골라 일괄 작업할 때 쓴다(Web).
14
+ 정렬·필터 실행, 페이지 나누기는 제품이 소유한다. DataTable은 다음 정렬 상태와 선택 상태만 판정한다.
15
+ 선택·정렬이 없는 단순 표는 companion `Table`을 쓴다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 한 대상의 속성(이름–값) 나열 | [DescriptionList](description-list.md) |
22
+ | 행 하나가 하나의 항목인 목록, 모바일 화면 | [List](list.md), [ListRow](list-row.md) |
23
+ | 아주 긴 목록의 가상 스크롤 | [VirtualList](virtual-list.md) |
24
+ | 행 확장(상세 펼침) | [Accordion](accordion.md)·[Collapsible](collapsible.md)을 행 안에 합성 |
25
+ | Native 화면 | Native renderer 없음 |
26
+
27
+ ## 공개 이름과 import
28
+
29
+ | 이름 | 역할 | Web | Native |
30
+ | --- | --- | --- | --- |
31
+ | `DataTable` | 기본 — 정렬·행 선택·비동기 상태를 갖는 표 | `@hjmds/react`, `/data-table` | 없음 |
32
+ | `Table` | 동반 — 행 객체와 `cell` 렌더러를 받는 단순 표 | `@hjmds/react`, `/display` | 없음 |
33
+
34
+ ## 최소 사용 예
35
+
36
+ ```tsx
37
+ // Web
38
+ import { DataTable } from "@hjmds/react/data-table";
39
+
40
+ <DataTable
41
+ columns={[
42
+ { id: "name", header: t("orders.col.name"), sortable: true },
43
+ { id: "total", header: t("orders.col.total"), align: "end" },
44
+ ]}
45
+ rows={orders.map((order) => ({ id: order.id }))}
46
+ renderCell={(rowId, columnId) => cellFor(rowId, columnId)}
47
+ labels={{
48
+ table: t("orders.table"),
49
+ selectAll: t("orders.selectAll"),
50
+ selectRow: (rowId) => t("orders.selectRow", { name: nameOf(rowId) }),
51
+ // 두 번째 인자는 이 열이 아니라 표 전체의 정렬 상태다(함정 참조).
52
+ sortColumn: (header) => t("orders.sortBy", { header }),
53
+ }}
54
+ sortState={sort}
55
+ onSortChange={setSort}
56
+ selection={{ mode: "multiple", selectedKeys: selected, onSelectionChange: setSelected }}
57
+ asyncState={loading ? { status: "loading", message: t("orders.loading") } : { status: "idle" }}
58
+ footer={pagination /* 제품이 합성한 Pagination 요소 */}
59
+ />
60
+ ```
61
+
62
+ Native: 없음. Native renderer가 없다.
63
+
64
+ ## 축과 기본값
65
+
66
+ | prop | 값 | 기본값 | 설명 |
67
+ | --- | --- | --- | --- |
68
+ | `columns` | `{ id, header: string, align?, sortable?, width? }[]` | — (필수, 하나 이상) | `id`는 비어 있지 않고 중복 없음 |
69
+ | `rows` | `{ id, disabled? }[]` | — (필수) | 셀 내용은 `renderCell`이 그린다 |
70
+ | `renderCell` | `(rowId: RowKey, columnId: ColumnKey) => ReactNode` | — (필수) | 행·열 id로 셀을 그린다 |
71
+ | `labels` | `{ table: string; selectAll: string; selectRow: (rowId: string) => string; sortColumn: (header: string, direction: DataTableSortState) => string }` | — (필수) | 모든 문구는 i18n 키 |
72
+ | `sortState` | `{ columnId, direction: "ascending" \| "descending" } \| null` | `null` | 제어 전용(내부 상태 없음) |
73
+ | `onSortChange` | `(next: DataTableSortState<ColumnKey>) => void` | — | 다음 정렬 상태(`null` = 정렬 없음)를 알린다. 재정렬은 제품이 한다 |
74
+ | `sortCycle` | `three-state` · `two-state` | `three-state` | `three-state`는 오름차순 → 내림차순 → 정렬 없음 |
75
+ | `selection` | `{ mode: "none" }` · `{ mode: "single", selectedKey: Key \| null, onSelectionChange: (key: Key \| null) => void, disallowEmptySelection? }` · `{ mode: "multiple", selectedKeys: ReadonlySet<Key>, onSelectionChange: (keys: ReadonlySet<Key>) => void }` | 없음(선택 열 없음) | 제어(`selectedKey(s)`) 대신 `defaultSelectedKey(s)`를 주면 비제어로 내부 상태가 유지된다 |
76
+ | `asyncState` | `{ status: "idle" }` · `{ status: "loading" \| "loadingMore" \| "empty" \| "error", message: string }` | `{ status: "idle" }` | `error`는 `role="alert"`, 나머지는 `role="status"`로 표 위에 나온다 |
77
+ | `density` | `regular` · `compact` | Provider 밀도 | 지정하지 않으면 Provider 밀도를 따른다(`comfortable` → `regular`) |
78
+ | 열 `align` | `start` · `center` · `end` | `start` | 머리 칸과 본문 칸 모두에 적용 |
79
+ | 열 `sortable` | `boolean` | `false` | — |
80
+ | 열 `width` | 양수 px | — | 힌트 |
81
+ | `footer` | `ReactNode` | — | 표 아래 합성 영역 |
82
+ | `layoutStyle` | 배치 전용 style(여백·폭·grid 위치) | — | 루트 wrapper에 붙는다. 시각 키는 받지 않는다 |
83
+
84
+ ## 배치
85
+
86
+ | 항목 | 값 | 근거 |
87
+ | --- | --- | --- |
88
+ | 크기 | 본문 폭을 채운다(표 `inline-size: 100%`). 선택 열 폭 `control.minTouchTarget` 44, 정렬 버튼·체크 상자 터치 영역 44 | `packages/react/src/styles.css` `.hjm-data-table*`, `src/data-table.ts` |
89
+ | 간격 | 루트 세로 간격 `spacing.sm` 12. 셀 여백(`dataTableRecipe.density`): `regular` 세로 `spacing.sm` 12 · 가로 `spacing.md` 16, `compact` 세로 `spacing.xs` 8 · 가로 `spacing.sm` 12. `footer` 간격 `spacing.sm` 12 | `packages/react/src/styles.css` |
90
+ | 순서·정렬 | 위에서 [상태 메시지] → 표 → [footer]. 선택 열은 맨 앞. 숫자·금액 열은 `align: "end"`로 끝 정렬하고 머리 행 정렬도 열 `align`을 따른다. 셀은 위 정렬. `footer`는 한 줄 flex(`space-between`, 넘치면 줄바꿈)이며 요약을 앞에, [Pagination](pagination.md)을 끝에 둔다 | `packages/react/src/data-table.tsx` |
91
+ | 고정·스크롤 | 가로 스크롤 컨테이너는 그리지 않는다. 긴 글자는 줄바꿈한다 | `packages/react/src/styles.css` |
92
+ | 좁은 폭·큰 글자 | 좁은 폭(모바일)에서 열이 많으면 표를 줄이지 말고 [List](list.md)로 바꾼다 | — |
93
+
94
+ ```text
95
+ ┌ DataTable ───────────────────────────────────────┐
96
+ │ 불러오는 중…(asyncState 메시지, status/alert) │
97
+ │ ☐ │ 이름 ▲ │ 상태 │ 합계 │ ← 머리 행
98
+ │ ☐ │ … │ … │ 12,000 │
99
+ ├────────────────────────────────────────────────────┤
100
+ │ 3개 선택됨 [‹ 1 2 3 ›] Pagination │ ← footer
101
+ └────────────────────────────────────────────────────┘
102
+ ```
103
+
104
+ ## 꼭 지킬 것
105
+
106
+ - 열 `id`·행 `id`는 비어 있지 않고 중복이 없어야 한다. 열은 하나 이상. 어기면 렌더 중 `TypeError`가 난다.
107
+ - `sortState`는 `sortable: true`인 열만 가리킨다. 아니면 `TypeError`.
108
+ - `labels`의 모든 문구는 i18n 키로 만든다. `selectRow`는 행 `id`를 받으므로 사람이 읽을 이름으로 바꿔 돌려준다.
109
+ - 받은 `onSortChange` 값으로 행을 재정렬하는 일은 제품이 한다(로컬 배열이든 서버 쿼리든).
110
+ - 페이지네이션·더 보기는 `footer`에 [Pagination](pagination.md)·[LoadMore](load-more.md)로 합성한다.
111
+ - 배치는 `layoutStyle`, 시각 override는 `className`으로만 한다. 행 hover·선택 색을 덮지 않는다.
112
+
113
+ ## 함정
114
+
115
+ - `sortState`는 제어 전용이다. 내부 상태가 없어 `sortState` 없이 `onSortChange`만 주면 머리 칸 화살표가 바뀌지 않는다.
116
+ 선택은 2026-10-06부터 `defaultSelectedKey(s)` 비제어도 내부 상태로 유지된다(이전에는 표시가 바뀌지 않았다).
117
+ - 현재 `labels.sortColumn`의 두 번째 인자는 그 열의 방향이 아니라 표 전체의 `sortState`다(정렬되지 않은 열도 다른 열의
118
+ 상태를 받는다). 열의 현재 방향을 이름에 넣으려면 제품이 `header`로 열을 찾아 `columnId`와 비교한다.
119
+ - `asyncState`의 `message`는 표 위에 `status`/`alert`로 나오지만 표는 그대로 그려진다. 빈 상태 화면이
120
+ 따로 필요하면 [EmptyState](empty-state.md)로 표를 대신 그린다.
121
+ - `Table`의 `onSortChange`는 `null`을 받지 못해 항상 two-state로 돈다. `emptyState`는 필수 prop이다.
122
+ - 미게시(1.12.1 이후) 변경: 1.12.1까지 Web `regular` 셀은 사방 `spacing.sm` 12였다. 열 폭을 그 값으로 맞춘 화면은 가로 16에서 다시 확인한다.
@@ -0,0 +1,142 @@
1
+ # DatePicker
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.12.1
7
+ - 검토일: 2026-10-06
8
+ - 근거: [DatePicker](../../date-picker.md), 격자는 [Calendar](../../calendar.md), recipe `datePickerRecipe`(`src/date-picker.ts`)
9
+ - 스토리북: `배포/컴포넌트/입력/날짜 선택`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 폼·필터 자리에서 날짜 **하나**를 고를 때 쓴다(생년월일, 방문일, 시작일 필터). 평소에는 필드 트리거만
14
+ 보이고, 누르면 Web은 필드에 붙은 팝오버, Native는 [Sheet](sheet.md) 안에 같은 달력 격자를 연다.
15
+ 날짜를 고르거나 지우면 닫히고 트리거로 포커스가 돌아간다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 시작~끝 기간 | [DateRangePicker](date-range-picker.md) |
22
+ | 달력을 화면에 늘 펼쳐 둠 | [Calendar](calendar.md) |
23
+ | 날짜를 키보드로 타이핑 | 지원하지 않는다(트리거로만 연다) |
24
+ | 날짜가 아닌 목록에서 하나 고름 | [Select](select.md) |
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | `DatePicker` | 기본 | `@hjmds/react`, `/date-picker`, `/forms` | `@hjmds/react-native`, `/date-picker`, `/inputs` |
31
+
32
+ ## 최소 사용 예
33
+
34
+ 격자 `cells`(7의 배수, 행 우선)·`weekdayLabels`·`todayDate`는 제품이 자기 시계와 로캘로 만든다.
35
+ DatePicker는 날짜를 포맷하지 않으므로 `displayValue`도 제품이 만든다.
36
+
37
+ ```tsx
38
+ // Web
39
+ import { DatePicker } from "@hjmds/react/date-picker";
40
+
41
+ <DatePicker
42
+ descriptor={{
43
+ grid: { cells, weekdayLabels, todayDate },
44
+ label: t("visit.date"),
45
+ placeholder: t("visit.datePlaceholder"),
46
+ displayValue: visitDate === null ? null : formatDate(visitDate),
47
+ selectedDate: visitDate,
48
+ onSelectionChange: (date) => setVisitDate(date),
49
+ focusedMonth: month,
50
+ onFocusedMonthChange: (next) => setMonth(next),
51
+ }}
52
+ monthLabel={formatMonth(month)}
53
+ previousMonth={{ month: prevMonthOf(month), label: t("calendar.previousMonth") }}
54
+ nextMonth={{ month: nextMonthOf(month), label: t("calendar.nextMonth") }}
55
+ composeAccessibleName={({ date, isToday, isSelected }) =>
56
+ t(isSelected ? "calendar.cell.selected" : isToday ? "calendar.cell.today" : "calendar.cell.default", { date: formatDate(date) })}
57
+ clearLabel={t("visit.clearDate")}
58
+ closeLabel={t("calendar.close")}
59
+ />
60
+ ```
61
+
62
+ ```tsx
63
+ // Native
64
+ import { DatePicker } from "@hjmds/react-native/date-picker";
65
+
66
+ <DatePicker
67
+ descriptor={/* Web과 같은 descriptor */ descriptor}
68
+ monthLabel={formatMonth(month)}
69
+ composeAccessibleName={composeCellName}
70
+ clearLabel={t("visit.clearDate")}
71
+ closeLabel={t("calendar.close")}
72
+ />
73
+ ```
74
+
75
+ ## 축과 기본값
76
+
77
+ | prop | 값 | 기본값 | 설명 |
78
+ | --- | --- | --- | --- |
79
+ | `size` | `medium` · `large` | `medium` | — |
80
+ | `descriptor.grid` | `{ cells: { date?, outsideFocusedMonth?, disabled?, content? }[], weekdayLabels: [7개 string], todayDate: string }` | — (필수) | `cells`는 7의 배수, 행 우선 |
81
+ | `descriptor.label` · `accessibilityLabel` | `string` | — | 둘 중 하나는 필수 |
82
+ | `descriptor.placeholder` · `displayValue` | `string` · `string \| null` | — (필수) | 트리거 문구. `displayValue`는 제품이 포맷한다 |
83
+ | `descriptor.open` / `defaultOpen` / `onOpenChange` | `boolean` · `(open: boolean, reason: "trigger" \| "keyboard" \| "selection" \| "clear" \| "escape" \| "outside" \| "blur" \| "programmatic") => void` | 비제어 닫힘 | 제어(`open`+`onOpenChange`) 또는 비제어 한 쌍 |
84
+ | `descriptor.selectedDate` / `defaultSelectedDate` / `onSelectionChange` | ISO `YYYY-MM-DD` \| `null` · `(date: string \| null, reason: "activate" \| "clear") => void` | — | 제어 또는 비제어 한 쌍만 쓴다. 지우기는 `null`, `"clear"` |
85
+ | `descriptor.focusedMonth` / `defaultFocusedMonth` / `onFocusedMonthChange` | `YYYY-MM` · `(month: string, reason: "previous" \| "next" \| "jump") => void` | — | 표시 달. 제어 또는 비제어 한 쌍만 쓴다 |
86
+ | `descriptor.disabled` · `readOnly` | `boolean` | `false` | 트리거를 열지 않고 모든 날짜 셀을 비활성으로 그린다. `disabled`는 라벨과 트리거 줄을 `fieldRecipe.disabledOpacity`(0.6)로 흐리고 도움말·오류는 그대로 둔다([Field](field.md)) |
87
+ | `descriptor.invalid` · `error` | `boolean` · 오류 문구(Web `ReactNode`, Native `string`) | — | 둘 중 하나가 있으면 오류 테두리 |
88
+ | `monthLabel` | `string` | — (필수) | 제품이 포맷한 달 제목 |
89
+ | `composeAccessibleName` | `(info: { date, isToday, isSelected, disabled, content? }) => string` | — (필수) | 셀 이름 |
90
+ | `previousMonth` · `nextMonth` | `{ month: string, label: string }` | — | 없으면 이동 버튼이 없다 |
91
+ | `renderCellContent` | `(cell: ResolvedCalendarDateCell) => ReactNode` | — | 날짜 아래 보조 표시 |
92
+ | Web `onNavigateBeyondGrid` | `(detail: { date, intent, overflow: "before" \| "after" }, focusDate: (date: string) => void) => void` | — | 키보드가 격자 밖으로 나갈 때 |
93
+ | Native `safeAreaInsets` | Sheet `safeAreaInsets` | Provider 값 | — |
94
+ | `layoutStyle` | 배치 전용 style | — | 루트 배치. Native `style`은 deprecated |
95
+
96
+ ## 배치
97
+
98
+ | 항목 | 값 | 근거 |
99
+ | --- | --- | --- |
100
+ | 크기 | 트리거 높이: `medium` 44 · `large` 52(`datePickerRecipe.sizes`, 두 플랫폼), 좌우 여백 16 · 20. 2026-10-06까지 렌더 값이 Web 44·56, Native 48·56이었다(1.12.1 이후 미게시). 지우기 버튼 44×44. Web 팝오버 폭 `min(22.5rem, 100vw − 2rem)`(최대 360). 날짜 셀 44, 7열 | `datePickerRecipe.sizes`, `packages/react/src/styles.css` `.hjm-date-picker*`, `packages/react-native/src/date-picker.tsx`, `src/calendar.ts` |
101
+ | 간격 | 라벨·트리거·설명·오류 사이 Web `spacing.xs` 8, Native 6(렌더러 고정값). 트리거 가로 여백 `medium` `spacing.md` 16, `large` `spacing.lg` 20. 팝오버는 트리거 아래 `spacing.xs` 8, 안쪽 여백 `spacing.sm` 12 | 같은 파일 |
102
+ | 순서·정렬 | 폼 안에서 다른 필드와 같은 폭으로 세로로 쌓는다. 위에서 라벨 → 트리거 → 설명 → 오류. 지우기 버튼(값이 있을 때)은 Web은 트리거 안 끝에 겹치고(트리거가 끝 여백 3rem을 비움), Native는 트리거 바깥 끝에 최소 터치 영역으로 붙는다. Web 팝오버는 시작 쪽 정렬 | `packages/react/src/styles.css`, `packages/react-native/src/date-picker.tsx` |
103
+ | 고정·스크롤 | Web 팝오버는 필드에 붙어 층 800에 뜬다. Native 달력은 화면 아래에서 올라오는 [Sheet](sheet.md)이며 하단 안전 영역은 Provider `safeAreaInsets`를 쓴다 | 같은 파일 |
104
+ | 좁은 폭·큰 글자 | Web 팝오버 폭은 뷰포트 − 2rem까지 줄어든다 | `packages/react/src/styles.css` `.hjm-date-picker__popover` |
105
+
106
+ ```text
107
+ Web Native
108
+ 라벨 라벨
109
+ ┌──────────────────────────[×]┐ ┌───────────────────────┐[×]
110
+ │ ▣ 2026-10-06 │ │ ▣ 2026-10-06 │
111
+ └─────────────────────────────┘ └───────────────────────┘
112
+ ↓ spacing.xs 8 ┌ Sheet ─────────────────┐
113
+ ┌ 팝오버 (최대 360) ───────────┐ │ 제목(라벨) [닫기]│
114
+ │ ‹ 2026년 10월 › │ │ ‹ 2026년 10월 › │
115
+ │ 일 월 화 수 목 금 토 │ │ 7×N 날짜 격자 │
116
+ │ 7×N 날짜 격자(셀 44) │ │ ─ 안전 영역 ─ │
117
+ └──────────────────────────────┘ └─────────────────────────┘
118
+ ```
119
+
120
+ ## 꼭 지킬 것
121
+
122
+ - `label` 또는 `accessibilityLabel` 중 하나, 비어 있지 않은 `placeholder`가 필수다. 어기면 `TypeError`.
123
+ - 모든 문구(라벨·placeholder·`clearLabel`·`closeLabel`·월 이동 라벨·셀 이름)는 i18n 키로 만든다.
124
+ 셀 이름(`composeAccessibleName`)의 어순·문법은 제품이 소유한다.
125
+ - 달을 넘기면 제품이 새 달의 `cells`와 `monthLabel`을 다시 만들어 넘긴다. 달을 바꿔도 선택값은 바뀌지 않는다.
126
+ - 배치는 `layoutStyle`로 한다(Web·Native). 트리거 색·높이를 덮지 않는다. Native `style`은 deprecated — `layoutStyle` 또는 `size`로 옮긴다.
127
+
128
+ ## 플랫폼 차이
129
+
130
+ | 항목 | Web | Native |
131
+ | --- | --- | --- |
132
+ | 달력이 뜨는 곳 | 필드에 붙은 팝오버(뷰포트 안으로 밀고 공간이 없으면 위로 뒤집음) | `Sheet` |
133
+ | `description`·`error` | `ReactNode` | `string` |
134
+ | 격자 밖 이동 알림 `onNavigateBeyondGrid` | 있음 | 없음 |
135
+ | 하단 안전 영역 `safeAreaInsets` | 없음 | 있음(기본은 Provider 값) |
136
+ | 배치 | `layoutStyle`(그 밖에 `className`) | `layoutStyle`(`style`은 deprecated) |
137
+
138
+ ## 함정
139
+
140
+ - `previousMonth`/`nextMonth`를 넘겨도 `descriptor.onFocusedMonthChange`가 없으면 두 renderer 모두 이동 버튼이 비활성이다.
141
+ - 예시의 Native 호출처럼 `previousMonth`/`nextMonth`를 빼면 이동 버튼 자체가 없다. 여러 달을 오가야 하면 넘긴다.
142
+ - 1.12.1 이하를 쓰는 화면은 트리거 높이가 recipe(`medium` 44 · `large` 52)와 달랐다(Web 44·56, Native 48·56). 다음 릴리스로 올리면 Native `medium`은 4, `large`는 두 플랫폼 모두 4 낮아지므로 그 높이로 맞춘 고정 높이 계산을 다시 본다.