@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,90 @@
1
+ # Consumer release gate
2
+
3
+ HJM의 canonical `v<version>` tag는 private consumer인 BurnTok Web, BurnTok Native,
4
+ Yajalal Native의 Storybook inventory가 같은 canonical release commit의 generated manifest와 맞는 경우에만
5
+ 생성됩니다. canonical repository는 public이고 두 consumer는 private이므로, public caller가
6
+ private reusable workflow를 직접 호출할 수 없습니다. 자동 gate는 canonical에만 보관한 단일
7
+ fine-grained token으로 두 consumer에 `repository_dispatch`를 보내고 결과 evidence를 검증합니다.
8
+
9
+ 릴리스 target의 식별자는 repository 하나가 아니라 **`repository + surface` tuple**입니다.
10
+ 현재 matrix는 `burntok-web`, `burntok-native`, `yajalal-native` 세 개입니다. Yajalal에는 현재
11
+ 적용 범위인 Web 앱/renderer evidence가 없으므로 `yajalal-web`을 만들어 성공을 가장하지 않습니다.
12
+
13
+ | target | repository / branch | surface | artifact prefix | evidence binding |
14
+ | --- | --- | --- | --- | --- |
15
+ | `burntok-web` | `jim1286/BurnTok` / `main` | `web` | `hjm-consumer-evidence-burntok-` | `hjm-evidence.json`의 `canonicalRelease` |
16
+ | `burntok-native` | `jim1286/BurnTok` / `main` | `native` | `hjm-consumer-evidence-burntok-native-` | `native-storybook.json` + `dispatch.json`의 `releaseCandidate` |
17
+ | `yajalal-native` | `jim1286/yajalal` / `main` | `native` | `hjm-consumer-evidence-yajalal-` | `native-storybook.json` + `dispatch.json`의 `releaseCandidate` |
18
+
19
+ 두 consumer workflow의 `run-name`은 기존
20
+ `HJM <version> · <correlation_id>`를 유지합니다. canonical이 correlation ID 끝에 target ID를
21
+ 붙이므로 run-name/concurrency를 별도로 늘리지 않아도 surface run이 서로 구분됩니다. workflow는
22
+ `github.event.client_payload.surface`를 읽고 허용된 surface만 실행해야 합니다. Native
23
+ `dispatch.json`과 `inventory.releaseCandidate`에는 payload의 `surface`를 그대로 기록합니다.
24
+
25
+ ## 실행 순서
26
+
27
+ 1. authored Changeset을 포함한 source commit을 검증한 뒤 로컬에서 `pnpm release:version`을
28
+ 실행합니다. 생성된 fixed-package version, changelog, dist, generated docs를 하나의 release
29
+ commit으로 `main`에 push합니다.
30
+ 2. `main` push마다 `Release Packages` workflow가 실행되고, `release-candidate` step이 package
31
+ version을 유일한 trigger로 씁니다. 현재 `v<version>` tag가 이미 있거나 authored Changeset이
32
+ 남아 있으면 `should-release=false`로 두어 나머지 step을 모두 건너뜁니다. tag가 없고 남은
33
+ Changeset도 없는 push에서만 릴리스를 진행합니다. 이때 `pnpm release:commit:check HEAD^`가
34
+ push된 commit이 `pnpm release:version` 생성물과 정확히 같은 shape인지(소비된 Changeset,
35
+ 허용된 path, 세 manifest의 lockstep bump, authored bump type과 일치하는 version, 동기화된
36
+ source version 상수) 먼저 검증하고, 그 다음 package, renderer, Storybook, committed artifact를
37
+ 검증합니다. 실패한 release를 다시 시도할 때는 같은 gate를 그대로 통과하는 `workflow_dispatch`
38
+ 수동 실행을 씁니다.
39
+ 3. `scripts/check-consumer-release.mjs`는 설정된 default branch 이름을 확인하고, 두 repository의
40
+ 현재 HEAD를 full SHA로 각각 한 번 캡처한 뒤 그 SHA의 workflow invariant를 검사합니다.
41
+ BurnTok Web/Native tuple은 같은 캡처 SHA를 공유합니다.
42
+ 4. script는 `{ repository, release_sha, version, correlation_id, consumer_ref, surface }` payload로
43
+ 세 `hjm-release-candidate` event를 보냅니다. 같은 repository의 동시 run이 섞이지 않도록
44
+ 공통 correlation ID에 target ID(`burntok-web` 등)를 붙인 고유 correlation ID를 사용합니다.
45
+ consumer workflow는 `surface`를 검증하고 해당 surface job/evidence만 실행하며, 자체 checkout을
46
+ `consumer_ref`에, public canonical checkout을 `release_sha`에 고정하고 실제 HEAD를 다시
47
+ 비교합니다.
48
+ 5. canonical은 workflow 파일, event, evaluated run name, 생성 시각, 캡처한 consumer
49
+ `head_sha`가 모두 일치하는 단 하나의 run만 추적합니다. 캡처 뒤 default branch가 이동해
50
+ 다른 SHA에서 실행된 run이나 과거 성공 run은 재사용하지 않고 즉시 실패합니다.
51
+ 6. 세 run이 모두 `success`여야 하며, 각 run에서 다음 고유 artifact가 정확히 하나 생성되어야
52
+ 합니다.
53
+ - `hjm-consumer-evidence-burntok-<burntok-web-correlation-id>`
54
+ - `hjm-consumer-evidence-burntok-native-<burntok-native-correlation-id>`
55
+ - `hjm-consumer-evidence-yajalal-<yajalal-native-correlation-id>`
56
+ 7. canonical은 artifact ZIP을 다운로드해 evidence JSON, generated manifest, Native dispatch
57
+ record 안의 repository, surface, canonical release SHA, consumer SHA, version, correlation ID를
58
+ 다시 exact-join합니다. evidence의 `source.id`도 target ID와 같아야 하고 `source.surface` 및
59
+ inventory projection도 target surface와 같아야 합니다. artifact가 비었거나 만료됐거나 내부
60
+ 값이 다르면 실패합니다.
61
+ 8. 위 검증이 모두 끝난 뒤에만 같은 job의 tag step이 실행되어 현재 HEAD에 `v<version>`을
62
+ 생성합니다. token 누락, dispatch 실패, timeout, cancelled/failure run, artifact 누락 또는
63
+ payload 불일치는 모두 tag 생성을 막습니다.
64
+
65
+ ## Secret과 최소 권한
66
+
67
+ canonical `hjm-design-system` repository에 Actions secret 하나만 둡니다.
68
+
69
+ - 이름: `HJM_CONSUMER_SYNC_TOKEN`
70
+ - 종류: fine-grained personal access token 또는 동등한 GitHub App token
71
+ - repository access: `jim1286/BurnTok`, `jim1286/yajalal`만 선택
72
+ - repository permissions:
73
+ - `Contents: Read and write` — `repository_dispatch` 생성
74
+ - `Actions: Read-only` — workflow run과 evidence artifact 조회·다운로드
75
+
76
+ consumer repository에는 `HJM_CANONICAL_READ_TOKEN`이 필요하지 않습니다. canonical은 public이라
77
+ consumer의 기본 `GITHUB_TOKEN`으로 full SHA를 읽을 수 있고, consumer 자체 private checkout은
78
+ 각 consumer run의 기본 token으로 수행합니다. broad classic PAT를 복제하거나 소비 앱에 canonical
79
+ token을 저장하지 않습니다.
80
+
81
+ ## 이 gate가 증명하는 것과 증명하지 않는 것
82
+
83
+ 이 gate는 canonical release SHA의 active ID가 실제 exported CSF story에 빠짐없이 연결되고,
84
+ BurnTok Web에서는 built Storybook index에, BurnTok Native와 Yajalal Native에서는 generated
85
+ Native registration과 Storybook CSF index 해석 결과에 결합됐음을 surface별로 증명합니다.
86
+ known planned ID는 문서 registration으로만 남고 active evidence에는 포함되지 않습니다.
87
+
88
+ inventory 일치만으로 dark/RTL/large-text/accessibility scenario가 실행됐다고 주장하지 않습니다.
89
+ tag 생성과 publish가 끝난 뒤 consumer dependency를 그 version range로 올리고 각 앱의 CI/device gate를
90
+ 통과시키는 작업도 별도 adoption 단계입니다.
@@ -0,0 +1,156 @@
1
+ # ContentState scope axis
2
+
3
+ **문제.** 로딩·빈 화면·오류는 화면 전체를 대신할 수도, 화면의 한 구역만 대신할 수도
4
+ 있다. 야잘알은 이 둘을 위해 이미 서로 다른 컴포넌트를 쓰고 있다 —
5
+ `AppStateView`(화면 전체, 유일한 출구, 채움 버튼)와 `AppStateRegion`(한 구역, 선택적
6
+ 복구, ghost + 브랜드 텍스트)(`modules/app-rn/src/components/ui/AppStateView.tsx`,
7
+ `AppStateRegion.tsx`). 이 구분은 `DESIGN_SYSTEM.md` §6 「오류 복구 행동」이 이미
8
+ 문장으로 규정해 뒀고, UI 검수(`review.md`)에서 이 축이 없어서 생긴 결함이 확산 1위
9
+ (상태가 화면 크롬을 삼킨다, 6화면+라우트 7개)와 7위(구역 실패를 화면 전체 컴포넌트로
10
+ 그린다, 3화면)를 차지했다. 그런데 이 축은 `Result`의 `status`(success/failure/info,
11
+ `docs/result.md`가 이미 확인)에도, `EmptyState`의 시각 recipe에도 없다 — 어느 계약도
12
+ "이 실패가 화면 전체를 막는가, 구역만 막는가"를 모른다.
13
+
14
+ ## 판정 — 새 컴포넌트도 아니고 `EmptyState`/`Result`의 축도 아니다
15
+
16
+ 세 후보를 확인했다.
17
+
18
+ 1. **`EmptyState`에 붙이는 안.** 기각. `emptyStateRecipe`(`src/component-recipes.ts`)는
19
+ 지금 icon/title/description/action의 **시각 토큰만** 있고 descriptor·validator·
20
+ content 상태 자체가 없다(`loading`을 표현할 수 없다 — 실제 제품은 loading을
21
+ `AppSkeleton`으로 완전히 다른 모양으로 그린다). scope 축은 loading에도 적용되는
22
+ 축이라 EmptyState 하나로는 담기지 않는다.
23
+ 2. **`Result`에 붙이는 안.** 기각. `docs/result.md`가 이미 확인한 대로 Result가 푸는
24
+ 문제(사용자 행동 뒤의 flow terminus — 결제 성공/실패)는 이 축이 관측된 화면
25
+ (데이터 로드 실패)과 다르다. `status`(success/failure/info)와 `scope`(screen/region)는
26
+ 서로 다른 실물 질문이라 하나의 필드로 묶으면 Result가 갖지 않는 질문(로딩 중엔
27
+ 무엇을 보여줄지)까지 떠안는다.
28
+ 3. **독립된 공용 축 + 작은 계약.** 채택. `docs/expansion-roadmap.md`의 「공통 상태 축」
29
+ 표(interaction/availability/value/validation/content)에 들어갈 만큼 여러 컴포넌트가
30
+ 참조할 개념이지만, 그 표의 다른 축과 성격이 다르다 — 그것들은 **한 인스턴스가
31
+ 시간에 따라 오가는 상태**(hover→pressed, idle→loading)인데 `scope`는 **저작 시점에
32
+ 한 번 정해지는 배치 결정**이다(화면을 만들 때 "이 실패가 전체냐 구역이냐"를 정하지,
33
+ 렌더링 도중 전체와 구역을 오가지 않는다). 그래서 `content` 축의 **직교 차원**으로
34
+ 문서에는 나란히 두되, 별도 모듈(`src/content-state.ts`)로 분리했다 — `EmptyState`가
35
+ 나중에 loading을 포함하는 완전한 descriptor를 갖추면 이 모듈의 타입을 `import`해서
36
+ 쓸 자리다.
37
+
38
+ `content` 축과의 직교 확인: `ContentStateStatus`는 공통 축의 `loading|empty|error`만
39
+ 쓴다(`idle`·`success`는 블록 자체를 그리지 않고, `loadingMore`·`complete`는 `LoadMore`
40
+ footer 전용이라 이 계약 밖이다 — footer는 정의상 항상 region이므로 그 축이 필요 없다).
41
+ 세 상태 모두 screen과 region 양쪽에서 실제로 관측된다(로딩 스켈레톤은 전체 화면일 수도
42
+ `MatchDetailSectionState`처럼 구역일 수도 있다, 빈 화면도 마찬가지, 오류도 마찬가지) —
43
+ 그래서 새 축이 맞다.
44
+
45
+ ## 일반화한 계약
46
+
47
+ ```ts
48
+ const regionError = {
49
+ status: "error",
50
+ scope: "region",
51
+ title: "불러오지 못했어요",
52
+ description: "기본 경기 정보는 그대로 볼 수 있어요.",
53
+ action: { label: "다시 불러오기", onAction: retry },
54
+ } satisfies ContentStateDescriptor;
55
+ ```
56
+
57
+ - `scope: "screen" | "region"`. `status: "loading" | "empty" | "error"`(공통 `content`
58
+ 축의 부분집합).
59
+ - `loading`은 `title`/`description`/`action`이 **없다** — `loadingLabel`만 필수다.
60
+ 실제 컴포넌트도 로딩을 스켈레톤만으로 그린다.
61
+ - `action`은 최대 하나다(`Result`의 primary+secondary 두 개짜리 계약과 다르다 — 이
62
+ 문제의 실물에는 복구 행동이 항상 하나였다).
63
+ - `resolveContentStateActionEmphasis(scope)`가 `"sole" | "optional"`을 반환한다.
64
+ `screen`은 콘텐츠 층의 유일한 행동이므로 강조, `region`은 선택지 중 하나이므로
65
+ 비강조다. Button tone 이름에 직접 묶지 않았다 — HJM의 현재 `ghost` tone
66
+ (`src/recipes.ts`)은 `content: "textMuted"`로 해석되는데, 이 저장소의 실제 region
67
+ 복구 버튼은 **브랜드 텍스트**다(`AppButton tone="link"`,
68
+ `modules/app-rn/src/features/match-detail/SectionState.tsx:30`). `ghost`가 아니라
69
+ 브랜드 틴트 저강조 tone이 필요하다는 뜻이라, Button의 tone vocabulary가 그 tone을
70
+ 갖추기 전까지는 제품이 로컬로 매핑한다.
71
+
72
+ ## HJM 기본값 — 접근성이 이 계약의 핵심이다
73
+
74
+ `scope`가 실제로 강제하는 것은 버튼 두께보다 **접근성 발표 범위**다. 구역 실패를
75
+ 낭독할 때 사용자가 화면 전체가 실패했다고 오해하면 안 된다는 요구를
76
+ `resolveContentStateAnnouncement(status, scope)`로 코드화했다.
77
+
78
+ | status | scope | web role/live | native live/role | focus 이동 |
79
+ |---|---|---|---|---|
80
+ | loading | 무관 | status/polite | polite/progressbar | 안 함 |
81
+ | empty | 무관 | status/polite | polite/text | **안 함** (screen이어도) |
82
+ | error | screen | alert/assertive | assertive/alert | **함** |
83
+ | error | region | alert/assertive | assertive/alert | **안 함** |
84
+
85
+ 세 가지가 이 표에서 의도적으로 대칭이 아니다.
86
+
87
+ - **empty는 scope와 무관하게 focus를 옮기지 않는다.** §8.4 "빈 상태는 결핍이 아니라
88
+ 초대다"— 화면 전체가 비어도 침묵을 깨고 끼어들 정도로 급하지 않다. `moveFocus =
89
+ scope === "screen"`을 모든 status에 일괄 적용하면 이 규칙을 놓친다 — 그래서
90
+ `resolveContentStateAnnouncement`는 `empty` 분기에서 scope를 아예 참조하지 않는다.
91
+ 테스트가 이 함정(모든 status에 같은 공식을 재사용하는 것)을 직접 겨눈다.
92
+ - **error만 scope로 focus 이동이 갈린다.** screen은 지킬 다른 초점이 없으니 블록으로
93
+ 이동해도 안전하지만, region은 사용자가 화면의 다른 곳에서 하던 일이 있을 수 있어
94
+ 강제로 끌어오면 "화면 전체가 죽었다"는 오해를 만든다 — 이게 이 계약이 푸는 핵심
95
+ 문제다.
96
+ - **live/role 자체(assertive/alert)는 error에서 scope와 무관하게 항상 켠다.** 발표
97
+ **강도**는 실패의 심각성(오류)에서 오지 화면 범위에서 오지 않는다. scope가 바꾸는
98
+ 것은 발표가 사용자의 **초점**까지 끌고 오느냐일 뿐이다.
99
+
100
+ ## 이 축이 아닌 것 — 「상태가 화면 크롬을 삼킨다」와의 경계
101
+
102
+ UI 검수 표에서 확산 1위(§9, 6화면+라우트 7개)와 7위(§6, 3화면)는 언뜻 같은 결함처럼
103
+ 보이지만 서로 다르다.
104
+
105
+ - **7위(구역→화면 오분류)**는 `scope` **값 선택**이 틀렸다 — 구역 실패인데 `screen`
106
+ 컴포넌트를 골라 실패하지 않은 나머지 콘텐츠까지 덮어썼다. 이 계약의 문서와
107
+ validator가 두 값의 의미 차이를 명확히 해 막는 종류의 실수다.
108
+ - **1위(크롬 삼킴)**는 `scope: "screen"`을 **올바르게 골랐어도** 나는 실수다 —
109
+ 컴포넌트를 화면 크롬(`AppScaffold`/`AppScreen`) **밖**에 두면 상단바·뒤로가기·탭
110
+ 바까지 함께 사라진다. 이건 값의 문제가 아니라 **배치**의 문제다. 즉 `scope:
111
+ "screen"`의 진짜 의미는 "화면 전체"가 아니라 **"영구 크롬 안의 콘텐츠 영역
112
+ 전체"**다 — 크롬은 어떤 scope 값도 건드리지 않는다. 이 계약은 JSX 트리를 모르므로
113
+ (런타임 의존성 금지) 이 불변식을 **강제할 수 없다** — 타입도 validator도 "이
114
+ 컴포넌트가 실제로 크롬의 자손인가"를 알 방법이 없다. 그래서 이 계약은 이 요구사항을
115
+ **문서화된 렌더러 의무**로만 남기고, 실제 강제는 제품이 구조적 가드로 해야 한다.
116
+ 야잘알의 `modules/app-rn/src/screen-chrome-boundary.test.ts`(JSX 조상을 정적으로
117
+ 세는 vitest)가 정확히 이 역할이고, 참조 구현은
118
+ `modules/app-rn/src/features/home/HomeScreen.tsx:73-113`(`AppStateView`가
119
+ `AppScaffold`의 자손)이다. 이 계약을 다음에 다른 제품에 적용하는 저작자는 같은
120
+ 구조적 가드를 그 제품 레이어에 새로 만들어야 한다 — 패키지가 대신해 줄 수 없다.
121
+
122
+ **결론**: 두 결함은 같은 축(scope) 위에 있지만 방향이 다르다. 하나는 **값 선택
123
+ 오류**, 하나는 **배치 불변식 위반**이다. 이 모듈은 전자를 명세하고, 후자는 명세할 수
124
+ 없다는 사실과 그 대신 필요한 장치를 문서화한다.
125
+
126
+ ## 플랫폼 번역
127
+
128
+ Web/RN 모두 아이콘(있으면)+제목+설명+action 세로 배치는 `EmptyState`의 시각 grammar를
129
+ 그대로 따른다 — 이 모듈은 그 grammar를 다시 정의하지 않는다. 이 모듈이 추가하는 것은:
130
+
131
+ - Web: `role`/`aria-live`를 상태 블록 컨테이너 자신에만 건다. region은 그 블록이 속한
132
+ landmark(예: `main`)의 형제로 존재해야 하고, 별도 landmark를 새로 열지 않는다 —
133
+ 그래야 보조기기가 "페이지의 일부가 바뀌었다"로 읽지 "새 페이지"로 읽지 않는다.
134
+ - Native: `accessibilityLiveRegion`/`accessibilityRole`은 블록 컨테이너에, focus 이동은
135
+ `moveAccessibilityFocus`가 참일 때만 렌더러가 명시적으로 수행한다(예:
136
+ `AccessibilityInfo.setAccessibilityFocus`) — 기본값은 발표만 하고 focus는 그대로
137
+ 둔다.
138
+
139
+ ## 검증 화면
140
+
141
+ - `modules/app-rn/src/features/home/HomeScreen.tsx:73-113` — `AppStateView`가
142
+ `AppScaffold` 자손. `scope: "screen"`이 크롬 불변식을 지킨 참조 구현.
143
+ - `modules/app-rn/src/features/match-detail/SectionState.tsx:20-56` — `status ===
144
+ 'error'`일 때 `AppNotice announcement="assertive"` + `AppButton tone="link"`,
145
+ 나머지는 `AppStateRegion`(`compact`) 기본 복구 tone(`link`)에 위임. `scope:
146
+ "region"`의 실물 — 저강조 action이 `ghost`가 아니라 브랜드 텍스트임을 실제로 보여
147
+ 준다.
148
+ - `modules/app-rn/src/screen-chrome-boundary.test.ts:61-74`
149
+ (`CHROME_ESCAPE_BASELINE`) — 크롬 불변식이 아직 안 지켜지는 파일 11개 21건의 실측
150
+ 목록. `scope: "screen"`을 선택한 화면 중 몇이 아직 배치 불변식을 어기는지 보여주는
151
+ 현재 상태.
152
+
153
+ `planned → beta` 승격에는 이 모듈을 실제로 소비하는 renderer(예: `EmptyState`가 이
154
+ 타입을 받아들이는 시점, 또는 `AppStateView`/`AppStateRegion`이 이 계약으로 재작성되는
155
+ 시점)의 vertical slice가 필요하다. 지금은 계약만 있고 어떤 renderer도 아직 이 모듈을
156
+ import하지 않는다.
@@ -0,0 +1,88 @@
1
+ # ContextPanel — 새 컴포넌트를 만들지 않는다
2
+
3
+ ## 이게 무엇인지부터 정해야 했다
4
+
5
+ `docs/expansion-roadmap.md`의 「Batch 2 — 입력과 탐색」은 `adaptive: Select, Combobox,
6
+ ContextPanel, BottomNavigation`이라고만 적어 두었을 뿐 문제 정의가 없다. crosswalk
7
+ (`src/component-references.ts`)에도 `ContextPanel`을 가리키는 antd source entry가
8
+ **하나도 없다** — `TimePicker`(`targets: ["TimePicker"]`)나 `Drawer`(`targets: ["Sheet",
9
+ "SidePanel"]`)처럼 어딘가 대응이 있는 다른 planned 항목과 다르다. 이름과 카탈로그의
10
+ `category: "overlay"`만으로 판정해야 했다.
11
+
12
+ 이름("문맥 패널")과 자리(overlay, adaptive)로 가장 가능성 높은 해석은 하나다: **선택한
13
+ 대상의 상세를 본문 옆에서 보여주는 면 — Web은 사이드 영역, Native는 Sheet.** 이는 antd
14
+ `Drawer`가 실제로 풀던 문제이기도 하다.
15
+
16
+ ## 판정: Drawer 분해가 이미 이 문제를 끝냈다
17
+
18
+ `docs/ant-design-coverage.md`와 `src/component-references.ts:114`는 이미 `Drawer`를
19
+ `Sheet`(adaptive)와 `SidePanel`(web)로 **decomposed** 처리해 뒀다. 그리고 방금 다른
20
+ 저작자가 완성한 `SidePanel`(`src/side-panel.ts`, `docs/side-panel.md`)을 읽어보면, 이
21
+ 컴포넌트가 이미 "ContextPanel"이 말하려던 것 그 자체다:
22
+
23
+ - `modal: false` 축이 **정확히** "본문을 밀어내며 옆에서 보여주되 나머지 페이지는
24
+ 계속 상호작용 가능한, 상세를 보여주는 비-차단 패널"이다 — 로드맵이 말하려던
25
+ "문맥"의 의미가 이미 여기 있다.
26
+ - `edge: "start" | "end"`로 어느 쪽에 도킹하는지도 이미 계약돼 있다.
27
+
28
+ Native에서 같은 사용자 의도("선택한 대상의 상세를 본다")를 만들 표면은 이미 `Sheet`
29
+ (`platform: "adaptive"`, `status: "beta"`)다 — Native에는 상시 옆 패널을 놓을 화면
30
+ 폭이 없으므로 모달 Sheet로 여는 것이 Select/DatePicker가 이미 채택한 것과 같은 적응
31
+ 경로다.
32
+
33
+ 즉 "ContextPanel"이 하려던 일 — **Web에서는 SidePanel(필요하면 `modal:false`로
34
+ 비차단), Native에서는 Sheet** — 은 새 컴포넌트가 아니라 **이미 완결된 Drawer 분해를
35
+ 어떤 제품이 어떻게 조합해서 쓰느냐**의 문제다. 여기에 `ContextPanel`이라는 셋째 이름을
36
+ 얹으면:
37
+
38
+ - `sidePanelRecipe`의 `edge`/`modal`/`content` anatomy와 거의 같은 것을 다시
39
+ 선언하게 되고,
40
+ - `sheetBehaviorDefaults`의 dismiss 어휘와 거의 같은 것을 Native 쪽에 다시
41
+ 계약하게 되어,
42
+
43
+ Dropdown이 Menu와 같은 실수를 반복할 뻔한 것과 같은 자리다(`docs/dropdown.md`) — 두
44
+ 컴포넌트가 같은 상태를 서로 다른 이름으로 소유하는 문제.
45
+
46
+ ## 대안 검토 — "선택과 결합된 패널"이라면?
47
+
48
+ `ContextPanel`이 단순한 표면이 아니라 "목록에서 항목을 고르면 자동으로 열리고, 패널을
49
+ 닫으면 선택이 풀리는" **결합된 상태**를 뜻할 가능성도 검토했다. 그렇다면 SidePanel/Sheet
50
+ 어느 쪽도 갖지 않는 축(선택 key ↔ 열림 상태의 양방향 동기화)이 새로 필요할 수 있다.
51
+ 하지만 이를 요구하는 목록형 컴포넌트(`DataTable`, `Tree`)가 아직 하나도 저작되지 않았고
52
+ (둘 다 `planned`, recipe 없음), 이 동기화가 실제로 어떤 모양이어야 하는지 뒷받침할
53
+ vertical slice가 없다. 지금 그 축을 짐작해서 만들면 `Select`의 `open`/`selectedKey`를
54
+ "섞지 않는다"는 이 저장소의 원칙(`docs/architecture.md`「Select와 Combobox의 적응형
55
+ 경계」)과 반대로, 검증되지 않은 결합을 먼저 만드는 것이 된다. 이 판정은 그 결합이
56
+ 필요한 실제 화면이 나오면 뒤집힐 수 있다 — 아래 참고.
57
+
58
+ ## 판정이 뒤집힐 조건
59
+
60
+ `DataTable` 또는 `Tree`(둘 다 Batch 3 planned)가 저작되고, 그 목록의 "선택 항목 →
61
+ 상세 패널" 흐름이 여러 제품에서 반복해서 같은 동기화 코드를 다시 쓰는 것이 확인되면,
62
+ 그때는 새 표면이 아니라 **SidePanel/Sheet를 감싸는 얇은 selection-synced open-state
63
+ 헬퍼**(새 recipe 없이 behavior 레벨의 작은 함수 하나)를 여는 쪽을 권한다 — Select의
64
+ `reconcileSelectSelection`이 표면 자체가 아니라 상태 유도 함수만 추가했던 것과 같은
65
+ 자리다.
66
+
67
+ ## 배선 명세 제안 (리드 적용)
68
+
69
+ Dropdown(`docs/dropdown.md`)과 달리 ContextPanel은 alias로 흡수할 단일 기존 이름이
70
+ 없다(SidePanel과 Sheet 둘로 나뉘므로). 두 가지 중 리드가 고른다.
71
+
72
+ **(a) 권고: catalog에서 제거.** `src/catalog.ts`의
73
+ `{ name: "ContextPanel", category: "overlay", platform: "adaptive", status: "planned" }`
74
+ 행을 지운다. `component-definitions.ts`의 예약 ID(`ContextPanel: "context-panel"`)는
75
+ 남겨 둬도 무해하다 — `componentCatalog`에 없으면 어차피 노출되지 않는다.
76
+ `docs/expansion-roadmap.md`의 「Batch 2」 목록에서 `ContextPanel`을 빼고, 대신 "선택
77
+ 항목 상세는 Web `SidePanel`(`modal:false` 옵션 포함) + Native `Sheet`로 이미 커버된다"는
78
+ 한 줄을 Drawer 분해 설명 옆에 남긴다.
79
+
80
+ **(b) 대안: catalog row 유지 + 이 문서만 링크.** 로드맵 문구를 지금 당장 고치기보다
81
+ Component Explorer에서 "ContextPanel"을 검색했을 때 이 문서로 안내되는 쪽이 낫다고
82
+ 판단하면 이 대안을 쓴다 — Notification이 catalog row를 유지한 채 `docs/notification.md`
83
+ 로 안내한 것과 같은 방식.
84
+
85
+ 이 저작자 판단은 (a)다 — Notification/Dropdown과 달리 ContextPanel은 대응할 단일
86
+ 기존 컴포넌트 이름이 없어 alias를 만들 수 없고, catalog에 recipe/behavior 없이 남아
87
+ 있는 `planned` 행 자체가 "아직 구현 안 된 진짜 계획"으로 오인될 여지가 있다 — 이미
88
+ Drawer 분해로 완결된 문제를 다시 계획 중인 것처럼 보이게 하는 쪽이 더 나쁘다고 봤다.
@@ -0,0 +1,118 @@
1
+ # Cross-platform core normalization
2
+
3
+ Text, Surface, Stack, Grid, Button, IconButton, Tag, Card의 공통 의미 축은 contracts가 소유하고 Web과
4
+ Native renderer는 각 플랫폼 host로 번역한다. 라이브러리 비교와 채택/기각 근거는
5
+ [library-reference-decisions.md](./library-reference-decisions.md)를 따른다.
6
+
7
+ ## Canonical API
8
+
9
+ | Component | 공통 축 | 기본값 | 공통 anatomy |
10
+ | --- | --- | --- | --- |
11
+ | Text | `variant`, `tone`, `emphasis`, `children` | `body`, `primary`, `regular` | `root` |
12
+ | Surface | `tone`, `bordered`, `padding`, `radius` | `default`, `false`, `none`, `lg` | `root` |
13
+ | Stack | `axis`, `gap`, `align`, `justify`, `wrap` | `block`, `md`, `stretch`, `start`, `false` | `root` |
14
+ | Grid | `columns`, `gap`, `minColumnWidth`, `children` | `gap: md`, `flow: row-major` | `root`, `item` |
15
+ | Button | `tone`, `size`, `loading`, `disabled`, `leading`, `trailing`, `children` | `primary`, `medium`, `false`, `false` | `root`, `leading`, `label`, `trailing`, `spinner` |
16
+ | IconButton | `label`, `tone`, `size`, `shape`, `loading`, `disabled`, `children` | `ghost`, `medium`, `rounded`, `false`, `false` | `root`, `icon`, `spinner` |
17
+ | Tag | `tone`, `children` | `neutral` | `root`, `label` |
18
+ | Card | `tone`, `selected`, `bordered`, `padding`, `title`, `description`, `media`, `children`, `actions` | `default`, `false`, `true`, `md` | `root`, `media`, `body`, `title`, `description`, `content`, `actions` |
19
+
20
+ 공통 타입과 값의 source of truth는 다음과 같다.
21
+
22
+ - `base-recipes.ts`: Button/Surface tone·size·geometry와 `surfaceDefaults`
23
+ - `component-recipes.ts`: Text/Stack axis와 defaults
24
+ - `grid.ts`: responsive columns/gap/minimum-width descriptor와 공통 layout resolver
25
+ - `icon-button-recipe.ts`: IconButton tone/size/shape, hit target, non-CSS color resolver
26
+ - `tag.ts`: Tag descriptor, recipe, non-CSS presentation resolver
27
+ - `card.ts`: Card slot anatomy와 defaults
28
+
29
+ ## 같은 의미, 다른 host
30
+
31
+ 플랫폼 host 차이를 가짜 공통 prop으로 숨기지 않는다.
32
+
33
+ | Web | Native | 이유 |
34
+ | --- | --- | --- |
35
+ | Text `as` | Text `align` | HTML element semantics와 Native logical alignment |
36
+ | Surface `as` | 숫자 `padding`/`radius` 허용 | HTML landmark 선택과 Native animation/layout interop |
37
+ | Grid `windowWidth` | Grid `onLayoutResolved`, `itemStyle` | DOM 측정/SSR override와 Native item wrapper 적용 |
38
+ | Button form props, DOM events | Pressable props, Native events | 각 플랫폼 activation 모델 |
39
+ | Card `headingLevel` 2–4 | title에 `accessibilityRole="header"` | Native에는 HTML heading level과 동등한 host primitive가 없음 |
40
+
41
+ `loading`과 `disabled`는 같은 상태가 아니다. 두 renderer 모두 loading 중 activation을
42
+ 막고 busy 상태를 보조 기술에 노출하지만, pending control의 focusability는 유지한다.
43
+ 명시적 `disabled`만 host의 disabled 상태가 된다.
44
+
45
+ IconButton은 보이는 텍스트가 없으므로 현지화된 `label`과 icon `children`을 두 renderer에서
46
+ 필수로 받는다. `size`는 36/44/52 visual diameter를 선택하고 small은 Web pseudo hit area와
47
+ Native `hitSlop=4`로 44 target을 만든다. tone 색은 renderer별 표가 아니라 공통 resolver에서
48
+ 결정한다.
49
+
50
+ Surface의 `accent` border도 동일하게 해석한다. `borderAlways`가 `false`이므로
51
+ `bordered=true`일 때만 primary 30% edge를 그리고, `subtle`은 contract가 요구하는
52
+ edge를 항상 그린다.
53
+
54
+ ## 0.5 compatibility aliases
55
+
56
+ 0.6에서 canonical API로 옮길 수 있도록 기존 Native 호출은 당분간 동작한다.
57
+
58
+ ```tsx
59
+ // before
60
+ <Stack direction="row" />
61
+ <Button label="저장" />
62
+ <Tag label="신규" />
63
+ <Surface tone="brand" />
64
+ <Grid descriptor={{ columns: { compact: 1, medium: 2 } }} />
65
+ <IconButton accessibilityLabel="닫기" icon={<CloseIcon />} tone="link" />
66
+
67
+ // canonical
68
+ <Stack axis="inline" />
69
+ <Button>저장</Button>
70
+ <Tag>신규</Tag>
71
+ <Surface tone="accent" />
72
+ <Grid columns={{ compact: 1, medium: 2 }} />
73
+ <IconButton label="닫기" tone="ghost"><CloseIcon /></IconButton>
74
+ ```
75
+
76
+ - Native `Stack.direction`: `row → inline`, `column → block` deprecated alias
77
+ - Native `Button.label`, `Tag.label`: `children` deprecated alias
78
+ - Native `Surface.sunken`, `Surface.brand`: 각각 `subtle`, `accent` deprecated alias
79
+ - Native `Grid.descriptor`: flat `columns`/`gap`/`minColumnWidth`를 위한 deprecated alias
80
+ - Native `IconButton.accessibilityLabel`, `icon`: `label`, `children` deprecated alias
81
+ - Native `IconButton tone="link"`: `ghost`로 번역되는 deprecated alias
82
+
83
+ 호환 alias는 새 문서와 예제에서 사용하지 않는다. 제거는 major release에서만 한다.
84
+
85
+ ## Reference application notes
86
+
87
+ 상위 판단 원칙은 [library-reference-decisions.md](./library-reference-decisions.md)에만 둔다.
88
+ 이번 API에 적용한 구체적 참고점은 다음과 같다.
89
+
90
+ - [MUI Grid](https://mui.com/material-ui/react-grid/)처럼 responsive columns와 spacing을
91
+ wrapper descriptor가 아닌 Grid의 직접 public axis로 둔다.
92
+ - [MUI IconButton API](https://mui.com/material-ui/api/icon-button/)의 size/content 분리와
93
+ [React Aria Button](https://react-aria.adobe.com/Button)의 pending focus 보존을 결합하되,
94
+ HJM tone/shape/default는 공통 recipe가 소유한다.
95
+ - Native host에는 [React Native accessibility](https://reactnative.dev/docs/accessibility)의
96
+ accessible name과 `accessibilityState.busy/disabled`를 번역한다.
97
+
98
+ ## Audited next migrations
99
+
100
+ Tabs와 Select는 단순 alias만 추가하면 두 상태 모델이 동시에 노출되므로 이번 변경에서 억지로
101
+ 합치지 않았다.
102
+
103
+ - Tabs: Web `items[{id,label,panel}]`와 Native `options[{value,label,panel?,badge?}]`를 공통
104
+ item key/content contract로 옮기고, `size`/`layout`/activation semantics를 동시에 맞춰야 한다.
105
+ - Select: Web collection의 nullable `selectedKey`, sections, open-change reason과 Native의
106
+ string `value`/`options` modal 모델 사이에 공통 collection/selection resolver가 먼저 필요하다.
107
+ canonical key callback을 도입한 뒤 `value`/`onValueChange`를 호환 alias로 유지한다.
108
+
109
+ ## Intentional remaining differences
110
+
111
+ - Web ref는 실제 DOM element이고 Native ref/event/style은 React Native host 계약을 따른다.
112
+ - Web Button은 submit/reset/form association을 제공하고 Native Button은 제공하지 않는다.
113
+ - Web Card는 실제 heading level을 선택할 수 있지만 Native는 header role까지만 보장한다.
114
+ - Native는 layout animation과 계산 결과를 적용하기 위해 숫자 spacing/radius를 확장 입력으로
115
+ 허용한다. 공통 코드에는 token 이름을 사용한다.
116
+
117
+ 이 차이는 parity 실패가 아니라 platform translation이다. tone, size, defaults, state,
118
+ slot anatomy가 달라지면 parity 실패로 간주한다.
@@ -0,0 +1,79 @@
1
+ # DataTable contract
2
+
3
+ **문제.** 사용자가 여러 행의 데이터를 표로 훑고, 필요하면 열 기준으로 정렬하거나
4
+ 행을 골라 배치 작업을 한다. antd `Table`은 이 배치에서 **가장 큰 표면**이다 —
5
+ 정렬·필터·페이징·선택·확장행·고정열·그룹헤더·요약행을 전부 갖는다. 이 모듈은 그
6
+ 표면을 옮기지 않는다. antd `Table` → HJM `DataTable`은 **adapted**
7
+ 관계다(`src/component-references.ts`) — decompose하지 않고 한 컴포넌트로 두되,
8
+ 문제를 HJM 의미로 다시 번역한다.
9
+
10
+ **일반화한 계약 — 그리고 계약이 아닌 것.**
11
+
12
+ - **계약인 것**: 열 정의(`id`·`header`·정렬 가능 여부·정렬 방향·align·width 힌트),
13
+ 행 식별(stable `id`, `disabled?`), 행 선택, 비동기 로딩 상태, 정렬 상태의
14
+ toggle 규칙.
15
+ - **계약이 아닌 것**: 실제 정렬 실행과 필터링. `Combobox`가 `filtering:
16
+ "local" | "external"`로 실행 방식을 나누고 검색 실행 자체는 항상 제품(로컬 배열이든
17
+ 서버 쿼리든)에 맡기는 것과 같은 경계다(`src/combobox.ts`). `getNextDataTableSortState`는
18
+ 버튼을 눌렀을 때 **다음 정렬 상태가 무엇이어야 하는지**만 판정하고, 실제 행을
19
+ 재배열하는 일은 하지 않는다.
20
+ - 행 선택은 `CollectionSelectionModel`(`src/behaviors.ts`)을 **그대로** 재사용한다 —
21
+ `none | single | multiple`, controlled/uncontrolled 쌍 전부 Select/Menu와 같다.
22
+ 새 선택 타입을 만들지 않는다.
23
+ - 로딩 상태는 `AsyncCollectionState`(`idle | loading | loadingMore | empty | error`,
24
+ 각 상태의 message)를 **그대로** 재사용한다.
25
+ - 반대로 **`CollectionItemDescriptor`는 재사용하지 않는다.** 그 타입의 `label`/
26
+ `textValue`는 "이 항목을 보이는 한 문장과 검색어로 대표할 수 있다"는 Menu/Select
27
+ 전제인데, 표 행은 여러 열의 값으로 이루어져 하나의 대표 문장이 없다. `DataTableRowDescriptor`는
28
+ `id`와 `disabled?`만 가진 훨씬 좁은 타입이다 — Collection 기본 계약 중 실제로
29
+ 행에 맞는 부분만 가져오고, 맞지 않는 부분(label/textValue/typeahead)은 그대로
30
+ 두었다.
31
+ - `Pagination`(다른 저작자가 이번 배치에서 계약)과 `LoadMore`(이미 beta)는 DataTable이
32
+ **소유하지 않고 합성**한다. `asyncState`가 `loadingMore`일 때 그 아래 어느
33
+ 컴포넌트를 두는지는 제품 선택이다 — Native 긴 목록에 LoadMore를 쓰듯 Web 표에도
34
+ 같은 패턴이 통한다.
35
+ - 정렬 값은 `"ascending" | "descending"`이며 `null`이 "정렬 없음"이다 — antd의
36
+ `"ascend"/"descend"` 축약형을 그대로 옮기지 않고 `aria-sort`의 실제 어휘를 썼다.
37
+ Web renderer가 번역표 없이 그대로 속성에 꽂는다(Slider의 `valueText` pass-through와
38
+ 같은 선택).
39
+ - 헤더 전체 선택 상태는 새 `"none"|"some"|"all"` enum을 만들지 않고 이미 있는
40
+ `CheckboxState`(`boolean | "mixed"`)를 그대로 반환한다 — 헤더 체크박스가 어차피
41
+ check/dash로 그리는 값과 같은 타입이라 recipe가 또 번역할 일이 없다.
42
+ - **행 확장(expandable row)은 여기서 소유하지 않는다.** antd Table의 확장행은 문제상
43
+ 이미 beta인 `Accordion`의 disclosure 문제와 겹친다 — 확장/축소 축, 트리거,
44
+ `aria-expanded`를 또 계약하면 Tag가 Chip의 `selected`와 별도 `closable` 축을 만들지
45
+ 않기로 한 것과 같은 실수가 된다(`docs/dropdown.md`가 Menu/Dropdown에서 이미 같은
46
+ 논리를 적용했다). 확장 가능한 상세 행이 필요한 제품은 `disclosureGroup` 행동
47
+ 계약을 행 단위로 합성한다.
48
+ - **넣지 않은 것(불필요/미측정)**: 열 고정(고정열)과 가로 스크롤 동기화는 렌더러
49
+ 기법이다. 열 리사이즈·드래그 재정렬·그룹 헤더(colgroup)·요약(합계) 행은 실제
50
+ 제품 요구가 측정되지 않았다. 각각 나중에 vertical slice가 나오면 이 문서를
51
+ 갱신하고 새 축을 연다.
52
+ - **roving-tabindex 그리드 키보드 탐색을 만들지 않는다.** 정렬 가능한 헤더는
53
+ 일반 버튼, 선택 셀은 일반 checkbox/radio native tab stop이다 — ARIA grid의 셀
54
+ 단위 방향키 이동은 측정된 요구가 없고 지금 범위보다 훨씬 큰 접근성 표면이라
55
+ 넣지 않는다.
56
+
57
+ **HJM 기본값.** 정렬 헤더 버튼과 선택 셀은 `control.minTouchTarget`(44) 이상을
58
+ 유지한다. 행 hover/selected 배경은 `collectionItemContract`의 `highlightedBackground`/
59
+ `selectedBackground`를 그대로 재사용해 Menu·Select·DataTable이 한 상호작용 색
60
+ 어휘를 공유한다 — 새 recipe가 새 hover 색을 발명하지 않는다. 기본 정렬 cycle은
61
+ `three-state`(ascending → descending → 정렬 없음)로, 사용자가 정렬을 완전히
62
+ 해제할 수 있는 경로를 항상 남긴다.
63
+
64
+ **플랫폼 번역.**
65
+
66
+ - Web: `role="table"`/`"row"`/`"columnheader"`/`"cell"`. **정렬 가능한 헤더는
67
+ `columnheader` 안의 버튼이지, `columnheader` 자체가 인터랙티브해지는 것이
68
+ 아니다** — Collection 기본 계약의 "interactive item 안에 또 다른 button/link
69
+ 금지"를 뒤집어 표에 적용한 것이다(바깥 컨테이너가 아니라 안쪽 하나의 컨트롤만
70
+ 포커스 가능해야 한다). 정렬 방향은 그 `columnheader`의 `aria-sort`로 그대로
71
+ 나간다.
72
+ - 선택 셀의 checkbox/radio는 CheckboxGroup 선례와 같이 각자 독립 tab stop이다 —
73
+ 별도 roving focus를 만들지 않는다.
74
+ - Native renderer는 이번 배치에 없다(`platform: "web"`) — `behaviorRegistry`의
75
+ `native` 필드는 breadcrumb·form 같은 다른 Web 전용 계약과 같은 빈 배열
76
+ 자리표시자다.
77
+
78
+ **검증 화면.** 아직 실제 제품 vertical slice가 없다 — catalog는 `planned`으로
79
+ 남고, `beta` 승격은 로드맵 gate(실제 화면 검증)를 통과한 뒤 리드가 진행한다.
@@ -0,0 +1,84 @@
1
+ # DatePicker contract
2
+
3
+ ## 문제
4
+
5
+ 압축된 필드 하나에서 날짜 값 하나를 고른다 — 상시 표시되는 달력이 화면 공간을 쓸 수 없는
6
+ 폼/필터 자리(생년월일, 계약일, 시작일 필터)를 위한 것이다. `docs/calendar.md`가 이미
7
+ 판정했듯 이 문제의 "격자" 부분은 `Calendar`와 완전히 같은 문제이고, DatePicker가 새로
8
+ 푸는 부분은 **그 격자를 트리거+오버레이 뒤에 숨기고, 커밋 시 닫는 생명주기**뿐이다.
9
+
10
+ ## antd 대응
11
+
12
+ `DatePicker`(data-entry) → HJM `DatePicker`, `direct`. `crosswalk`는 이미 이 이름 그대로
13
+ 연결돼 있다(`component-references.ts:76`). `RangePicker`(같은 antd 컴포넌트의 variant)는
14
+ 이 계약에 없다 — range 선택 자체를 배제했기 때문이다(아래 참고).
15
+
16
+ ## Calendar와의 경계
17
+
18
+ `docs/calendar.md`의 판정을 그대로 따른다: 격자(월 표시, 셀, 오늘/선택/비활성, 방향키
19
+ 이동)는 `calendar.ts`가 소유하고, 이 파일은 그 위에 세 가지만 더한다.
20
+
21
+ 1. **필드 트리거** — `fieldFrameContract`를 그대로 재사용한다(NumberField, Select가 각자
22
+ 독립적으로 같은 재사용을 하는 것과 같은 이유 — 프레임이 두 벌이면 하나만 바뀌는 순간
23
+ 어긋난다).
24
+ 2. **적응형 오버레이** — `selectRecipe.adaptive`와 동일한 `{ web: "popover", native:
25
+ "sheet" }`. Select가 이미 검증한 패턴을 그대로 따른다(`src/behaviorRegistry.select`,
26
+ `docs/architecture.md`의 「Select와 Combobox의 적응형 경계」).
27
+ 3. **커밋·닫힘 생명주기** — 날짜를 고르면 `onSelectionChange`가 불리고 팝오버/시트가
28
+ 닫힌다. Select의 `selection-requests-close-and-restores-trigger-focus` 시나리오와
29
+ 동일한 결과를 낸다.
30
+
31
+ `resolveDatePickerGrid`는 `resolveCalendarGridDescriptor`를 그대로 호출한다 — 셀의 의미,
32
+ 오늘/선택/비활성 판정, 방향키 산수 중 어느 것도 다시 정의하지 않는다. DatePicker의
33
+ `docs/calendar.md`가 다루는 접근성 회색지대(격자 안 이동)를 다시 열지 않기 위해서다.
34
+
35
+ ## 공개한 상태 축
36
+
37
+ | 축 | 값 |
38
+ | --- | --- |
39
+ | availability | enabled, disabled, readOnly, busy |
40
+ | value | empty, selected, open |
41
+ | validation | valid, invalid |
42
+
43
+ Select의 축과 동일하되 `content`(idle/loading/loadingMore/error)는 없다 — 격자 자체가
44
+ 비동기 컬렉션이 아니라는 `docs/calendar.md`의 판정이 그대로 이어진다.
45
+
46
+ ### 배제한 축
47
+
48
+ - **range 선택**: 측정된 수요가 없다(`docs/calendar.md` 참고). 시작~끝 날짜 구간이
49
+ 필요해지면 `DatePickerSelection`을 넓히지 않고 별도 `DateRangePicker` 계약을 여는 쪽을
50
+ 권한다 — 단일 날짜 소비자의 타입을 좁히지 않기 위해서다(Steps가 clickable을 얹지 않고
51
+ 새 축을 배제한 것과 같은 판단 방식).
52
+ - **자유 입력 텍스트 필드(Combobox 스타일)**: antd의 `DatePicker`는 텍스트 입력으로 날짜를
53
+ 타이핑하는 것도 허용하지만, 이 계약은 트리거(버튼)로만 연다 — Combobox의 `inputValue`
54
+ 같은 별도 편집 축을 추가하려면 날짜 문자열 파싱을 이 패키지가 갖게 되어 "제품이 포맷한
55
+ 문자열을 받는다" 원칙과 정면으로 충돌한다. 필요해지면 별도 컴포넌트로 연다.
56
+ - **PageUp/PageDown 월·년 단축키**: `docs/calendar.md`와 같은 이유로 배제.
57
+
58
+ ## HJM 기본값
59
+
60
+ - 트리거는 `displayValue`(제품이 포맷한 문자열, 예: "2026년 8월 19일")를 보여주고, 없으면
61
+ `placeholder`를 보여준다. Statistic이 값을 절대 스스로 포맷하지 않는 것과 같은 원칙이다.
62
+ - `clear`는 `onSelectionChange(null, "clear")`를 커밋하고 닫는다 — 별도 "빈 선택 금지"
63
+ 설정을 두지 않았다. 대부분의 날짜 필드는 선택 사항이고(Select가 기본으로 빈 선택을
64
+ 허용하는 것과 같다), 강제로 채워야 하는 날짜 필드는 폼 레벨 validation(`invalid` 축)이
65
+ 이미 표현한다.
66
+
67
+ ## 플랫폼 번역
68
+
69
+ - Web: 트리거는 `button`(`aria-haspopup="dialog"`, `aria-expanded`). 열린 표면은 실제
70
+ DOM 포커스가 셀 사이를 이동하는 `dialog` + `grid`다 — Select의 `activeDescendant`
71
+ 리스트박스 패턴과 다르다(Select 팝업의 옵션은 실제 포커스를 받지 않고 트리거가 계속
72
+ 포커스를 쥔다). 날짜 격자는 WAI-ARIA Date Picker Dialog 패턴을 따라 실제 focus가
73
+ gridcell 사이를 roving하므로, `datePickerBehavior.web.focus`는 `"roving"`이다. 닫히면
74
+ 포커스는 트리거로 복원된다.
75
+ - Native: 트리거는 버튼, 오버레이는 Sheet — Select의 Native 렌더러와 동일한 조합이다.
76
+ - 두 플랫폼 모두 Escape/뒤로가기·바깥 탭·선택 커밋이 같은 세 가지 닫힘 경로를 낸다
77
+ (`dismiss: ["selection", "escape"/"back", "outside"]`).
78
+
79
+ ## 검증 화면
80
+
81
+ first-party Web dialog·Native Sheet renderer, 환경 matrix, 기본 실행 증거는 연결되어 surface는
82
+ `beta`다. 다만 `docs/calendar.md`가 밝힌 대로 Yajalal에는 이 문제의 살아있는 vertical
83
+ slice가 아직 없다. 값 하나를 고르는 새 폼 필드가 실제 제품에 생기고 보조기기 증거까지
84
+ 쌓이기 전에는 `stable`로 승격하지 않는다.