@hjmds/design-contracts 1.1.1 → 1.3.4

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 (199) hide show
  1. package/dist/agreement.d.ts +183 -0
  2. package/dist/agreement.d.ts.map +1 -0
  3. package/dist/agreement.js +137 -0
  4. package/dist/agreement.js.map +1 -0
  5. package/dist/anchor.d.ts +62 -0
  6. package/dist/anchor.d.ts.map +1 -0
  7. package/dist/anchor.js +43 -0
  8. package/dist/anchor.js.map +1 -0
  9. package/dist/asset.d.ts +94 -0
  10. package/dist/asset.d.ts.map +1 -0
  11. package/dist/asset.js +60 -0
  12. package/dist/asset.js.map +1 -0
  13. package/dist/auth-screen.d.ts +119 -0
  14. package/dist/auth-screen.d.ts.map +1 -0
  15. package/dist/auth-screen.js +87 -0
  16. package/dist/auth-screen.js.map +1 -0
  17. package/dist/base-recipes.d.ts +19 -0
  18. package/dist/base-recipes.d.ts.map +1 -1
  19. package/dist/base-recipes.js +17 -0
  20. package/dist/base-recipes.js.map +1 -1
  21. package/dist/behaviors.d.ts +363 -3
  22. package/dist/behaviors.d.ts.map +1 -1
  23. package/dist/behaviors.js +37 -1
  24. package/dist/behaviors.js.map +1 -1
  25. package/dist/bottom-info.d.ts +58 -0
  26. package/dist/bottom-info.d.ts.map +1 -0
  27. package/dist/bottom-info.js +48 -0
  28. package/dist/bottom-info.js.map +1 -0
  29. package/dist/carousel.d.ts +3 -3
  30. package/dist/carousel.js +1 -1
  31. package/dist/carousel.js.map +1 -1
  32. package/dist/catalog.d.ts +1315 -245
  33. package/dist/catalog.d.ts.map +1 -1
  34. package/dist/catalog.js +67 -25
  35. package/dist/catalog.js.map +1 -1
  36. package/dist/collapsible.d.ts +56 -0
  37. package/dist/collapsible.d.ts.map +1 -0
  38. package/dist/collapsible.js +37 -0
  39. package/dist/collapsible.js.map +1 -0
  40. package/dist/component-definitions.d.ts +17 -0
  41. package/dist/component-definitions.d.ts.map +1 -1
  42. package/dist/component-definitions.js +17 -0
  43. package/dist/component-definitions.js.map +1 -1
  44. package/dist/component-recipes.d.ts +20 -3
  45. package/dist/component-recipes.d.ts.map +1 -1
  46. package/dist/component-recipes.js +24 -1
  47. package/dist/component-recipes.js.map +1 -1
  48. package/dist/context-menu.d.ts +53 -0
  49. package/dist/context-menu.d.ts.map +1 -0
  50. package/dist/context-menu.js +44 -0
  51. package/dist/context-menu.js.map +1 -0
  52. package/dist/counter-badge-recipe.d.ts +5 -0
  53. package/dist/counter-badge-recipe.d.ts.map +1 -1
  54. package/dist/counter-badge-recipe.js +5 -0
  55. package/dist/counter-badge-recipe.js.map +1 -1
  56. package/dist/dataviz.d.ts +76 -0
  57. package/dist/dataviz.d.ts.map +1 -0
  58. package/dist/dataviz.js +58 -0
  59. package/dist/dataviz.js.map +1 -0
  60. package/dist/date-range.d.ts +56 -0
  61. package/dist/date-range.d.ts.map +1 -0
  62. package/dist/date-range.js +79 -0
  63. package/dist/date-range.js.map +1 -0
  64. package/dist/design-system-provider.d.ts +24 -0
  65. package/dist/design-system-provider.d.ts.map +1 -1
  66. package/dist/design-system-provider.js +21 -0
  67. package/dist/design-system-provider.js.map +1 -1
  68. package/dist/floating-action-button.d.ts +1 -1
  69. package/dist/floating-action-button.d.ts.map +1 -1
  70. package/dist/floating-action-button.js +3 -1
  71. package/dist/floating-action-button.js.map +1 -1
  72. package/dist/formatters.d.ts +37 -0
  73. package/dist/formatters.d.ts.map +1 -0
  74. package/dist/formatters.js +69 -0
  75. package/dist/formatters.js.map +1 -0
  76. package/dist/heading.d.ts +82 -0
  77. package/dist/heading.d.ts.map +1 -0
  78. package/dist/heading.js +48 -0
  79. package/dist/heading.js.map +1 -0
  80. package/dist/index.d.ts +20 -0
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +20 -0
  83. package/dist/index.js.map +1 -1
  84. package/dist/menubar.d.ts +133 -0
  85. package/dist/menubar.d.ts.map +1 -0
  86. package/dist/menubar.js +80 -0
  87. package/dist/menubar.js.map +1 -0
  88. package/dist/native-platform.d.ts +88 -0
  89. package/dist/native-platform.d.ts.map +1 -0
  90. package/dist/native-platform.js +70 -0
  91. package/dist/native-platform.js.map +1 -0
  92. package/dist/popover.d.ts +19 -4
  93. package/dist/popover.d.ts.map +1 -1
  94. package/dist/popover.js +3 -0
  95. package/dist/popover.js.map +1 -1
  96. package/dist/progress-recipe.d.ts +14 -0
  97. package/dist/progress-recipe.d.ts.map +1 -1
  98. package/dist/progress-recipe.js +14 -1
  99. package/dist/progress-recipe.js.map +1 -1
  100. package/dist/provider-button.d.ts +140 -0
  101. package/dist/provider-button.d.ts.map +1 -0
  102. package/dist/provider-button.js +83 -0
  103. package/dist/provider-button.js.map +1 -0
  104. package/dist/recipes.d.ts +16 -1
  105. package/dist/recipes.d.ts.map +1 -1
  106. package/dist/recipes.js +15 -0
  107. package/dist/recipes.js.map +1 -1
  108. package/dist/sheet.d.ts +16 -0
  109. package/dist/sheet.d.ts.map +1 -1
  110. package/dist/sheet.js +33 -0
  111. package/dist/sheet.js.map +1 -1
  112. package/dist/sidebar.d.ts +168 -0
  113. package/dist/sidebar.d.ts.map +1 -0
  114. package/dist/sidebar.js +98 -0
  115. package/dist/sidebar.js.map +1 -0
  116. package/dist/skip-nav.d.ts +67 -0
  117. package/dist/skip-nav.d.ts.map +1 -0
  118. package/dist/skip-nav.js +47 -0
  119. package/dist/skip-nav.js.map +1 -0
  120. package/dist/tags-input.d.ts +157 -0
  121. package/dist/tags-input.d.ts.map +1 -0
  122. package/dist/tags-input.js +108 -0
  123. package/dist/tags-input.js.map +1 -0
  124. package/dist/text-formats.d.ts +85 -0
  125. package/dist/text-formats.d.ts.map +1 -0
  126. package/dist/text-formats.js +43 -0
  127. package/dist/text-formats.js.map +1 -0
  128. package/dist/toggle-group.d.ts +111 -0
  129. package/dist/toggle-group.d.ts.map +1 -0
  130. package/dist/toggle-group.js +78 -0
  131. package/dist/toggle-group.js.map +1 -0
  132. package/dist/top.d.ts +113 -0
  133. package/dist/top.d.ts.map +1 -0
  134. package/dist/top.js +72 -0
  135. package/dist/top.js.map +1 -0
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.js +1 -1
  138. package/dist/version.js.map +1 -1
  139. package/docs/agreement.md +33 -0
  140. package/docs/anchor.md +59 -59
  141. package/docs/ant-design-coverage.md +5 -3
  142. package/docs/asset.md +26 -0
  143. package/docs/auth-screen.md +65 -0
  144. package/docs/bottom-info.md +15 -0
  145. package/docs/breadcrumb.md +15 -15
  146. package/docs/button-label.md +20 -0
  147. package/docs/calendar.md +82 -154
  148. package/docs/carousel.md +27 -2
  149. package/docs/cascader.md +17 -0
  150. package/docs/chart.md +33 -0
  151. package/docs/clipboard.md +13 -0
  152. package/docs/collapsible.md +19 -0
  153. package/docs/command-palette.md +19 -0
  154. package/docs/confirm-popover.md +11 -1
  155. package/docs/context-menu.md +23 -0
  156. package/docs/data-table.md +22 -0
  157. package/docs/date-range.md +31 -0
  158. package/docs/density.md +52 -0
  159. package/docs/expansion-roadmap.md +16 -3
  160. package/docs/floating-action-button.md +35 -3
  161. package/docs/formatters.md +18 -0
  162. package/docs/generated/component-maturity.md +40 -23
  163. package/docs/generated/renderer-evidence.json +3029 -652
  164. package/docs/generated/renderer-evidence.md +55 -6
  165. package/docs/generated/showcase-manifest.json +1156 -81
  166. package/docs/heading.md +25 -0
  167. package/docs/identity.md +5 -0
  168. package/docs/layout.md +1 -1
  169. package/docs/library-gap-analysis.md +29 -9
  170. package/docs/list-row.md +14 -0
  171. package/docs/mentions.md +18 -0
  172. package/docs/menubar.md +22 -0
  173. package/docs/native-platform.md +36 -0
  174. package/docs/overlay-stack.md +24 -0
  175. package/docs/pagination.md +16 -8
  176. package/docs/popover.md +42 -13
  177. package/docs/product-audit-2026-09-15.md +88 -0
  178. package/docs/progress.md +16 -0
  179. package/docs/provider-button.md +33 -0
  180. package/docs/rating.md +19 -0
  181. package/docs/react-native-completion.md +347 -0
  182. package/docs/screen-chrome.md +52 -0
  183. package/docs/side-panel.md +23 -2
  184. package/docs/sidebar.md +30 -0
  185. package/docs/skip-nav.md +20 -0
  186. package/docs/splitter.md +22 -2
  187. package/docs/stable-promotion.md +51 -0
  188. package/docs/tags-input.md +25 -0
  189. package/docs/text-formats.md +15 -0
  190. package/docs/theming.md +84 -0
  191. package/docs/time-picker.md +22 -1
  192. package/docs/toast.md +18 -0
  193. package/docs/toggle-group.md +21 -0
  194. package/docs/top.md +32 -0
  195. package/docs/tour.md +23 -2
  196. package/docs/transfer-list.md +20 -0
  197. package/docs/tree-select.md +18 -0
  198. package/docs/tree.md +21 -0
  199. package/package.json +126 -6
@@ -0,0 +1,84 @@
1
+ # 테마 주입 — 내 브랜드색으로 시작하기
2
+
3
+ HJM은 `theme`(light/dark/system) 같은 **환경**과, 그 환경이 해석된 **값**을 분리해서
4
+ 받는다. 제품 브랜드색은 값 쪽에 넣는다. 이 문서는 새 제품이 처음 부딪히는 그 경로만
5
+ 설명한다. 팔레트를 어떻게 고를지는 [theme-palette.md](./theme-palette.md), 색의 의미
6
+ 구분은 [identity.md](./identity.md)에 있다.
7
+
8
+ ## 두 가지 사용 방식
9
+
10
+ ### 1. 기본 팔레트로 시작 (환경만 넘긴다)
11
+
12
+ ```tsx
13
+ import { HjmProvider } from "@hjmds/react/provider";
14
+
15
+ <HjmProvider theme="system" direction="ltr">
16
+ <App />
17
+ </HjmProvider>
18
+ ```
19
+
20
+ `theme="system"`이면 provider가 OS 설정을 읽고, `textScale`·`reducedMotion`도 같은
21
+ 방식으로 환경에서 해석한다. 이 경로는 HJM 기본 팔레트를 쓴다.
22
+
23
+ ### 2. 제품 팔레트 주입 (`brandPalette`)
24
+
25
+ 브랜드색은 별도 토큰 층을 만들지 않고 **HJM semantic key 위에 덮는다**. 넘긴 key만
26
+ 교체되고 나머지는 기본값을 유지하므로 recipe와 대비 규칙이 그대로 적용된다.
27
+
28
+ ```tsx
29
+ import { resolveDesignSystemProviderValue } from "@hjmds/design-contracts/components/design-system-provider";
30
+ import { HjmProvider } from "@hjmds/react/provider";
31
+ import { useMemo } from "react";
32
+
33
+ function ProductProvider({ preference, systemDark, children }) {
34
+ const value = useMemo(
35
+ () => resolveDesignSystemProviderValue(
36
+ { theme: preference },
37
+ {
38
+ systemTheme: systemDark ? "dark" : "light",
39
+ systemDirection: "ltr",
40
+ systemTextScale: 1,
41
+ systemReducedMotion: false,
42
+ // 브랜드가 소유하는 key만 덮는다. 중성색·상태색은 HJM 기본값을 쓴다.
43
+ brandPalette: {
44
+ light: { primary: "#0F6FFF", contentBrand: "#0B57C7", borderControl: "#C9D3E0" },
45
+ dark: { primary: "#5AA2FF", contentBrand: "#8CC0FF", borderControl: "#3A4757" },
46
+ },
47
+ },
48
+ ),
49
+ [preference, systemDark],
50
+ );
51
+ return <HjmProvider value={value}>{children}</HjmProvider>;
52
+ }
53
+ ```
54
+
55
+ React Native는 `HjmNativeProvider`가 같은 `value`를 받는다. 실제 사용 예는 BurnTok의
56
+ `apps/web/src/components/ThemeProvider.tsx`다 — 경계선 색 두 개만 주입하고 나머지는
57
+ 기본값을 쓴다.
58
+
59
+ ## 덮어도 되는 것과 아닌 것
60
+
61
+ | key | 덮기 | 이유 |
62
+ | --- | --- | --- |
63
+ | `primary` / `onPrimary` / `contentBrand` | 권장 | 브랜드의 자리다. 주 행동과 현재 위치를 이 색이 말한다 |
64
+ | `borderControl` / `focus` | 선택 | 브랜드 채도가 높으면 포커스 대비를 맞추기 위해 함께 조정한다 |
65
+ | `border` / `borderControl` | 선택 | 제품 경계선 밀도가 다를 때. BurnTok이 이 둘만 주입한다 |
66
+ | `bg` / `surface` / `text*` 중성 계열 | 신중히 | 대비 검증이 붙어 있다. 바꾸면 라이트·다크 양쪽에서 4.5:1을 다시 확인한다 |
67
+ | `danger` / `success` / `warning` / `info` | 비권장 | 상태색을 브랜드색으로 바꾸면 "오류"와 "브랜드"가 같은 색이 된다 |
68
+ | 컴포넌트별 색 | 불가 | recipe가 semantic key만 읽는다. 컴포넌트 하나만 다른 색이 되면 그것은 제품 예외지 테마가 아니다 |
69
+
70
+ ## 하지 말아야 할 세 가지
71
+
72
+ 1. **CSS로 `.hjm-*` 클래스를 덮거나 `--hjm-color-*`를 인라인 style로 재정의하지 않는다.** 그 순간 업그레이드마다 깨진다. 필요한 것이
73
+ semantic key로 표현되지 않으면 그것은 계약 공백이고, 우회가 아니라 이슈로 올린다.
74
+ 2. **recipe 값을 읽어 인라인 스타일로 다시 싣지 않는다.** 렌더러가 이미 그 값을
75
+ 칠한다. 제품이 다시 실으면 두 벌이 생기고 한쪽만 갱신된다.
76
+ 3. **제공자 브랜드색을 팔레트에 넣지 않는다.** 소셜 로그인 색은 테마가 아니라 남의
77
+ 자산이다 — [AuthProviderButton](./provider-button.md)이 그 자리를 갖는다.
78
+
79
+ ## 어댑터를 두는 이유
80
+
81
+ 제품 저장소의 `@<product>/design-system` 같은 얇은 층은 세 가지만 한다: (1) 위
82
+ `brandPalette` 주입, (2) 현지화된 필수 문구(`closeLabel`, `emptyMessage` 등)의 주입,
83
+ (3) 제품 고유 합성(`AppModal`처럼 닫힘 사유를 제품 어휘로 옮기는 것). 그 이상을 하고
84
+ 있다면 — 크기를 다시 계산하거나 색을 다시 칠하고 있다면 — 그것은 HJM의 공백이다.
@@ -19,7 +19,7 @@ Collection 기본 계약을 적용하지 않기로 했지만, 시·분은 처음
19
19
  - `none|single` 선택 모드, 정적 목록(비동기 상태 불필요)
20
20
 
21
21
  인 **정확히 `CollectionItemDescriptor` 하나**다. 그리고 "트리거 + 적응형 오버레이(Web
22
- popover / Native Sheet) + 단일 committed key + disabled 항목 skip 없는 예측 가능한
22
+ popover / Native Sheet) + 단일 committed key + disabled 항목을 건너뛰는 예측 가능한
23
23
  방향키 이동"은 이미 `Select`가 `beta`로 검증한 계약 그대로다. 즉:
24
24
 
25
25
  **시 Select 하나 + 분 Select 하나 = TimePicker.** 제품이 두 값을 `"HH:mm"` 문자열로
@@ -86,3 +86,24 @@ Yajalal 전체(`날짜/시간 선택`, `TimePicker`, 알림 설정 화면 `Notif
86
86
  catalog의 `{ name: "TimePicker", category: "input", platform: "adaptive", status:
87
87
  "planned" }`(`src/catalog.ts:69`) 행은 바꿀 것이 없다 — recipe/behavior가 원래 없었고,
88
88
  지금도 없다. `src/time-picker.ts`, `test/time-picker.test.ts`는 만들지 않았다.
89
+
90
+
91
+ ## 2026-09-16: React/RN 작동 조합
92
+
93
+ 명시 요청에 따라 위 합성 경로를 양쪽 Showcase의 `Patterns/Time selection`에 구현했다.
94
+ 시 00–23와 분 00–59의 독립 Select, 두 값이 있어야 가능한 확정, 초기화, 변경 시 확정 결과 해제를
95
+ 포함한다. 새 TimePicker renderer를 선언하지 않고 기존 공개 `Select`를 그대로 조합한다.
96
+
97
+ - Web API: `@hjmds/react/forms`의 `Select`, `@hjmds/react/actions`의 `Button`.
98
+ - Native API: `@hjmds/react-native/forms`의 `Select`, `@hjmds/react-native/actions`의 `Button`.
99
+ - [Web 예제](../../../showcase/web/src/patterns/TimeSelection.stories.tsx)와
100
+ [Native 예제](../../../showcase/native/src/TimeSelection.stories.tsx)는 제품이 가져갈 상태 연결을 보여준다.
101
+ - 두 값은 `selectedKey` / `onSelectionChange`로 통제하고 둘 다 non-null일 때 `HH:mm`으로 조합한다.
102
+ `00`도 유효한 선택이다. 시각을 실제 알림·서버 시각으로 바꾸는 작업은 제품 소유다.
103
+ - [Ant TimePicker](https://ant.design/components/time-picker/)의 명시 확정 흐름을 비교했다.
104
+ HJM 예제에서는 각 Select의 선택과 최종 시각 확정을 분리한다. 두 popup을 하나로 합치지 않는다.
105
+ - 320px 브라우저에서 시만 선택했을 때 확정 비활성, 23:59 확정, 초기화 후 비활성을 확인했다.
106
+ Native는 타입·Showcase 검사 범위이며 실제 기기 조작 확인은 별도다.
107
+
108
+ Catalog의 `planned + roadmap.composed`는 독립 renderer가 없다는 의미로 유지한다.
109
+ 합성 예제 완료를 새 renderer 수나 stable 승격으로 더하지 않는다.
package/docs/toast.md CHANGED
@@ -93,6 +93,24 @@ close, exit complete와 두 번째 provider dispose는 아무 상태도 바꾸
93
93
 
94
94
  ## Renderer acceptance
95
95
 
96
+ ### Compact Web layout — 2026-09-15
97
+
98
+ BurnTok `apps/web/src/app/globals.css:301` records a product workaround for a
99
+ close-only second row. The first-party browser regression reproduced it at
100
+ 320, 390, and 480px: the old fixed subtraction in `flex-basis` did not account
101
+ for the actual control and gap layout. The Web renderer now gives icon, copy,
102
+ and close explicit columns and places an optional action below the copy.
103
+ This keeps a 44px close target and allows long text to wrap without a guessed
104
+ remaining width. Native layout and the shared Toast lifecycle are unchanged.
105
+ The same card grid applies on desktop: ToastProvider caps cards at 420px,
106
+ so a window-width breakpoint left doubled text and action labels compressed
107
+ even in a wide window. A 1280px window/420px card regression covers that case.
108
+
109
+ Proof: `packages/react/test/toast-layout.browser.test.tsx`; review fixture:
110
+ Storybook `Patterns/Toast layout`. The fixture covers long Korean/English copy,
111
+ doubled text, dark theme and RTL. Automated browser checks are separate from
112
+ real assistive-technology and consumer-release evidence; Toast remains beta.
113
+
96
114
  Web renderer:
97
115
 
98
116
  - viewport를 문서 root 근처에 하나만 두고 recipe placement와 logical start/end를 사용합니다.
@@ -0,0 +1,21 @@
1
+ # ToggleGroup contract
2
+
3
+ **문제.** 여러 개를 동시에 켜고 끄는 버튼 묶음 — 굵게/기울임/밑줄, 여러 개를 함께 거는
4
+ 필터 칩.
5
+
6
+ **SegmentedControl과의 경계.** Segmented는 **하나를 고른다**(어떤 화면을 볼지),
7
+ ToggleGroup은 **여러 개를 켠다**(무엇을 적용할지). 그래서 Segmented에는 "선택 없음"이
8
+ 없고 ToggleGroup에는 있다. 한 컴포넌트의 `mode` 축으로 합치면 그 차이가 사라지고
9
+ "아무것도 선택되지 않은 Segmented"라는 표현 불가능한 상태가 타입에 생긴다. 이 계약에
10
+ single 모드를 넣지 않는 것이 규칙이다.
11
+
12
+ **CheckboxGroup과의 경계.** CheckboxGroup은 목록 안의 선택이라 각 항목이 자기 줄과
13
+ 설명을 갖는다. ToggleGroup은 도구 모음이라 한 줄에 붙고 라벨이 짧다. 판정(무엇을 켤 수
14
+ 있는가, 비활성은 어떻게 되는가)은 같은 `selection-helpers`를 공유한다 — 두 번째 선택
15
+ 모델을 만들지 않는다.
16
+
17
+ **상태는 색이 아니라 `aria-pressed`(Native `selected`)가 말한다.** 색은 보강일 뿐이다.
18
+
19
+ **tab stop.** 각 토글이 자기 tab stop이다. 도구 모음식 roving focus를 쓰지 않는 이유는
20
+ 묶음이 대개 2~4개로 짧고, roving은 "그룹 안에서 화살표로 이동"이라는 추가 학습을
21
+ 요구하기 때문이다. 항목이 많아지는 실제 화면이 나오면 그때 축을 연다.
package/docs/top.md ADDED
@@ -0,0 +1,32 @@
1
+ # Top contract
2
+
3
+ **문제.** 화면 본문의 첫 블록 — 사용자가 "이 화면이 무엇을 묻는지" 읽는 제목과 보조
4
+ 문장. 토스 TDS가 "Top과 ListRow로 화면 대부분을 만든다"고 말하는 그 Top이다.
5
+
6
+ **TopBar와 다른 자리.** 이름이 비슷해서 합치고 싶어지지만 다른 문제다.
7
+
8
+ | | TopBar | Top |
9
+ | --- | --- | --- |
10
+ | 정체 | 화면에 고정된 크롬 | 스크롤과 함께 움직이는 본문 |
11
+ | 내용 | 뒤로가기·화면 이름·액션 | 제목·보조 문장·보조 행동 |
12
+ | 관심사 | safe area, 고정, 축약 | 읽기, 줄바꿈, heading 구조 |
13
+ | 접근성 | 내비게이션 landmark | 실제 `h1`/`h2` |
14
+
15
+ **Section과도 다르다.** Section은 본문 *중간*의 묶음 제목이라 heading level이 주변
16
+ 구조에 종속된다. Top은 화면당 하나뿐인 첫 제목이라 기본이 `1`이다. 합치면 "이 Section이
17
+ 이 화면의 h1인가"를 매번 물어야 하고, 그 질문은 컴포넌트가 아니라 화면이 답해야 한다.
18
+
19
+ **크기는 둘뿐이다.** `large`(화면의 첫 제목)와 `medium`(시트·모달 안의 첫 제목).
20
+ 세 번째가 필요해지는 자리는 이미 `Section`이다 — 크기를 늘리는 대신 다른 컴포넌트를
21
+ 쓰라는 신호로 둔다. 두 크기 모두 기존 타입 스케일(`heading.level2`, `typography.titleLarge`)을
22
+ 그대로 쓰고 새 숫자를 만들지 않는다.
23
+
24
+ **잘리지 않는다.** `description`은 줄바꿈하며 말줄임하지 않는다. 큰 글자 설정에서 화면이
25
+ 묻는 내용이 사라지면 안 되기 때문이다. `trailing` 보조 행동은 제목 줄을 공유하다가
26
+ 공간이 부족하면 아래로 내려간다.
27
+
28
+ **eyebrow 규칙.** 제목이 어떤 형태로든 담고 있지 않은 정보를 eyebrow에 넣지 않는다.
29
+ 카테고리·단계 표시처럼 제목을 한정하는 짧은 말만 쓴다.
30
+
31
+ **상태가 없다.** controlled 축이 없고 키보드도 dismiss도 없다 — 화면이 제목을 바꾸면
32
+ 다른 문자열을 넘기면 된다.
package/docs/tour.md CHANGED
@@ -130,7 +130,28 @@ Popover처럼 순수 판정 함수(`resolveTourAdvance`, `validateTourOpenState`
130
130
  | "다시 보지 않기" 영속화 | **배제** — 제품이 `TourCloseReason`을 받아 직접 저장할 몫 |
131
131
  | 비동기 busy/error 상태 | **배제** — Tour에는 되돌릴 수 없는 side effect가 없다. AlertDialog의 세션 패턴을 가져올 이유가 없다 |
132
132
 
133
+ ## Web renderer (2026-09-18)
134
+
135
+ `@hjmds/react/tour`의 `Tour`가 이 계약을 실행한다. catalog는 Web `beta`,
136
+ Native `unsupported`다. 제품 vertical slice와 보조기기 실측은 아직 없다.
137
+
138
+ - **anchor는 끝까지 제품 소유다.** props는 노드가 아니라
139
+ `resolveAnchor(anchorId)`를 받는다. renderer는 단계가 바뀔 때마다, 그리고 스크롤·
140
+ 리사이즈마다 다시 측정한다 — rect를 보관하면 제품이 페이지를 다시 그리는 순간 어긋난다.
141
+ - **초점은 카드로 간다.** 카드 자체(`tabIndex=-1`)가 초기 초점이며 단계마다 다시 간다.
142
+ 카드의 첫 버튼이 아니다 — 카드가 단계 안내 문구를 들고 있고, 그 문구가 시각적 포인터를
143
+ 대신하는 전부이기 때문이다. anchor는 열려 있는 동안 inert 배경이라 초점 대상이 아니다.
144
+ - **화면에 보이는 제목·본문은 보조기기에서 숨긴다.** 합성된 안내 문구가 이미 위치·제목·
145
+ 설명을 담고 있어 그대로 두면 두 번 읽힌다.
146
+ - **veil에는 dismiss handler가 없다.** 계약에 `outside` 사유 자체가 없으므로 실수로 찍은
147
+ 포인터가 둘러보기를 끝내지 못한다. Escape와 건너뛰기는 어느 단계에서나 나간다.
148
+ - **unmount는 `interrupted`로 한 번만 정산한다.** StrictMode의 probe cleanup과 실제
149
+ unmount를 epoch로 구분한다(Sheet와 같은 방식).
150
+ - 로컬 검증: `test/tour.browser.test.tsx` 6개(이름·안내·단계마다 초점 이동, 배경 inert와
151
+ 바깥 pointer 무시, 첫 단계 이전은 no-op·마지막 다음은 complete, Escape·건너뛰기 탈출,
152
+ unmount의 1회 interrupted, controlled owner 종료와 320px 배치)와 `Patterns/Tour`.
153
+
133
154
  ## 검증 화면
134
155
 
135
- 아직 없음. `planned → beta` 승격은 실제 제품 vertical slice 이후 리드가
136
- 진행한다. 유력 후보: Yajalal 홈 화면 첫 진입 안내(검색 → 즐겨찾기 → 알림).
156
+ 제품 vertical slice는 아직 없다. 유력 후보는 Yajalal 홈 화면 첫 진입
157
+ 안내(검색 → 즐겨찾기 → 알림)이며, 채택 전까지 `beta`는 renderer 수준 증거만 뜻한다.
@@ -92,3 +92,23 @@ Native의 "터치 전용"에도 같게 적용되므로, 버튼이 항상 스와
92
92
  ## 검증 화면
93
93
 
94
94
  아직 없음. `planned → beta` 승격은 실제 제품 vertical slice 이후 리드가 진행한다.
95
+
96
+ ## Web renderer (2026-09-18)
97
+
98
+ `@hjmds/react/transfer-list`의 `TransferList`가 이 계약을 실행한다. catalog는 Web `beta`,
99
+ Native `planned`다 — Native renderer는 아직 없다.
100
+
101
+ - **이동은 키보드만으로 끝난다.** Space로 고르고 이동 버튼을 누르거나, 초점이 있는 행에서
102
+ Enter로 그 행 하나만 바로 옮긴다. 후자는 계약의 "단일 이동에 다중 선택을 먼저 만들게
103
+ 하지 않는다"는 항목이다.
104
+ - **이동 후 초점은 계약이 정한다.** `resolveTransferListFocusAfterMove`가 미끄러져 들어온
105
+ 행을 돌려주고, 패널이 비면 빈 상태 문구(`tabIndex=-1`)로 보낸다. 문서 body로 초점이
106
+ 풀리는 경우가 없다.
107
+ - **옮긴 항목은 도착 패널에서 선택되지 않는다.** 이동은 값을 확정하는 것이지 사용자가 하지
108
+ 않은 새 선택을 만드는 것이 아니다.
109
+ - **모두 선택은 disabled를 분모에서 뺀다.** 분자·분모 모두에서 빠지므로 잠긴 행이 있어도
110
+ "모두 선택"이 mixed로 굳지 않는다.
111
+ - **문장은 제품이 만든다.** `onMove`는 옮긴 id를 원래 패널 순서로만 넘긴다.
112
+ - 로컬 검증: `test/transfer-list.browser.test.tsx` 5개(키보드 다중 이동과 id 보고, 단일
113
+ 행 즉시 이동, 이동 후 초점과 빈 상태 초점, disabled 제외와 이동 금지, 되돌리기와 패널당
114
+ tab stop 하나)와 `Patterns/TransferList`.
@@ -99,3 +99,21 @@ antd `TreeSelect`의 `checkStrictly`(부모/자식을 독립적으로 체크할
99
99
  ## 검증 화면
100
100
 
101
101
  아직 없음. `planned → beta` 승격은 실제 제품 vertical slice 이후 리드가 진행한다.
102
+
103
+ ## 조합으로 제공한다 (2026-09-18)
104
+
105
+ 이 모듈의 판정대로 TreeSelect는 새 primitive가 아니다. Tree renderer가 들어오면서
106
+ 필요한 세 조각이 모두 존재하므로 작동 예제를 Showcase에 두고 catalog는 `composed`로 적는다.
107
+
108
+ - 표면: `@hjmds/react/popover`의 `Popover`(트리거 + 팝업 + 확정 버튼)
109
+ - collection: `@hjmds/react/tree`의 `Tree`(깊이 발표·화살표·확장)
110
+ - 집계: 이 모듈의 `resolveTreeCheckedStates`/`toggleTreeCheckedSelection`
111
+ - 예제: Showcase `Patterns/Tree`의 TreeSelect 화면
112
+
113
+ **Cascader도 같은 자리에서 끝난다.** catalog는 Cascader를 "TreeSelect에 path value와
114
+ 중간 단계 commit 축이 필요하다"는 prerequisite로 묶어 뒀는데, Tree renderer의 resolve
115
+ 결과가 이미 `parentId` 사슬을 들고 있어 경로는 파생이고, 중간 단계 확정은 그 노드를 그대로
116
+ 고르는 것이다. 새 축을 계약에 추가할 이유가 없어 prerequisite를 풀고 `composed`로 옮겼다
117
+ (`Patterns/Tree`의 Cascader 화면). antd의 열(column) 방식 화면은 베끼지 않는다 — HJM의
118
+ 계층 탐색 어휘를 그대로 쓰는 쪽이 같은 문제를 이미 푼다.
119
+
package/docs/tree.md CHANGED
@@ -121,3 +121,24 @@ depth/sibling 발화, 화살표 판정, `expandedKeys` 재조정을 그대로
121
121
  popup/sheet 표면에 얹으면 된다. 새 recipe나 새 상태 축이 필요해 보이지 않으므로, 측정된
122
122
  제품 요구가 나오기 전까지는 `src/tree-select.ts`를 만들지 않는다(`docs/dropdown.md`·
123
123
  `docs/notification.md`와 같은 판단).
124
+
125
+ ## Web renderer (2026-09-18)
126
+
127
+ `@hjmds/react/tree`의 `Tree`가 이 계약을 실행한다. catalog는 Web `beta`,
128
+ Native `unsupported`다. 제품 채택·보조기기 실측은 아직 없다.
129
+
130
+ - **판정은 전부 계약이 한다.** 화살표(`getTreeArrowResult`), 상하·Home/End
131
+ (`getVisibleTreeNavigationTarget`), 타이핑 검색(`getVisibleTreeTypeaheadMatch`),
132
+ 확장 재조정(`reconcileTreeExpansion`)을 그대로 호출한다. renderer에는 트리 산술이 없다.
133
+ - **중첩 group 대신 aria-level을 쓴 평평한 목록이다.** roving tab stop과 보이는 노드
134
+ 탐색이 모두 resolve 결과 배열 하나 위에서 돌고, WAI-ARIA는 두 형태를 모두 허용한다.
135
+ 들여쓰기는 장식이고 깊이는 항상 발표된다.
136
+ - **tab stop은 트리 전체에 하나다.** 펼침 glyph는 장식이라 행 안에 중첩 컨트롤이 없다.
137
+ 접기로 초점 노드가 사라지면 roving tab stop이 보이는 첫 노드로 되돌아간다.
138
+ - **tri-state 체크는 노드 자체에 실린다.** `checkedStates`(=`resolveTreeCheckedStates`
139
+ 결과)를 주면 행이 `aria-checked`로 true/false/mixed를 말한다. 체크박스를 행 안에 넣으면
140
+ tab stop 규칙이 깨지므로 넣지 않았다.
141
+ - 로컬 검증: `test/tree.browser.test.tsx` 6개(깊이·형제 위치 발표, 단일 tab stop과 roving,
142
+ 펼침·접힘과 접힌 subtree 건너뛰기, RTL 화살표 반전, disabled의 선택만 차단·타이핑 검색,
143
+ tri-state 파생과 enabled leaf만의 cascade)와 `Patterns/Tree`.
144
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hjmds/design-contracts",
3
- "version": "1.1.1",
3
+ "version": "1.3.4",
4
4
  "description": "Renderer-neutral design contracts, tokens, recipes, and behaviors shared by HJM products.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,12 +52,24 @@
52
52
  "import": "./dist/responsive.js",
53
53
  "default": "./dist/responsive.js"
54
54
  },
55
+ "./formatters": {
56
+ "types": "./dist/formatters.d.ts",
57
+ "react-native": "./dist/formatters.js",
58
+ "import": "./dist/formatters.js",
59
+ "default": "./dist/formatters.js"
60
+ },
55
61
  "./grid": {
56
62
  "types": "./dist/grid.d.ts",
57
63
  "react-native": "./dist/grid.js",
58
64
  "import": "./dist/grid.js",
59
65
  "default": "./dist/grid.js"
60
66
  },
67
+ "./dataviz": {
68
+ "types": "./dist/dataviz.d.ts",
69
+ "react-native": "./dist/dataviz.js",
70
+ "import": "./dist/dataviz.js",
71
+ "default": "./dist/dataviz.js"
72
+ },
61
73
  "./recipes": {
62
74
  "types": "./dist/component-recipes.d.ts",
63
75
  "react-native": "./dist/component-recipes.js",
@@ -115,18 +127,48 @@
115
127
  "./consumer-policy.md": {
116
128
  "default": "./docs/consumer-policy.md"
117
129
  },
130
+ "./components/agreement": {
131
+ "types": "./dist/agreement.d.ts",
132
+ "react-native": "./dist/agreement.js",
133
+ "import": "./dist/agreement.js",
134
+ "default": "./dist/agreement.js"
135
+ },
118
136
  "./components/alert-dialog": {
119
137
  "types": "./dist/alert-dialog.d.ts",
120
138
  "react-native": "./dist/alert-dialog.js",
121
139
  "import": "./dist/alert-dialog.js",
122
140
  "default": "./dist/alert-dialog.js"
123
141
  },
142
+ "./components/anchor": {
143
+ "types": "./dist/anchor.d.ts",
144
+ "react-native": "./dist/anchor.js",
145
+ "import": "./dist/anchor.js",
146
+ "default": "./dist/anchor.js"
147
+ },
124
148
  "./components/aspect-ratio": {
125
149
  "types": "./dist/aspect-ratio.d.ts",
126
150
  "react-native": "./dist/aspect-ratio.js",
127
151
  "import": "./dist/aspect-ratio.js",
128
152
  "default": "./dist/aspect-ratio.js"
129
153
  },
154
+ "./components/asset": {
155
+ "types": "./dist/asset.d.ts",
156
+ "react-native": "./dist/asset.js",
157
+ "import": "./dist/asset.js",
158
+ "default": "./dist/asset.js"
159
+ },
160
+ "./components/auth-screen": {
161
+ "types": "./dist/auth-screen.d.ts",
162
+ "react-native": "./dist/auth-screen.js",
163
+ "import": "./dist/auth-screen.js",
164
+ "default": "./dist/auth-screen.js"
165
+ },
166
+ "./components/bottom-info": {
167
+ "types": "./dist/bottom-info.d.ts",
168
+ "react-native": "./dist/bottom-info.js",
169
+ "import": "./dist/bottom-info.js",
170
+ "default": "./dist/bottom-info.js"
171
+ },
130
172
  "./components/bottom-navigation": {
131
173
  "types": "./dist/bottom-navigation.d.ts",
132
174
  "react-native": "./dist/bottom-navigation.js",
@@ -157,6 +199,12 @@
157
199
  "import": "./dist/carousel.js",
158
200
  "default": "./dist/carousel.js"
159
201
  },
202
+ "./components/collapsible": {
203
+ "types": "./dist/collapsible.d.ts",
204
+ "react-native": "./dist/collapsible.js",
205
+ "import": "./dist/collapsible.js",
206
+ "default": "./dist/collapsible.js"
207
+ },
160
208
  "./components/collection": {
161
209
  "types": "./dist/collection.d.ts",
162
210
  "react-native": "./dist/collection.js",
@@ -169,17 +217,23 @@
169
217
  "import": "./dist/command-palette.js",
170
218
  "default": "./dist/command-palette.js"
171
219
  },
220
+ "./components/container": {
221
+ "types": "./dist/container.d.ts",
222
+ "react-native": "./dist/container.js",
223
+ "import": "./dist/container.js",
224
+ "default": "./dist/container.js"
225
+ },
172
226
  "./components/content-state": {
173
227
  "types": "./dist/content-state.d.ts",
174
228
  "react-native": "./dist/content-state.js",
175
229
  "import": "./dist/content-state.js",
176
230
  "default": "./dist/content-state.js"
177
231
  },
178
- "./components/container": {
179
- "types": "./dist/container.d.ts",
180
- "react-native": "./dist/container.js",
181
- "import": "./dist/container.js",
182
- "default": "./dist/container.js"
232
+ "./components/context-menu": {
233
+ "types": "./dist/context-menu.d.ts",
234
+ "react-native": "./dist/context-menu.js",
235
+ "import": "./dist/context-menu.js",
236
+ "default": "./dist/context-menu.js"
183
237
  },
184
238
  "./components/data-table": {
185
239
  "types": "./dist/data-table.d.ts",
@@ -193,6 +247,12 @@
193
247
  "import": "./dist/date-picker.js",
194
248
  "default": "./dist/date-picker.js"
195
249
  },
250
+ "./components/date-range": {
251
+ "types": "./dist/date-range.d.ts",
252
+ "react-native": "./dist/date-range.js",
253
+ "import": "./dist/date-range.js",
254
+ "default": "./dist/date-range.js"
255
+ },
196
256
  "./components/description-list": {
197
257
  "types": "./dist/description-list.d.ts",
198
258
  "react-native": "./dist/description-list.js",
@@ -223,6 +283,12 @@
223
283
  "import": "./dist/form.js",
224
284
  "default": "./dist/form.js"
225
285
  },
286
+ "./components/heading": {
287
+ "types": "./dist/heading.d.ts",
288
+ "react-native": "./dist/heading.js",
289
+ "import": "./dist/heading.js",
290
+ "default": "./dist/heading.js"
291
+ },
226
292
  "./components/icon": {
227
293
  "types": "./dist/icon.d.ts",
228
294
  "react-native": "./dist/icon.js",
@@ -259,6 +325,18 @@
259
325
  "import": "./dist/mentions.js",
260
326
  "default": "./dist/mentions.js"
261
327
  },
328
+ "./components/menubar": {
329
+ "types": "./dist/menubar.d.ts",
330
+ "react-native": "./dist/menubar.js",
331
+ "import": "./dist/menubar.js",
332
+ "default": "./dist/menubar.js"
333
+ },
334
+ "./components/native-platform": {
335
+ "types": "./dist/native-platform.d.ts",
336
+ "react-native": "./dist/native-platform.js",
337
+ "import": "./dist/native-platform.js",
338
+ "default": "./dist/native-platform.js"
339
+ },
262
340
  "./components/number-field": {
263
341
  "types": "./dist/number-field.d.ts",
264
342
  "react-native": "./dist/number-field.js",
@@ -289,6 +367,12 @@
289
367
  "import": "./dist/popover.js",
290
368
  "default": "./dist/popover.js"
291
369
  },
370
+ "./components/provider-button": {
371
+ "types": "./dist/provider-button.d.ts",
372
+ "react-native": "./dist/provider-button.js",
373
+ "import": "./dist/provider-button.js",
374
+ "default": "./dist/provider-button.js"
375
+ },
292
376
  "./components/result": {
293
377
  "types": "./dist/result.d.ts",
294
378
  "react-native": "./dist/result.js",
@@ -307,6 +391,18 @@
307
391
  "import": "./dist/side-panel.js",
308
392
  "default": "./dist/side-panel.js"
309
393
  },
394
+ "./components/sidebar": {
395
+ "types": "./dist/sidebar.d.ts",
396
+ "react-native": "./dist/sidebar.js",
397
+ "import": "./dist/sidebar.js",
398
+ "default": "./dist/sidebar.js"
399
+ },
400
+ "./components/skip-nav": {
401
+ "types": "./dist/skip-nav.d.ts",
402
+ "react-native": "./dist/skip-nav.js",
403
+ "import": "./dist/skip-nav.js",
404
+ "default": "./dist/skip-nav.js"
405
+ },
310
406
  "./components/slider": {
311
407
  "types": "./dist/slider.d.ts",
312
408
  "react-native": "./dist/slider.js",
@@ -337,6 +433,18 @@
337
433
  "import": "./dist/tag.js",
338
434
  "default": "./dist/tag.js"
339
435
  },
436
+ "./components/tags-input": {
437
+ "types": "./dist/tags-input.d.ts",
438
+ "react-native": "./dist/tags-input.js",
439
+ "import": "./dist/tags-input.js",
440
+ "default": "./dist/tags-input.js"
441
+ },
442
+ "./components/text-formats": {
443
+ "types": "./dist/text-formats.d.ts",
444
+ "react-native": "./dist/text-formats.js",
445
+ "import": "./dist/text-formats.js",
446
+ "default": "./dist/text-formats.js"
447
+ },
340
448
  "./components/timeline": {
341
449
  "types": "./dist/timeline.d.ts",
342
450
  "react-native": "./dist/timeline.js",
@@ -349,12 +457,24 @@
349
457
  "import": "./dist/toast.js",
350
458
  "default": "./dist/toast.js"
351
459
  },
460
+ "./components/toggle-group": {
461
+ "types": "./dist/toggle-group.d.ts",
462
+ "react-native": "./dist/toggle-group.js",
463
+ "import": "./dist/toggle-group.js",
464
+ "default": "./dist/toggle-group.js"
465
+ },
352
466
  "./components/tooltip": {
353
467
  "types": "./dist/tooltip.d.ts",
354
468
  "react-native": "./dist/tooltip.js",
355
469
  "import": "./dist/tooltip.js",
356
470
  "default": "./dist/tooltip.js"
357
471
  },
472
+ "./components/top": {
473
+ "types": "./dist/top.d.ts",
474
+ "react-native": "./dist/top.js",
475
+ "import": "./dist/top.js",
476
+ "default": "./dist/top.js"
477
+ },
358
478
  "./components/tour": {
359
479
  "types": "./dist/tour.d.ts",
360
480
  "react-native": "./dist/tour.js",