@hjmds/design-contracts 0.8.2

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 (353) hide show
  1. package/README.md +208 -0
  2. package/dist/alert-dialog.d.ts +102 -0
  3. package/dist/alert-dialog.d.ts.map +1 -0
  4. package/dist/alert-dialog.js +136 -0
  5. package/dist/alert-dialog.js.map +1 -0
  6. package/dist/base-recipes.d.ts +184 -0
  7. package/dist/base-recipes.d.ts.map +1 -0
  8. package/dist/base-recipes.js +129 -0
  9. package/dist/base-recipes.js.map +1 -0
  10. package/dist/behaviors.d.ts +1254 -0
  11. package/dist/behaviors.d.ts.map +1 -0
  12. package/dist/behaviors.js +972 -0
  13. package/dist/behaviors.js.map +1 -0
  14. package/dist/bottom-navigation-defaults.d.ts +13 -0
  15. package/dist/bottom-navigation-defaults.d.ts.map +1 -0
  16. package/dist/bottom-navigation-defaults.js +10 -0
  17. package/dist/bottom-navigation-defaults.js.map +1 -0
  18. package/dist/bottom-navigation.d.ts +114 -0
  19. package/dist/bottom-navigation.d.ts.map +1 -0
  20. package/dist/bottom-navigation.js +223 -0
  21. package/dist/bottom-navigation.js.map +1 -0
  22. package/dist/breadcrumb.d.ts +97 -0
  23. package/dist/breadcrumb.d.ts.map +1 -0
  24. package/dist/breadcrumb.js +99 -0
  25. package/dist/breadcrumb.js.map +1 -0
  26. package/dist/calendar.d.ts +285 -0
  27. package/dist/calendar.d.ts.map +1 -0
  28. package/dist/calendar.js +297 -0
  29. package/dist/calendar.js.map +1 -0
  30. package/dist/card.d.ts +39 -0
  31. package/dist/card.d.ts.map +1 -0
  32. package/dist/card.js +50 -0
  33. package/dist/card.js.map +1 -0
  34. package/dist/carousel.d.ts +180 -0
  35. package/dist/carousel.d.ts.map +1 -0
  36. package/dist/carousel.js +172 -0
  37. package/dist/carousel.js.map +1 -0
  38. package/dist/catalog.d.ts +7158 -0
  39. package/dist/catalog.d.ts.map +1 -0
  40. package/dist/catalog.js +220 -0
  41. package/dist/catalog.js.map +1 -0
  42. package/dist/collection.d.ts +123 -0
  43. package/dist/collection.d.ts.map +1 -0
  44. package/dist/collection.js +211 -0
  45. package/dist/collection.js.map +1 -0
  46. package/dist/color-references.d.ts +46 -0
  47. package/dist/color-references.d.ts.map +1 -0
  48. package/dist/color-references.js +39 -0
  49. package/dist/color-references.js.map +1 -0
  50. package/dist/colors.d.ts +56 -0
  51. package/dist/colors.d.ts.map +1 -0
  52. package/dist/colors.js +90 -0
  53. package/dist/colors.js.map +1 -0
  54. package/dist/command-palette.d.ts +240 -0
  55. package/dist/command-palette.d.ts.map +1 -0
  56. package/dist/command-palette.js +97 -0
  57. package/dist/command-palette.js.map +1 -0
  58. package/dist/component-contracts.d.ts +137 -0
  59. package/dist/component-contracts.d.ts.map +1 -0
  60. package/dist/component-contracts.js +53 -0
  61. package/dist/component-contracts.js.map +1 -0
  62. package/dist/component-definitions.d.ts +135 -0
  63. package/dist/component-definitions.d.ts.map +1 -0
  64. package/dist/component-definitions.js +142 -0
  65. package/dist/component-definitions.js.map +1 -0
  66. package/dist/component-recipes.d.ts +3583 -0
  67. package/dist/component-recipes.d.ts.map +1 -0
  68. package/dist/component-recipes.js +1503 -0
  69. package/dist/component-recipes.js.map +1 -0
  70. package/dist/component-references.d.ts +416 -0
  71. package/dist/component-references.d.ts.map +1 -0
  72. package/dist/component-references.js +140 -0
  73. package/dist/component-references.js.map +1 -0
  74. package/dist/content-state.d.ts +104 -0
  75. package/dist/content-state.d.ts.map +1 -0
  76. package/dist/content-state.js +116 -0
  77. package/dist/content-state.js.map +1 -0
  78. package/dist/counter-badge-recipe.d.ts +80 -0
  79. package/dist/counter-badge-recipe.d.ts.map +1 -0
  80. package/dist/counter-badge-recipe.js +44 -0
  81. package/dist/counter-badge-recipe.js.map +1 -0
  82. package/dist/counter-badge.d.ts +12 -0
  83. package/dist/counter-badge.d.ts.map +1 -0
  84. package/dist/counter-badge.js +21 -0
  85. package/dist/counter-badge.js.map +1 -0
  86. package/dist/data-table.d.ts +203 -0
  87. package/dist/data-table.d.ts.map +1 -0
  88. package/dist/data-table.js +182 -0
  89. package/dist/data-table.js.map +1 -0
  90. package/dist/date-picker.d.ts +268 -0
  91. package/dist/date-picker.d.ts.map +1 -0
  92. package/dist/date-picker.js +168 -0
  93. package/dist/date-picker.js.map +1 -0
  94. package/dist/description-list.d.ts +68 -0
  95. package/dist/description-list.d.ts.map +1 -0
  96. package/dist/description-list.js +85 -0
  97. package/dist/description-list.js.map +1 -0
  98. package/dist/design-system-provider.d.ts +80 -0
  99. package/dist/design-system-provider.d.ts.map +1 -0
  100. package/dist/design-system-provider.js +147 -0
  101. package/dist/design-system-provider.js.map +1 -0
  102. package/dist/evidence.d.ts +49 -0
  103. package/dist/evidence.d.ts.map +1 -0
  104. package/dist/evidence.js +133 -0
  105. package/dist/evidence.js.map +1 -0
  106. package/dist/file-picker.d.ts +183 -0
  107. package/dist/file-picker.d.ts.map +1 -0
  108. package/dist/file-picker.js +224 -0
  109. package/dist/file-picker.js.map +1 -0
  110. package/dist/floating-action-button.d.ts +143 -0
  111. package/dist/floating-action-button.d.ts.map +1 -0
  112. package/dist/floating-action-button.js +149 -0
  113. package/dist/floating-action-button.js.map +1 -0
  114. package/dist/form.d.ts +143 -0
  115. package/dist/form.d.ts.map +1 -0
  116. package/dist/form.js +206 -0
  117. package/dist/form.js.map +1 -0
  118. package/dist/foundations.d.ts +300 -0
  119. package/dist/foundations.d.ts.map +1 -0
  120. package/dist/foundations.js +238 -0
  121. package/dist/foundations.js.map +1 -0
  122. package/dist/grid.d.ts +75 -0
  123. package/dist/grid.d.ts.map +1 -0
  124. package/dist/grid.js +133 -0
  125. package/dist/grid.js.map +1 -0
  126. package/dist/icon-button-recipe.d.ts +99 -0
  127. package/dist/icon-button-recipe.d.ts.map +1 -0
  128. package/dist/icon-button-recipe.js +53 -0
  129. package/dist/icon-button-recipe.js.map +1 -0
  130. package/dist/icon.d.ts +41 -0
  131. package/dist/icon.d.ts.map +1 -0
  132. package/dist/icon.js +147 -0
  133. package/dist/icon.js.map +1 -0
  134. package/dist/image.d.ts +82 -0
  135. package/dist/image.d.ts.map +1 -0
  136. package/dist/image.js +100 -0
  137. package/dist/image.js.map +1 -0
  138. package/dist/index.d.ts +58 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +66 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/layout.d.ts +118 -0
  143. package/dist/layout.d.ts.map +1 -0
  144. package/dist/layout.js +118 -0
  145. package/dist/layout.js.map +1 -0
  146. package/dist/link.d.ts +57 -0
  147. package/dist/link.d.ts.map +1 -0
  148. package/dist/link.js +142 -0
  149. package/dist/link.js.map +1 -0
  150. package/dist/load-more.d.ts +64 -0
  151. package/dist/load-more.d.ts.map +1 -0
  152. package/dist/load-more.js +124 -0
  153. package/dist/load-more.js.map +1 -0
  154. package/dist/mentions.d.ts +67 -0
  155. package/dist/mentions.d.ts.map +1 -0
  156. package/dist/mentions.js +107 -0
  157. package/dist/mentions.js.map +1 -0
  158. package/dist/number-field.d.ts +208 -0
  159. package/dist/number-field.d.ts.map +1 -0
  160. package/dist/number-field.js +247 -0
  161. package/dist/number-field.js.map +1 -0
  162. package/dist/otp-field.d.ts +152 -0
  163. package/dist/otp-field.d.ts.map +1 -0
  164. package/dist/otp-field.js +117 -0
  165. package/dist/otp-field.js.map +1 -0
  166. package/dist/pagination.d.ts +181 -0
  167. package/dist/pagination.d.ts.map +1 -0
  168. package/dist/pagination.js +226 -0
  169. package/dist/pagination.js.map +1 -0
  170. package/dist/password-field.d.ts +187 -0
  171. package/dist/password-field.d.ts.map +1 -0
  172. package/dist/password-field.js +120 -0
  173. package/dist/password-field.js.map +1 -0
  174. package/dist/popover.d.ts +152 -0
  175. package/dist/popover.d.ts.map +1 -0
  176. package/dist/popover.js +137 -0
  177. package/dist/popover.js.map +1 -0
  178. package/dist/progress-recipe.d.ts +43 -0
  179. package/dist/progress-recipe.d.ts.map +1 -0
  180. package/dist/progress-recipe.js +16 -0
  181. package/dist/progress-recipe.js.map +1 -0
  182. package/dist/recipes.d.ts +31 -0
  183. package/dist/recipes.d.ts.map +1 -0
  184. package/dist/recipes.js +46 -0
  185. package/dist/recipes.js.map +1 -0
  186. package/dist/responsive.d.ts +27 -0
  187. package/dist/responsive.d.ts.map +1 -0
  188. package/dist/responsive.js +66 -0
  189. package/dist/responsive.js.map +1 -0
  190. package/dist/result.d.ts +111 -0
  191. package/dist/result.d.ts.map +1 -0
  192. package/dist/result.js +97 -0
  193. package/dist/result.js.map +1 -0
  194. package/dist/selection-helpers.d.ts +16 -0
  195. package/dist/selection-helpers.d.ts.map +1 -0
  196. package/dist/selection-helpers.js +52 -0
  197. package/dist/selection-helpers.js.map +1 -0
  198. package/dist/semantic-colors.d.ts +275 -0
  199. package/dist/semantic-colors.d.ts.map +1 -0
  200. package/dist/semantic-colors.js +84 -0
  201. package/dist/semantic-colors.js.map +1 -0
  202. package/dist/sheet.d.ts +51 -0
  203. package/dist/sheet.d.ts.map +1 -0
  204. package/dist/sheet.js +69 -0
  205. package/dist/sheet.js.map +1 -0
  206. package/dist/showcase.d.ts +153 -0
  207. package/dist/showcase.d.ts.map +1 -0
  208. package/dist/showcase.js +210 -0
  209. package/dist/showcase.js.map +1 -0
  210. package/dist/side-panel.d.ts +199 -0
  211. package/dist/side-panel.d.ts.map +1 -0
  212. package/dist/side-panel.js +111 -0
  213. package/dist/side-panel.js.map +1 -0
  214. package/dist/slider.d.ts +138 -0
  215. package/dist/slider.d.ts.map +1 -0
  216. package/dist/slider.js +150 -0
  217. package/dist/slider.js.map +1 -0
  218. package/dist/splitter.d.ts +113 -0
  219. package/dist/splitter.d.ts.map +1 -0
  220. package/dist/splitter.js +99 -0
  221. package/dist/splitter.js.map +1 -0
  222. package/dist/statistic.d.ts +41 -0
  223. package/dist/statistic.d.ts.map +1 -0
  224. package/dist/statistic.js +76 -0
  225. package/dist/statistic.js.map +1 -0
  226. package/dist/steps.d.ts +196 -0
  227. package/dist/steps.d.ts.map +1 -0
  228. package/dist/steps.js +160 -0
  229. package/dist/steps.js.map +1 -0
  230. package/dist/tag.d.ts +128 -0
  231. package/dist/tag.d.ts.map +1 -0
  232. package/dist/tag.js +93 -0
  233. package/dist/tag.js.map +1 -0
  234. package/dist/timeline.d.ts +147 -0
  235. package/dist/timeline.d.ts.map +1 -0
  236. package/dist/timeline.js +127 -0
  237. package/dist/timeline.js.map +1 -0
  238. package/dist/toast.d.ts +164 -0
  239. package/dist/toast.d.ts.map +1 -0
  240. package/dist/toast.js +529 -0
  241. package/dist/toast.js.map +1 -0
  242. package/dist/tokens.d.ts +9 -0
  243. package/dist/tokens.d.ts.map +1 -0
  244. package/dist/tokens.js +9 -0
  245. package/dist/tokens.js.map +1 -0
  246. package/dist/tooltip.d.ts +44 -0
  247. package/dist/tooltip.d.ts.map +1 -0
  248. package/dist/tooltip.js +88 -0
  249. package/dist/tooltip.js.map +1 -0
  250. package/dist/tour.d.ts +218 -0
  251. package/dist/tour.d.ts.map +1 -0
  252. package/dist/tour.js +211 -0
  253. package/dist/tour.js.map +1 -0
  254. package/dist/transfer-list.d.ts +207 -0
  255. package/dist/transfer-list.d.ts.map +1 -0
  256. package/dist/transfer-list.js +193 -0
  257. package/dist/transfer-list.js.map +1 -0
  258. package/dist/tree-select.d.ts +78 -0
  259. package/dist/tree-select.d.ts.map +1 -0
  260. package/dist/tree-select.js +132 -0
  261. package/dist/tree-select.js.map +1 -0
  262. package/dist/tree.d.ts +206 -0
  263. package/dist/tree.d.ts.map +1 -0
  264. package/dist/tree.js +223 -0
  265. package/dist/tree.js.map +1 -0
  266. package/dist/upload-item.d.ts +173 -0
  267. package/dist/upload-item.d.ts.map +1 -0
  268. package/dist/upload-item.js +156 -0
  269. package/dist/upload-item.js.map +1 -0
  270. package/dist/version.d.ts +3 -0
  271. package/dist/version.d.ts.map +1 -0
  272. package/dist/version.js +3 -0
  273. package/dist/version.js.map +1 -0
  274. package/docs/affix.md +63 -0
  275. package/docs/anchor.md +59 -0
  276. package/docs/ant-design-coverage.md +119 -0
  277. package/docs/app-provider.md +50 -0
  278. package/docs/app-rn-adoption.md +227 -0
  279. package/docs/architecture.md +320 -0
  280. package/docs/authoring-brief.md +87 -0
  281. package/docs/border-beam.md +66 -0
  282. package/docs/bottom-navigation.md +127 -0
  283. package/docs/breadcrumb.md +82 -0
  284. package/docs/calendar.md +154 -0
  285. package/docs/carousel.md +130 -0
  286. package/docs/cascader.md +93 -0
  287. package/docs/catalog-decision-status.md +306 -0
  288. package/docs/color-picker.md +72 -0
  289. package/docs/command-palette.md +116 -0
  290. package/docs/confirm-popover.md +93 -0
  291. package/docs/consistency-audit.md +383 -0
  292. package/docs/consumer-release-gate.md +90 -0
  293. package/docs/content-state.md +156 -0
  294. package/docs/context-panel.md +88 -0
  295. package/docs/cross-platform-core-normalization.md +118 -0
  296. package/docs/data-table.md +79 -0
  297. package/docs/date-picker.md +84 -0
  298. package/docs/description-list.md +75 -0
  299. package/docs/design-system-provider.md +139 -0
  300. package/docs/dropdown.md +78 -0
  301. package/docs/expansion-roadmap.md +285 -0
  302. package/docs/file-picker.md +59 -0
  303. package/docs/floating-action-button.md +111 -0
  304. package/docs/form.md +134 -0
  305. package/docs/generated/component-maturity.md +103 -0
  306. package/docs/generated/renderer-evidence.json +5597 -0
  307. package/docs/generated/renderer-evidence.md +132 -0
  308. package/docs/generated/showcase-manifest.json +3732 -0
  309. package/docs/icon.md +22 -0
  310. package/docs/identity.md +121 -0
  311. package/docs/image.md +84 -0
  312. package/docs/implementation-0.5.md +85 -0
  313. package/docs/layout-primitives.md +107 -0
  314. package/docs/layout.md +83 -0
  315. package/docs/library-reference-decisions.md +123 -0
  316. package/docs/link.md +67 -0
  317. package/docs/load-more.md +31 -0
  318. package/docs/mentions.md +82 -0
  319. package/docs/migration-0.2.md +108 -0
  320. package/docs/migration-0.3.md +72 -0
  321. package/docs/migration-0.5.md +82 -0
  322. package/docs/migration-0.6.md +197 -0
  323. package/docs/notification.md +54 -0
  324. package/docs/number-field.md +82 -0
  325. package/docs/otp-field.md +103 -0
  326. package/docs/pagination.md +139 -0
  327. package/docs/password-field.md +98 -0
  328. package/docs/popover.md +124 -0
  329. package/docs/promotion-candidates.md +169 -0
  330. package/docs/qr-code.md +69 -0
  331. package/docs/rating.md +58 -0
  332. package/docs/responsive-grid.md +90 -0
  333. package/docs/result.md +96 -0
  334. package/docs/showcase.md +97 -0
  335. package/docs/side-panel.md +71 -0
  336. package/docs/slider.md +79 -0
  337. package/docs/splitter.md +55 -0
  338. package/docs/statistic.md +16 -0
  339. package/docs/steps.md +115 -0
  340. package/docs/tag.md +57 -0
  341. package/docs/time-picker.md +88 -0
  342. package/docs/timeline.md +146 -0
  343. package/docs/toast.md +127 -0
  344. package/docs/tooltip.md +58 -0
  345. package/docs/tour.md +136 -0
  346. package/docs/transfer-list.md +94 -0
  347. package/docs/tree-select.md +101 -0
  348. package/docs/tree.md +123 -0
  349. package/docs/upload-item.md +68 -0
  350. package/docs/utility.md +53 -0
  351. package/docs/virtual-list.md +70 -0
  352. package/docs/watermark.md +58 -0
  353. package/package.json +402 -0
@@ -0,0 +1,50 @@
1
+ # AppProvider — 새 컴포넌트를 만들지 않는다
2
+
3
+ ## 문제로 제기된 것
4
+
5
+ antd `App`은 `message`/`notification`/`Modal`을 **명령형 훅**(`App.useApp()`)으로
6
+ 어디서든 부를 수 있게 context를 심어 주는 컴포넌트다 — 정적 `Modal.confirm()` 같은
7
+ 호출이 실제로는 테마·locale context에 접근할 수 있도록 antd가 마련한 배선이다.
8
+
9
+ ## 판정: 런타임을 걷어내면 아무것도 남지 않는다
10
+
11
+ `App`이 명령형으로 여는 세 가지는 이미 전부 다른 HJM 계약에 배정돼 있다.
12
+
13
+ | antd `App`이 여는 것 | HJM 대응 | 상태 |
14
+ | --- | --- | --- |
15
+ | `message` | `Toast` | 이미 계약, `beta` |
16
+ | `notification` | `Toast`(`docs/notification.md`가 이미 alias로 흡수) | 이미 계약 |
17
+ | `Modal.confirm`/`Modal.info` 등 | `Dialog`/`AlertDialog`(`decomposed`, crosswalk 확정) | 이미 계약 |
18
+
19
+ 즉 `message`/`notification`/`modal`이 **무엇을 보여주는지**는 이미 다 계약돼 있다.
20
+ `App`이 실제로 더하는 것은 오직 하나 — "이미 계약된 그 표면을, 컴포넌트 트리 아무 곳에서나
21
+ `useApp()`으로 명령형 호출할 수 있게 context에 인스턴스를 심어 둔다"는 **배선**이다. 그건
22
+ 정의상 React Context + 특정 프레임워크의 훅 API이고, 이 패키지는 React도 RN도 import하지
23
+ 않는다(`docs/architecture.md`). 새로운 상태 축도, 새로운 접근성 개념도, 새로운 시각
24
+ recipe도 없다 — Toast/Dialog/AlertDialog가 이미 가진 것 이상으로 계약할 것이 없다.
25
+
26
+ `DesignSystemProvider`(`docs/design-system-provider.md`)와 비교하면 차이가 분명하다:
27
+ 그쪽은 런타임을 걷어내도 "테마·방향·배율·모션 선호"라는 **값 타입**이 남았다. `App`은
28
+ 걷어내면 **값 타입조차 남지 않는다** — 남는 셋(Toast/Dialog/AlertDialog)이 이미 각자의
29
+ 파일에서 완결된 계약이기 때문이다.
30
+
31
+ ## 결론
32
+
33
+ `src/app-provider.ts`, `test/app-provider.test.ts`는 만들지 않는다. 제품이 "어디서든
34
+ Toast/AlertDialog를 부르고 싶다"는 요구를 실제로 갖게 되면, 그건 새 HJM 계약이 아니라
35
+ **각 제품 renderer가 자기 프레임워크(React Context, RN 동등물)로 만드는 명령형 wrapper**
36
+ 다 — Toast의 `createToastStore`/`createToastSession`이 이미 큐·타이머·중복 처리를
37
+ 소유하고 있으므로 renderer는 그 세션에 접근하는 hook만 얹으면 된다.
38
+
39
+ ## 판정이 뒤집힐 조건
40
+
41
+ Toast/Dialog/AlertDialog 중 무엇으로도 표현할 수 없는 넷째 표면이 `App`에 새로 필요하다는
42
+ 것이 확인되면(예: 화면 밖 push나 시스템 알림함처럼 `docs/notification.md`가 이미 배제
43
+ 조건으로 남긴 것), 그때는 그 표면부터 별도로 계약하고 `App`의 배선 문제는 그 이후에
44
+ 다시 본다.
45
+
46
+ ## 배선 명세 (리드 참고)
47
+
48
+ catalog의 `{ name: "AppProvider", category: "provider", platform: "adaptive", status:
49
+ "planned", aliases: ["App"] }`(`src/catalog.ts:124`)는 그대로 둔다 — recipe/behavior가
50
+ 없었고 지금도 없다. crosswalk의 `App → AppProvider`(`adapted`)도 바꿀 필요 없다.
@@ -0,0 +1,227 @@
1
+ # app-rn 채택 계획 — 어느 계약을 채택하면 결함이 실제로 줄어드는가
2
+
3
+ 이 문서는 코드를 만들지 않는다. `hjm-design-system`·`yajalal/modules/app-rn` 두 저장소를 읽기 전용으로
4
+ 조사해, 「어느 계약을 app-rn이 채택하면 실측된 결함이 실제로 줄어드는가」에 근거로 답한다.
5
+ 근거가 되는 실측 자료:
6
+
7
+ - `/Users/jimin/Desktop/yajalal/.claude/qa/runs/20260818_234500_app-rn-ui-design/review.md` — 28화면 검수 요약, 반복 패턴 표
8
+ - 같은 폴더의 `designer-brief.md`(반복 패턴 원문, 8개 항목), `bugs.md`(결함 ID), `reviews/*.md`(화면별 보고서 19편)
9
+ - `modules/app-rn/DESIGN_SYSTEM.md` — 판단 기준(§1~§10), 특히 §7 컴포넌트 경계
10
+ - `modules/app-rn/src/lib/theme/*`, `modules/app-rn/src/design-boundary.test.ts` — 실제로 무엇이 경계를 건너가는지
11
+ - `docs/promotion-candidates.md`, `src/catalog.ts`, `docs/expansion-roadmap.md` — hjm 쪽 승격 후보와 성숙도
12
+
13
+ ## 0. 가장 먼저 답해야 했던 질문 — 그 경계로 무엇이 건너가는가
14
+
15
+ 리드의 짐작대로였다. **토큰(색·간격·타이포·radius·motion)만 건너가고, 컴포넌트 계약은 하나도
16
+ 건너가지 않는다.** `modules/app-rn/src/design-boundary.test.ts:512-551`의
17
+ `hjmImportsOutsideTheme` 감사는 `@hjmds/design-contracts` import를 `lib/theme/` 밖에서 쓰면
18
+ 빌드를 깨뜨린다 — 그래서 이 확인은 근사치가 아니라 게이트로 강제된 사실이다.
19
+
20
+ `lib/theme/` 안에서 실제로 `@hjmds/design-contracts`을 import하는 이름을 전부 세면:
21
+
22
+ - 순수 토큰: `spacing`, `radius`, `motion`, `typography`, `withAlpha`, `control`, `glyph`,
23
+ `overlay`, `scrim`, `shadow`, `brandGradient`, `onBrandGradient`, `onAccentFill`,
24
+ `accentFill`, `ACCENTS`, `THEMES` — `shared-theme-bridge.ts:1-6`, `index.ts:9-16,1240-1254`
25
+ - 타입: `AccentTone`, `ResolvedTheme`, `ThemeColors`, `ThemePreference`, `TextVariant`,
26
+ `GlyphSize`, `FieldShape`, `FieldVariant`, `ButtonSize`, `SegmentedControlSize` —
27
+ `index.ts:1258-1268,1838`, `foundations.ts:1`
28
+ - 시각 레시피(순수 함수/객체, 행동 없음) **딱 넷**: `buttonRecipe`(`buttons.ts:1`),
29
+ `surfaceRecipe`(`surfaces.ts:1`), `fieldRecipe`·`segmentedControlRecipe`(`index.ts:9-16`)
30
+
31
+ 그런데 실제 컴포넌트 레이어(`components/ui/*.tsx`)가 쓰는 시각 레시피는 이 넷이 아니다.
32
+ `index.ts`를 훑으면 **19곳**에 다음과 같은 주석이 붙어 있고, 이 주석 자체가 "아직 채택
33
+ 전"이라는 저자의 진단이다:
34
+
35
+ > `/** Temporary v0.1 bridge matching the [upstream/renderer-neutral] HJM v0.2 <X> recipe exactly. */`
36
+
37
+ | 줄 | 대상 | hjm 쪽 정의 |
38
+ |---|---|---|
39
+ | `index.ts:102` | Icon | `src/icon.ts` (`iconRecipe`, `component-recipes.ts`) |
40
+ | `index.ts:223` | Sheet(behavior) | `src/sheet.ts` |
41
+ | `index.ts:1017` | Menu | `src/component-recipes.ts` |
42
+ | `index.ts:1311` | AlertDialog(type) | `src/alert-dialog.ts` |
43
+ | `index.ts:1865` | Tabs | `src/component-recipes.ts` |
44
+ | `index.ts:1914` | SearchField | `src/component-recipes.ts` |
45
+ | `index.ts:2002` | Chip | `src/component-recipes.ts` |
46
+ | `index.ts:2056` | Accordion | `src/component-recipes.ts` |
47
+ | `index.ts:2110` | AlertDialog(recipe) | `src/component-recipes.ts` |
48
+ | `index.ts:2269` | Badge | `src/component-recipes.ts:1428` 부근 |
49
+ | `index.ts:2338` | Switch | `src/component-recipes.ts` |
50
+ | `index.ts:2383` | Progress | `src/component-recipes.ts` |
51
+ | `index.ts:2404` | **Statistic** | `src/component-recipes.ts:1428`, `src/statistic.ts` |
52
+ | `index.ts:2488` | Toast | `src/toast.ts` |
53
+ | `index.ts:2595` | Divider | `src/component-recipes.ts:539` |
54
+ | `index.ts:2608` | Text | `src/component-recipes.ts` |
55
+ | `index.ts:2675` | **List** | `src/component-recipes.ts:551` |
56
+ | `index.ts:2687` | **ListRow** | `src/component-recipes.ts:564` |
57
+ | `index.ts:2722` | **Section** | `src/component-recipes.ts:1923` |
58
+
59
+ 즉 "56개 계약 중 무엇을 채택할까"는 잘못된 질문 구조다. **정답은 이미 저자들이 적어
60
+ 뒀다 — 19개 자리가 명시적으로 "임시 다리"라고 스스로 선언했다.** 이 문서가 답할 질문은
61
+ "이 19개 중 무엇을 먼저 실제 import로 바꾸면 결함이 줄어드는가"이다.
62
+
63
+ `expansion-roadmap.md`의 "공개 배치 → 현재 실제 앱에서 검증 중" 절은 Button/Field/Surface/
64
+ IconButton/Badge/CounterBadge/SegmentedControl/Tabs/SearchField/Checkbox 계열/List/ListRow/
65
+ Divider/Section/Notice/EmptyState/Skeleton/Sheet/Dialog/Toast/BottomCTA/TopBar/Select/Combobox
66
+ **약 24개**를 나열하지만, 실측하면 그중 진짜로 import되는 것은 Button·Field·Surface·
67
+ SegmentedControl 넷뿐이다(위 표). 나머지는 hjm 쪽 테스트가 app-rn 모양을 본떠 검증한
68
+ 것이지, app-rn이 실제로 그 코드를 쓰고 있다는 뜻이 아니다. 이 간극 자체가
69
+ `expansion-roadmap.md`가 이미 경고한 실수("네 문서가 존재하지 않는 화면을 후보로
70
+ 적어 뒀다")와 같은 모양이라 별도로 적어 둔다 — 다음 저자가 로드맵 문구만 보고
71
+ "이미 검증됐다"로 오해하지 않도록.
72
+
73
+ ## 1. 1순위 결함 계열 — 지금은 채택할 계약이 없다 (설계 중)
74
+
75
+ 확산 1위(§9, 「상태가 화면 크롬을 삼킨다」, 6화면+라우트 7개, `bugs.md:295-303,355-358`)와
76
+ 7위(§6, 「구역 실패를 화면 전체로 그린다」, 3화면, `designer-brief.md:82-84`)는 **같은
77
+ 원인**이다: app-rn은 `AppStateView`(화면 전체)와 `AppStateRegion`(구역)을 구별하는데
78
+ (`DESIGN_SYSTEM.md:184-188`), **hjm 카탈로그에는 이 축이 없다.** `Result`(`catalog.ts`의
79
+ `status: "planned"`)를 이 축으로 흡수할 수 있는지는 이미 조사됐고 결론은 "흡수 아님"이다
80
+ — `docs/promotion-candidates.md`의 "후속 판정 완료" 절: Result의 실제 문제(행동 뒤 flow
81
+ terminus)는 두 제품 어디에도 없고, 두 제품이 실제로 가르는 축은 "실패가 화면 전체를
82
+ 막는가 구역만 막는가"라는 **제3의 축**이며 Result의 `status`에도 EmptyState의 tone에도
83
+ 없다.
84
+
85
+ **그러니 이 두 계열은 이번 「처음 셋」에 넣지 않는다.** 다른 저작자가 이 축을 설계
86
+ 중이므로, 설계가 나오면 이 문서에서 확산 범위가 가장 넓은(6+7, 3화면) 최우선 후보가
87
+ 된다. 지금 고르는 셋은 **이미 있는 계약 중에서** 고른 것이고, 이 축의 부재가 이번 선택이
88
+ 결함 1·2위를 다루지 못하는 이유임을 여기 명시해 둔다 — 다음 사람이 "왜 가장 큰 결함이
89
+ 채택 계획에 없나"를 다시 묻지 않도록.
90
+
91
+ ## 2. 후보 평가
92
+
93
+ ### 2.1 Statistic — 채택 권고 1순위
94
+
95
+ - **인용**: app-rn `lib/theme/index.ts:2404`("Temporary v0.1 bridge matching the
96
+ renderer-neutral HJM v0.2 Statistic recipe exactly")부터 `index.ts:2405-2469`까지가
97
+ `statisticRecipe` 전체 재구현이고, `components/ui/statistic-renderer-contract.ts:1-9`가
98
+ 거기서 `resolveStatisticDescriptor`, `statisticTrendMarks`, `validateStatisticGroup`을
99
+ 가져다 쓴다. hjm 쪽 원본은 `src/statistic.ts:16-131`(`StatisticDescriptor`,
100
+ `validateStatisticDescriptor`, `resolveStatisticDescriptor`, `statisticTrendMarks`) +
101
+ `src/component-recipes.ts:1428`(`statisticRecipe`, 슬롯·밀도·톤).
102
+ - **지금 자체 코드가 하는 일**: label/value/prefix/suffix/hint/trend를 가진 통계 카드
103
+ 하나(`AppStatistic`, `components/ui/AppStatistic.tsx:37-97`)와 그룹(`AppStatisticGroup`,
104
+ `:150-190`)을 렌더링한다. 실사용처는 `home/HomeScreen.tsx:262`(관심 선수 행의 보조
105
+ 통계), `player-explorer/PlayerExplorerScreen.tsx:953`, `fa/FaCenterScreen.tsx:424`(FA
106
+ 카드의 3열 주요 기록, `columns={fontScale >= 1.6 ? 1 : 3}`)이다. 이 셋은 label/value
107
+ 전용이거나 hint까지만 쓰고, 실측 범위 안에서 trend를 함께 쓰는 자리는 못 찾았다.
108
+ - **채택 비용**: `resolveStatisticDescriptor`·`validateStatisticGroup`·
109
+ `statisticTrendMarks`·디스크립터 타입은 hjm 쪽과 이미 필드 단위로 동일하다(같은 이름,
110
+ 같은 옵셔널 구조 — `statistic.ts:16-25` vs `AppStatisticProps` 정의). **열 수 계산
111
+ (`resolveStatisticColumnCount`, `statistic-renderer-contract.ts:65-` 부근, `fontScale`
112
+ 기반 단조 축소)은 hjm `statistic.ts`에 아예 없다** — 의도적으로 없다. hjm
113
+ `src/design-system-provider.ts:11-18`가 "`description-list.ts`'s local `fontScale`
114
+ clamp, which is that one component's own layout math"라고 명시하듯, 배율 반응형 레이아웃
115
+ 계산은 공용 계약이 아니라 렌더러 몫으로 hjm 스스로 설계했다. 그래서 채택은 **디스크립터
116
+ 검증·트렌드 마크만 hjm import로 바꾸고, 열 수 계산은 그대로 app-rn 소유로 남기는** 부분
117
+ 치환이다 — 배율 회귀를 재도입할 위험이 없다.
118
+ - **채택하면 사라지는 결함**: `review.md:36`의 반복 패턴 2("기준이 다른 두 수를 `·`로
119
+ 잇는다", §8, 5화면 — `designer-brief.md:85-86`이 원문 지목한 곳은 홈·랭킹·FA)와
120
+ `reviews/history.md:106`의 D-4("커리어 최고·최저의 모집단이 규정 출장 시즌인데 캡션이
121
+ 그 말을 하지 않고 기준이 다른 세 수를 `·`로 이었다")가 이 계열이다. **다만 이건
122
+ 조건부다** — 지금 그 자리들(홈의 순위 카드, 랭킹, FA 요약 캡션, 커리어 곡선)은
123
+ `AppStatistic`을 쓰지 않고 손으로 만든 캡션 문자열이다. `AppStatistic`을 채택하는
124
+ 것 자체가 이 결함을 지우지 않는다 — **label/hint/trend가 독립된 필드인 컴포넌트로
125
+ 그 캡션들을 옮겨야** "두 비교를 한 문자열에 이어붙이는" 실수 자체가 구조적으로
126
+ 불가능해진다(hint와 trend가 애초에 다른 슬롯이라 하나로 뭉칠 수 없다). 그러므로 이
127
+ 채택의 값은 "지금 바로 결함이 줄어든다"가 아니라 "계약을 정리해 두면 다음 리팩터가
128
+ 그 자리들을 옮길 때 같은 실수를 구조적으로 막는다"는 것이다 — 정직하게 조건부로
129
+ 적는다.
130
+
131
+ ### 2.2 Icon — 채택 권고 2순위
132
+
133
+ - **인용**: app-rn `lib/theme/icon.ts:9-53`의 `semanticIconNames` 42개 배열이 hjm
134
+ `src/icon.ts:9-53`의 배열과 **순서까지 완전히 동일하다**(직접 대조 완료, 한 글자도
135
+ 다르지 않다). `index.ts:102`("Temporary v0.1 bridge matching the renderer-neutral HJM
136
+ v0.2 Icon recipe exactly")가 `iconRecipe`를 다시 선언한다.
137
+ - **지금 자체 코드가 하는 일**: `components/ui/AppIcon.tsx`가 `lucide-react-native`
138
+ 글리프를 이 42개 semantic name에 매핑해 모든 화면에 공급한다(`design-boundary.test.ts`의
139
+ `LUCIDE_VALUE_IMPORT_BASELINE`이 이 매핑의 유일한 창구임을 강제).
140
+ - **채택 비용**: 사실상 0에 가깝다 — 배열이 이미 완전히 같으므로 `export` 문 하나를
141
+ `@hjmds/design-contracts`으로 바꾸는 문제다. `expansion-roadmap.md`의 Icon 절
142
+ ("Yajalal Statistic trend mark에서 같은 semantic registry·tone·size·stroke·RTL 계약을
143
+ 실제 renderer로 검증했다")이 이미 이 정확한 이름 목록을 기준으로 hjm 쪽 beta 승격
144
+ 근거를 삼았다 — 즉 hjm 저자가 검증에 쓴 "실제 renderer"가 바로 이 앱의 모양이다.
145
+ - **채택하면 사라지는 결함**: 이번 라운드에서 아이콘 자체를 지목한 결함은 **찾지
146
+ 못했다**(`bugs.md`·`reviews/*.md` 전체에서 "아이콘"·"Icon" 검색 시 §6 인용문 하나뿐).
147
+ 그래서 이 후보는 defect-fix가 아니라 **예방/중복 제거** 근거로 든다 — 두 저장소가
148
+ 독립적으로 42개 이름을 유지하면 한쪽만 늘어나는 순간(`ai`처럼 이미 있는 이름 옆에
149
+ 다른 이름으로 같은 개념을 또 추가하는 식) §8 「같은 개념에는 하나의 단어」와 같은
150
+ 모양의 아이콘판 분열이 생긴다. 지금은 그 분열이 **없다**(완전 동일)는 것이 이 채택의
151
+ 가장 싼 창(window)이다 — 나중에 둘이 갈라진 뒤 합치는 것보다 지금 합치는 비용이 확실히
152
+ 낮다.
153
+
154
+ ### 2.3 List / ListRow / Section / Divider — 채택 권고 3순위 (묶음)
155
+
156
+ - **인용**: `index.ts:2595`(Divider), `2675`(List), `2687`(ListRow), `2722`(Section) 네
157
+ 자리가 연달아 "Temporary v0.1 bridge … recipe exactly"로 표시돼 있다. hjm 원본은
158
+ `component-recipes.ts:539`(divider), `551`(list), `564`(listRow), `1923`(section).
159
+ - **지금 자체 코드가 하는 일**: `AppList`/`AppListRow`/`AppSection`/`AppDivider`가 앱
160
+ 전체 화면 구조의 뼈대다 — `design-boundary.test.ts`의 `RAW_TEXT_BASELINE`만 봐도
161
+ `AppListRow.tsx`·`AppSection` 계열이 거의 모든 화면 파일에서 참조된다.
162
+ - **채택 비용**: 이 넷은 **행동을 전혀 갖지 않는 순수 시각 레시피**다(색·간격·radius·
163
+ 최소 높이·구분선 유무) — `ListRow`의 press 처리, 스프레드 안전성, 인터랙션 여부는
164
+ `AppListRow.tsx`/`design-boundary.test.ts`의 `appListRow`/`appLinkedListRowForbiddenProps`
165
+ 감사가 이미 별도로 강제하고 있고 그 로직은 레시피 교체와 무관하게 그대로 app-rn에
166
+ 남는다. 즉 이 셋(List/ListRow/Section/Divider)의 recipe만 hjm import로 바꾸는 것은
167
+ **행동 회귀 위험이 없는, 가장 안전한 범주의 채택**이다. `ListRow`의
168
+ `states.selectedBackground: themeColor("surfaceAccent")`(`index.ts:2698`)는 §6이 요구한
169
+ 대로 선택 상태 전용으로 이미 올바르게 쓰이고 있어(로컬 재구현이 hjm 계약과 어긋나는
170
+ 자리를 찾지 못했다), 치환이 시각적 회귀를 낼 가능성도 낮다.
171
+ - **채택하면 사라지는 결함**: **이번 라운드에서 List/ListRow/Section/Divider의 레시피
172
+ 자체를 지목한 결함은 없다.** 이 셋도 Icon과 같은 이유로 든다 — defect-fix가 아니라
173
+ **가장 넓은 중복 표면을 가장 싼 값에 줄이는** 근거다. 다만 Icon과 달리 이름 목록이
174
+ 완전히 같다는 확증은 못 했다(레시피 필드 형태는 대응되지만 `listRowRecipe`의
175
+ `density.compact.oneLineMinHeight: sharedControl.minTouchTarget` 같은 자리는 이미
176
+ 공용 토큰을 참조해 hjm 쪽과 자연히 맞물려 있다는 정황일 뿐, 필드 단위 완전 대조는
177
+ 하지 않았다) — 그래서 3순위로 낮춘다.
178
+
179
+ ### 2.4 함정을 확인한 자리 — 채택하지 않는 이유를 적는다
180
+
181
+ - **Tag / AppBadge** — `docs/promotion-candidates.md`가 "승격 가능"으로 이미 실측한
182
+ 후보(`FaCenterScreen.tsx:270,369`)이고 이름 문제도 없다(팀 지적대로 `AppBadge`는
183
+ 실제로 `Tag`의 문제를 푼다). 그런데 이번 조사에서 **AppBadge의 `tone="brand"`가
184
+ 26개 파일에 흩어져 있는 것을 발견**하고(`grep`으로 확인, 예:
185
+ `features/lineup/LineupScreen.tsx:466`, `features/ai/AiAnalysisScreen.tsx:516`,
186
+ `features/live/LiveScreen.tsx:792,820`) §6 위반(선택 아닌 정적 라벨이 브랜드 틴트를
187
+ 입는 것)을 의심했으나, `index.ts:2284-2290`의 주석이 이미 그 교훈을 반영해 뒀다 —
188
+ `brand` 톤의 `background`는 `surfaceAlt`(중립)이고 `content`만 브랜드색이다("The brand
189
+ tint now means 'selected' everywhere, so a badge that only labels something … cannot
190
+ wear it"). **즉 의심한 결함은 이미 로컬에서 고쳐져 있다.** Tag 계약을 채택해도
191
+ 지금 없는 결함을 지울 수 없으므로 이번 셋에서 뺀다.
192
+ - **Chip** — `reviews/overall-onboarding.md:59-60`에 실제 결함이 있다
193
+ (`OverallScreen.tsx:256-278`가 순수 요약 칩 줄에 `selectionMode="single"`을 넘겨
194
+ `accessibilityRole="radio"`+`surfaceAccent` 선택 면을 입힌다, §6 위반). 그러나 이건
195
+ **호출부가 계약을 잘못 쓴 것**이지 `AppChip`/`chipRecipe`(`index.ts:2002`)의 결함이
196
+ 아니다 — hjm의 `Chip` 계약을 채택해도 호출부가 여전히 `selectionMode="single"`을
197
+ 잘못 넘기면 같은 결함이 재현된다. 채택 후보가 아니라 **개발자에게 넘길 화면 버그**다.
198
+ - **Result** — `docs/promotion-candidates.md`의 "후속 판정 완료" 절이 이미 "흡수 아님"
199
+ 으로 답했다(위 §1 참고). 채택 후보 아님.
200
+ - **DataTable** — `catalog.ts`에 `platform: "web"`으로 고정돼 있어 RN 앱인 app-rn에는
201
+ 애초에 대상이 아니다. `StatTable`/`StandingsTable`의 배율 버그
202
+ (`reviews/live-standings.md:53-57`의 D-7 — 계약이 계산한 `columnScale`을 렌더러가
203
+ 한 번도 읽지 않는 버그)는 app-rn 자체 계약(`stat-table-contract.ts`,
204
+ `standings-table-contract.ts`)의 배선 문제이지 hjm 승격으로 풀리는 문제가 아니다.
205
+ - **Carousel** — `docs/promotion-candidates.md`가 이미 "보류"로 하향 조정했다(필수
206
+ 계약인 previous/next·dot·`inert`·adjustable 접근성이 `HomeGameCarousel`에 전혀 없다).
207
+ 같은 결론을 유지한다.
208
+
209
+ ## 3. 결론 — 처음 셋
210
+
211
+ | 순위 | 후보 | 결함 연결 | 비용 | 비고 |
212
+ |---|---|---|---|---|
213
+ | 1 | **Statistic** | §8 반복 패턴 2(5화면) — 조건부: 계약 채택 자체가 아니라 호출부 이전까지 마쳐야 닫힌다 | 낮음(디스크립터·검증·트렌드 마크만 교체, 배율 계산은 로컬 유지) | 이미 `index.ts:2404`에 "temporary" 명시 |
214
+ | 2 | **Icon** | 없음(예방/중복 제거) | 사실상 0(이름 목록 완전 동일) | hjm이 이미 이 앱 모양으로 beta 검증을 마쳤다고 주장(`expansion-roadmap.md`) — 검증 근거를 실제로 확인하려면 import 자체가 필요 |
215
+ | 3 | **List/ListRow/Section/Divider** | 없음(예방/중복 제거, 가장 넓은 표면) | 낮음(행동 없는 순수 레시피, 기존 행동 계약과 분리돼 있음) | `selectedBackground` 등 §6 규칙과 이미 정합적이라 회귀 위험 낮음 |
216
+
217
+ 우선순위는 확산 범위보다 **비용과 회귀 위험**으로 갈랐다 — 이번 조사의 결론은 지금
218
+ 당장 defect count를 줄이는 채택이 하나(Statistic, 그것도 조건부)뿐이라는 것이었고,
219
+ 그래서 나머지 둘은 "결함을 지운다"가 아니라 "이미 저자 스스로 임시라고 적어 둔 19개
220
+ 다리 중, 지금 합쳐도 안전하고 나중에 갈라지면 비싸지는 것부터 닫는다"는 다른 기준으로
221
+ 골랐다. §1의 상태 축이 설계되면 그것이 이 표의 1순위를 대체해야 한다.
222
+
223
+ ## 4. 게이트
224
+
225
+ `cd /Users/jimin/Desktop/hjm-design-system && pnpm typecheck && pnpm test` — 이 문서
226
+ 하나만 추가했고 `src/`·`test/`·공유 파일, app-rn·BurnTok 어느 파일도 고치지 않았다.
227
+ `pnpm build`는 실행하지 않았다.
@@ -0,0 +1,320 @@
1
+ # HJM Design System Architecture
2
+
3
+ ## 계약 계층
4
+
5
+ ```text
6
+ Foundations
7
+ color · spacing · radius · type · motion · elevation · layout
8
+ ↓
9
+ Semantic references
10
+ themeColor · accentColor · generic status roles
11
+ ↓
12
+ Shared component contracts
13
+ focus indicator · field frame · floating surface · collection item
14
+ ↓
15
+ Component recipes
16
+ slots · defaults · variants · sizes · states
17
+ ↕
18
+ Behavior and collection contracts
19
+ controlled state · keyboard · focus · accessibility · async collection
20
+ ↓
21
+ Platform renderers
22
+ React DOM / React Native
23
+ ↓
24
+ Product adapters
25
+ BurnTok vocabulary / Yajalal KBO vocabulary
26
+ ```
27
+
28
+ `@hjmds/design-contracts`은 TypeScript 외 런타임 의존성이 없는 계약 패키지로 유지합니다.
29
+ React, React Native, DOM, Expo import는 코어에 들어오지 않습니다.
30
+ 패키지 이름도 이 경계를 드러냅니다. `@hjmds/design-contracts`는 renderer-neutral core이고,
31
+ 실제 UI는 같은 저장소의 `@hjmds/react`와 `@hjmds/react-native`가 제공하므로 contracts를
32
+ component library로 오해하게 만드는 이전 `@hjm/design-system` package 이름은 사용하지 않습니다.
33
+
34
+ 시각 recipe와 behavior contract를 분리합니다. 전자는 어떤 모습인지, 후자는 어떤 상태와
35
+ 상호작용을 보장하는지를 말합니다. renderer는 다른 primitive를 쓸 수 있지만 같은 behavior
36
+ scenario를 통과해야 합니다.
37
+
38
+ ## Recipe 형식
39
+
40
+ 신규 recipe는 가능한 한 다음 항목을 가집니다.
41
+
42
+ - `slots`: root, label, icon, indicator처럼 스타일과 접근성 책임이 있는 anatomy
43
+ - `defaults`: HJM다운 기본 선택
44
+ - `tones` 또는 `variants`: 의미와 강조 수준
45
+ - `sizes`: 높이, padding, gap, glyph, text variant
46
+ - `states`: pressed, focused, selected, disabled, loading, invalid
47
+ - 플랫폼 renderer가 번역할 semantic color reference
48
+
49
+ Typography recipe의 `fontWeight`·`selectedFontWeight`·`checkedFontWeight`는 임의
50
+ `string`이 아니라 `FontWeightValue`(`fontWeight` 토큰 값 union)를 사용합니다. 기존 런타임
51
+ 값은 `"400" | "500" | "600" | "700" | "800"` 그대로지만 새 weight가 recipe에서 먼저
52
+ 생기거나 `"650"` 같은 값이 우회해서 들어오는 것은 typecheck에서 막습니다.
53
+
54
+ 모든 조합을 열어두는 것이 목표가 아닙니다. HJM다운 승인 조합만 public API로 만들고,
55
+ 제품 화면의 임의 style override보다 recipe의 새 variant를 우선합니다.
56
+
57
+ ## 지원 단계
58
+
59
+ - `stable`: 두 제품 이상 또는 두 플랫폼에서 사용되고 계약·접근성 검증이 있음
60
+ - `beta`: 실제 앱 패턴을 공용 recipe로 승격했지만 renderer parity 또는 시각 회귀가 진행 중
61
+ - `planned`: 범위에 포함되지만 API를 아직 안정화하지 않음
62
+ - `deprecated`: 새 사용을 막고 대체 경로와 제거 예정 버전을 문서화한 호환 계약
63
+
64
+ 현재 전체 범위와 목표 플랫폼 분류는 `componentCatalog`가 기계 판독 가능한 형태로 제공합니다.
65
+ 계획된 컴포넌트를 catalog에 올리는 것은 구현 완료를 의미하지 않습니다.
66
+ `recipeRegistry`가 실제 recipe와 catalog 이름을 타입으로 묶어 오타나 유령 계약을 막습니다.
67
+
68
+ 새 소비자는 `componentDefinitions`를 우선합니다. 이 normalized view는 display name이나 category가
69
+ 바뀌어도 유지되는 `ComponentId`, `primitive | component | provider | utility` kind, 복수
70
+ recipe/behavior 배열, Web/Native별 status와 고정 documentation story ID를 제공합니다. v0.2의
71
+ 평면 `componentCatalog`는 호환 view로 유지하며 renderer가 순차적으로 definition schema로
72
+ 이동합니다.
73
+
74
+ ## 플랫폼 분류
75
+
76
+ - `shared`: 같은 semantic API와 시각 계약을 Web/RN renderer가 각각 구현하는 목표
77
+ - `adaptive`: 같은 사용자 의도를 dialog/sheet, select/action sheet처럼 플랫폼에 맞게 구현
78
+ - `web`: data grid, tree, breadcrumb처럼 우선 Web에서 검증
79
+ - `native`: safe-area CTA처럼 우선 Native에서 검증
80
+
81
+ ## 선택형 탐색의 의미 경계
82
+
83
+ - `Tabs`: 서로 관련된 콘텐츠 panel 중 하나를 표시합니다. `keyed` mode는 tab과 panel을
84
+ 같은 stable key로 연결하고, `dynamic` mode는 모든 tab이 하나의 stable panel을 제어하며
85
+ 선택이 바뀔 때만 panel의 label과 content를 갱신합니다. 비동기 panel은 기본 `manual`
86
+ activation을 사용합니다.
87
+ - `SegmentedControl`: 현재 화면 안의 값·필터·표현 모드를 고릅니다. 별도 tabpanel 관계를
88
+ 만들지 않습니다.
89
+ - `BottomNavigation`: Web과 Native에서 앱의 2–6개 안정된 최상위 route를 이동합니다.
90
+ 콘텐츠 Tabs의 recipe, panel, roving focus를 재사용하지 않고 router의 현재 route를
91
+ read-only `selectedKey`로 받습니다. Web은 navigation landmark 안의 실제 link와
92
+ `aria-current="page"`, Native는 navigator의 selected state와 `tabPress`/`tabLongPress`를
93
+ 사용합니다. iOS navigator가 tab role을 안정적으로 지원하지 않는 경우 button+selected
94
+ fallback을 명시적으로 허용합니다.
95
+
96
+ BottomNavigation item은 destination만 허용합니다. 생성·작성처럼 새 작업을 시작하는 primary
97
+ action은 item, selected state, route count에서 제외하고 별도 Button/IconButton으로 합성합니다.
98
+ `center-gap` distribution은 이 sibling action의 자리를 예약할 뿐 action을 계약에 포함하지
99
+ 않습니다. 자세한 renderer 규칙은 `docs/bottom-navigation.md`에 둡니다.
100
+
101
+ ## 목적지와 동작의 경계
102
+
103
+ `Link`는 `href`가 있는 목적지이고 `Button`은 command입니다. 내부 route와 외부 URL을
104
+ discriminated destination으로 구분하되 Web은 실제 anchor/Next Link, Native는 Expo Router
105
+ Link 또는 external Linking adapter를 사용합니다. 공통 API가 `onClick`/`onPress`로 navigation을
106
+ 대체하지 않습니다.
107
+
108
+ disabled, visited application state, download는 공통 Link API에 없습니다. unavailable 목적지는
109
+ plain Text로, 파일 저장은 별도 workflow로, 인증·retry·back 같은 command는 Button으로
110
+ 표현합니다. Link의 icon은 semantic name만 허용하고 recipe가 크기·tone·방향을 소유합니다.
111
+ 별도 접근성 이름은 visible label을 포함하며 resolver는 숨은 플랫폼 field를 보존하지 않습니다.
112
+ 자세한 계약은 `docs/link.md`에 둡니다.
113
+
114
+ ## 선택 입력의 의미 경계
115
+
116
+ - `Checkbox`는 독립적인 참/거짓 값입니다. 집계 상태에서만 `mixed`를 허용하고, 기본
117
+ activate는 `mixed → checked`입니다.
118
+ - `CheckboxGroup`은 중복 없는 `ReadonlySet`을 값으로 사용합니다. 각 checkbox가 Web의
119
+ 독립 tab stop이며 방향키를 가로채지 않습니다. 긴 Native 목록은 그룹이 렌더링을 소유하지
120
+ 않고 같은 `Checkbox` row 계약을 가상 목록 안에서 조합합니다.
121
+ - `RadioGroup`은 하나 이하의 값을 선택합니다. Web은 roving focus와 방향·RTL을 반영한
122
+ 방향키 이동을 제공하고, Native는 각 radio의 `checked` 상태와 activate action을 제공합니다.
123
+ Radio를 그룹 밖의 독립 입력으로 공개하지 않습니다.
124
+ - 보이는 group label 또는 명시적인 accessibility label 중 하나는 필수입니다. label,
125
+ description, error는 서로 연결하고, disabled/readOnly는 상태를 바꾸지 않습니다.
126
+ - Native에는 Web의 `aria-required`, `aria-readonly`, `aria-invalid`와 동등한 공통 state가
127
+ 없으므로 해당 상태를 켜는 renderer API는 현지화된 상태 문구를 함께 요구하고
128
+ `accessibilityHint` 또는 오류 live region으로 전달합니다. 그룹의 `required`를 각 item의
129
+ required 상태로 복제하지 않습니다.
130
+
131
+ 선택 행 전체가 하나의 target이며 내부에 또 다른 button/link를 넣지 않습니다. `plain`과
132
+ `card`는 승인된 표현만 고르고, 선택 여부는 색뿐 아니라 checkbox check/dash 또는 radio dot으로
133
+ 항상 드러냅니다. 항목 간격은 임의 합성하지 않고 `vertical/plain=4`, `vertical/card=8`,
134
+ `horizontal/plain=12`, `horizontal/card=16`의 orientation×presentation 계약을 그대로 씁니다.
135
+
136
+ `keyed` Tabs의 mount policy는 `active`(활성 panel만), `visited`(방문한 panel 상태 보존),
137
+ `always`(모두 mount하되 비활성 panel은 hidden·inert)로 제한합니다. `dynamic` mode는 하나의
138
+ host 인스턴스를 유지하므로 `active`만 허용합니다. 자동 활성화는 panel이 이미 준비되어
139
+ 포커스 이동과 동시에 지연 없이 표시될 때만 명시적으로 선택합니다.
140
+
141
+ Native의 필수 접근성 baseline은 각 tab의 이름, `selected`/`disabled` 상태와 activate action입니다.
142
+ `tablist`/`tabpanel` role은 운영체제별 지원이 달라 renderer hint와 실제 기기 검증 항목으로 두며,
143
+ 중첩 control을 숨길 수 있는 panel 전체의 `accessible` 병합은 금지합니다.
144
+
145
+ ## 위험 확인의 생명주기
146
+
147
+ `AlertDialog`는 단순히 열린 boolean과 빨간 버튼을 공유하지 않습니다. 삭제·결제처럼 되돌릴 수
148
+ 없는 side effect를 한 번만 실행하고, 닫힘 모션이 끝난 뒤 결과를 돌려주는 session을 공통
149
+ 계약으로 사용합니다.
150
+
151
+ ```text
152
+ idle ─ confirm ─→ busy ─ success ─→ closing ─ exit complete ─→ closed
153
+ ↑ └─ failure ─→ error ─ retry ───────────────┘
154
+ └──────────── cancel / escape / back (idle 또는 error에서만) ─┘
155
+ ```
156
+
157
+ - `busy`에서는 confirm 연타와 cancel, Escape, Android back을 모두 무시합니다.
158
+ - outside press는 상태·콜백·결과를 전혀 바꾸지 않습니다.
159
+ - 오류 번역기가 실패하거나 빈 문장을 반환해도 현지화된 fallback 오류를 발표합니다.
160
+ - 결과 Promise는 action 시점이 아니라 실제 exit 완료 시 한 번만 끝납니다.
161
+ - provider unmount나 route 교체는 `interrupted` 결과로 정산해 pending Promise를 남기지 않습니다.
162
+ - confirm 모드는 cancel, 확인만 있는 alert 모드는 confirm에 초기 포커스를 둡니다.
163
+ - Web은 modal isolation·Tab trap·trigger focus restore를, Native는 custom Modal·back 처리·초기
164
+ accessibility focus·live error를 renderer에서 보장합니다.
165
+
166
+ 일반 `Dialog`와 `Sheet`도 이후 같은 `open reason → closing → exit complete` 문법을 공유하지만,
167
+ outside dismiss 허용 여부와 action/result 의미는 각 behavior가 따로 소유합니다.
168
+
169
+ ### Sheet 열림·닫힘 계약
170
+
171
+ Sheet renderer는 `SheetOpenState`의 controlled/uncontrolled 축을 섞지 않고 모든 닫힘을
172
+ `close-action | escape | back | outside | swipe | programmatic` 중 하나로 보고합니다.
173
+ 기본 `SheetDismissPolicy`는 idle 상태의 close action, outside, Escape/Android back만 허용하며
174
+ swipe와 busy 중 사용자 dismiss는 허용하지 않습니다. 제품이 직접 `open=false`로 바꾸는
175
+ programmatic close는 controlled owner의 권한이므로 policy나 busy 상태로 막지 않습니다.
176
+
177
+ `canDismissSheet`가 플랫폼 이벤트를 이 정책으로 해석하는 유일한 공통 경계입니다. 기본 RN
178
+ behavior 목록에는 아직 swipe를 넣지 않습니다. 실제 gesture capability가 있고 제품이
179
+ `swipeDismiss=true`를 선택한 renderer만 swipe를 연결하며, 그때만 drag handle을 표시합니다.
180
+ handle은 장식이 아니라 가능한 동작의 신호이므로 기본값은 hidden입니다.
181
+
182
+ 지속 마운트되는 Native Modal renderer는 `createSheetLifecycle`로 visible cycle을 발급하고
183
+ close 요청과 dismiss 완료를 각각 한 번만 수락합니다. iOS `onDismiss`나 플랫폼 adapter가
184
+ 확인한 종료 시점에 `completeDismiss(cycle)`을 호출해야 다음 surface를 열 수 있습니다.
185
+ Android의 `InteractionManager`만으로는 native Modal의 slide 종료를 확인할 수 없습니다.
186
+ Android까지 정확한 successor 순서가 필요한 renderer는 `animationType="none"`인 native host 안에서
187
+ 퇴장 모션을 직접 실행하고, 그 animation 완료 뒤 host를 내린 다음 cycle을 완료합니다.
188
+
189
+ Sheet의 visual recipe는 dismiss 정책을 소유하지 않습니다. border/shadow, title/body/footer의
190
+ type·gap, Web max-width, bottom safe-area의 additive padding, Reduce Motion fallback만 제공합니다.
191
+ renderer는 `paddingBottom + safe-area inset`을 적용하고, transform을 줄여도 exit 완료 콜백은
192
+ 항상 한 번 발생시켜야 합니다.
193
+
194
+ Overlay stack coordinator가 모든 renderer에 들어가기 전에는 modal surface를 중첩하지 않습니다.
195
+ Sheet 안에서 AlertDialog가 필요하면 Sheet를 먼저 닫고 exit/onDismiss 완료 뒤 후속 surface를
196
+ 엽니다. 이 규칙은 Web의 Escape·focus trap 경쟁과 iOS의 Modal-on-Modal 발표 순서 문제를
197
+ 동시에 피합니다.
198
+
199
+ ## Select와 Combobox의 적응형 경계
200
+
201
+ Select와 Combobox는 Menu와 같은 collection contract를 사용하지만 Menu role을 재사용하지
202
+ 않습니다. Web은 field + popover/listbox, Native는 field + modal Sheet/radio options로 렌더링하고
203
+ 선택 결과와 상태 의미만 공유합니다.
204
+
205
+ - `Select`는 기존 항목의 stable key 하나 또는 `null`만 값으로 사용합니다. label을 값으로
206
+ 저장하지 않습니다.
207
+ - `SelectItemDescriptor`는 Menu 전용 `tone`/`shortcut`을 허용하지 않습니다. 위험 action과
208
+ 단축키는 `MenuItemDescriptor`에만 존재해 Select/Combobox가 색만으로 의미를 만들지 않습니다.
209
+ - `validateCollection`은 빈 section·item ID와 중복 ID, 빈 label/textValue, section accessible name을
210
+ renderer가 열리기 전에 거부합니다. `flattenCollectionItems`와 `resolveCollectionItem`은 Web
211
+ popup과 Native sheet가 같은 key namespace를 소비하게 합니다.
212
+ - `getCollectionNavigationTarget`과 `getCollectionTypeaheadMatch`는 disabled skip, wrap,
213
+ section 간 탐색 순서를 공통화합니다. typeahead는 NFC 정규화와 locale-aware base-sensitivity
214
+ 비교를 사용하고, 문자열 buffer와 timeout은 renderer가 소유합니다.
215
+ - collection 변경 뒤에는 `reconcileSelectSelection`으로 사라진 key를 `null` 또는 첫 enabled
216
+ key로 정리합니다. 아직 존재하지만 disabled가 된 현재 선택은 보존합니다.
217
+ - `Combobox`는 선택 key와 입력 문자열을 서로 다른 controlled 축으로 둡니다. 검색어와 선택값을
218
+ 하나의 string prop으로 합치지 않습니다.
219
+ - `filtering="local"`은 renderer가 `textValue`로 필터링하고, `external`은 제품이 결과와
220
+ `loading/loadingMore/empty/error` 상태, 서버에 실제 요청한 canonical `queryValue`, 그 결과를
221
+ 만든 `resultQuery`를 공급합니다. 사용자가 편집 중인 raw `inputValue`는 IME 조합과 공백을
222
+ 보존하며 stale 판정에 직접 쓰지 않습니다.
223
+ - external 결과 목록은 committed value의 저장소가 아닙니다. 현재 결과가 선택 항목을 포함하지
224
+ 않으면 제품이 stable key가 같은 `selectedItem` snapshot을 공급하고
225
+ `resolveComboboxSelectedItem`이 일치 여부를 검증합니다. 비동기 Select도 같은 이유로
226
+ `selectedItem`을 받을 수 있으며, loading/loadingMore/error 동안에는
227
+ `reconcileSelectSelection`이 committed key를 지우지 않고
228
+ `resolveSelectSelectedItem`이 표시 copy를 보존합니다.
229
+ - Web popup과 Native sheet는 외형·dismiss 방식이 달라도 선택 결과, 오류 발표, disabled skip,
230
+ stable key reconciliation은 같아야 합니다.
231
+ - 임의 값 생성은 실제 제품 요구와 별도의 discriminated policy가 생기기 전까지 공개하지
232
+ 않습니다. Select와 기본 Combobox는 기존 option 선택과 clear만 commit합니다.
233
+
234
+ `selectRecipe`와 `comboboxRecipe`는 같은 field frame, floating surface, collection item grammar를
235
+ 조합합니다. Web/RN 구현이 픽셀 parity를 만들 필요는 없지만 44-unit target, visible focus,
236
+ selected indicator, invalid border, support copy, loading/empty/error announcement는 같아야 합니다.
237
+ 기본 키보드 탐색은 collection 경계에서 멈추며(`loop: false`), 순환은 제품이 명시적으로
238
+ 선택할 때만 켭니다. behavior registry의 `controlled`에는 state triplet만 두고,
239
+ 서버 결과 같은 read-only 값은 `inputs`, 후속 side effect 알림은 `events`로 구분합니다.
240
+
241
+ ## Toast queue와 announcement 경계
242
+
243
+ Toast는 controlled `open` 하나가 아니라 앱 root의 bounded FIFO입니다. 제품은 plain-text
244
+ `ToastDescriptor`와 stable id를 publish하고, `createToastSession`이 queued/visible/closing/closed,
245
+ timer pause, action과 dismiss의 exact-once를 소유하며 `createToastStore`가 visible slot과 pending
246
+ promotion을 소유합니다. 실제 clock, animation, live region과 native announcement는 renderer에
247
+ 남깁니다.
248
+
249
+ queued 시간에는 timer가 시작되지 않습니다. visible이 된 뒤에만 renderer가 경과 시간을
250
+ 전달하며 pointer, focus, window/app background와 gesture pause가 모두 풀린 경우에만 흐릅니다.
251
+ 자동 닫힘은 최소 5000ms이고 action이 있으면 기본 persistent입니다. visual exit가 끝난 뒤
252
+ `completeExit`을 호출해야 다음 FIFO 항목이 올라오고 `onDismiss`가 정확히 한 번 정산됩니다.
253
+ Reduce Motion의 즉시 exit도 같은 완료 경계를 건너뜁니다.
254
+
255
+ tone과 announcement priority는 독립입니다. tone은 HJM recipe의 색과 서로 다른 non-color mark를
256
+ 고르고, `normal | high` priority는 Web polite/assertive 또는 Native announcement scheduling으로
257
+ 번역합니다. 색이 위험하다는 이유만으로 사용자의 현재 screen reader 발화를 끊지 않습니다.
258
+ 상세 descriptor, dedupe/update, overflow와 renderer acceptance는 [`toast.md`](./toast.md)를 따릅니다.
259
+
260
+ ## Monorepo 패키지
261
+
262
+ ```text
263
+ @hjmds/design-contracts 토큰·recipe·catalog
264
+ @hjmds/react DOM·ARIA·focus·keyboard renderer
265
+ @hjmds/react-native Pressable·Modal·safe-area renderer
266
+ @hjm/icons 같은 의미의 Web/RN icon adapter (planned)
267
+ @hjm/testing contract·접근성·fixture parity 도구 (planned)
268
+ ```
269
+
270
+ 세 현재 패키지는 `packages/design-contracts`, `packages/react`, `packages/react-native`에서
271
+ 같은 `0.6.x` fixed version train으로 관리합니다. renderer별 component status는 독립적이며,
272
+ 코드가 존재한다는 이유만으로 승격하지 않습니다. `beta`는 public renderer export와 package CI가
273
+ 실제로 실행하는 canonical `default` render proof를 모두 요구하고, 아직 검증하지 않은 환경 축은
274
+ generated debt로 공개합니다. `stable`은 Web browser 또는 RN device/AT 검증을 포함한 required
275
+ scenario 전체와 consumer adoption evidence까지 통과한 surface에만 허용합니다.
276
+
277
+ ## 적용 순서
278
+
279
+ 1. HJM theme를 모든 앱의 유일한 시각 원천으로 고정
280
+ 2. Button, Field, Surface의 Web/RN parity 복구
281
+ 3. Badge, CounterBadge, SearchField, ListRow, SegmentedControl, Switch, Notice, Skeleton을 공용 recipe로 승격
282
+ 4. BurnTok Web/RN의 테마 선택·검색·알림·공유 흐름에 적용
283
+ 5. Yajalal 설정·전체 목록·홈 화면에 적용
284
+ 6. Dialog, Sheet, Toast 같은 behavior-heavy renderer 안정화
285
+ 7. Select, Menu, DatePicker처럼 플랫폼 적응형 입력 확장
286
+ 8. DataTable, Tree, CommandPalette를 Web-first experimental로 검증
287
+
288
+ 세부 배치와 외부 시스템에서 흡수하는 원칙은 `docs/expansion-roadmap.md`를 따릅니다.
289
+
290
+ ## 출시 규칙
291
+
292
+ - public token/recipe 변경에는 타입 검사, 계약 테스트, light/dark 대비 검증을 포함합니다.
293
+ - beta → stable 승격에는 Web/RN fixture, 키보드 또는 screen reader 검증, Reduce Motion 확인이
294
+ 필요합니다.
295
+ - 소비 앱은 정확한 SemVer Git tag를 고정하며 `main`이나 로컬 경로를 커밋하지 않습니다.
296
+ - 한 태그 안의 package path를 함께 고정하고 세 package version을 항상 동일하게 유지합니다.
297
+ - 제거는 deprecation 기간과 migration note를 거칩니다.
298
+ - 원시 팔레트나 제3자 라이브러리 이름을 public component prop으로 노출하지 않습니다.
299
+
300
+ ## 향후 토큰 교환
301
+
302
+ Figma와 코드 생성이 필요해지면 [Design Tokens Community Group 형식](https://www.w3.org/community/design-tokens/)
303
+ 으로 foundations와 semantic roles를 직렬화합니다. JSON 파일을 당장 source of truth로
304
+ 도입하기보다 현재 TypeScript 계약과 동등성 테스트를 먼저 만든 뒤 전환합니다.
305
+
306
+ ## 오버레이 어휘 규칙
307
+
308
+ 병렬 저작에서 같은 개념이 다른 이름을 갖는 일이 실제로 일어났다(`docs/consistency-audit.md`).
309
+ 다음 둘은 이제 규칙이다.
310
+
311
+ - **여는 사유는 `"trigger"`다.** `"trigger-activation"`이라 부르지 않는다. 그 문자열은
312
+ `src/tooltip.ts`에서 **닫는** 사유(열려 있는 툴팁의 트리거를 눌러 닫는다)로 이미 쓰인다.
313
+ 한 문자열이 여는 뜻과 닫는 뜻을 겸하면 렌더러가 조용히 반대로 처리한다 — 두 값 모두
314
+ 유효한 문자열이라 컴파일러가 잡지 않는다.
315
+ - **바깥 닫힘은 모달이면 `"outside"` 하나, 비모달이면 `"outside-pointer"`와
316
+ `"outside-focus"`로 나눈다.** 모달은 바깥이 불활성이라 입력 양식을 구분할 필요가 없지만,
317
+ 비모달은 Tab으로 초점이 빠져나가는 것이 포인터 클릭과 다른 사건이다.
318
+
319
+ 공용 `BehaviorContract.dismiss`가 각 모듈의 사유보다 거친 것은 버그가 아니다. 레지스트리는
320
+ 렌더러 테스트가 읽는 **요약**이고, 정밀한 계약은 모듈 자신의 타입이 갖는다.