@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,130 @@
1
+ # Carousel contract
2
+
3
+ 이 컴포넌트는 접근성에서 가장 자주 실패하는 패턴이다. 아래 계약은 브리프가 요구한 세
4
+ 가지 안전장치(자동 재생 opt-in, 현재 위치의 낭독 가능한 이름, 순환 없음)를 타입과
5
+ 검증기로 강제해, 렌더러가 그중 하나를 빼먹은 채로 컴파일되지 않게 한다.
6
+
7
+ ## 문제
8
+
9
+ 사용자가 한 번에 하나만 보이는 카드 묶음을 순서대로 넘겨 본다 — 야잘알 홈의 "내 구단
10
+ 경기 스트립"(가로 스크롤 페이저)이 실사용처다. Ant Design `Carousel`과 같은 사용자
11
+ 문제를 풀지만, HJM은 `direct` crosswalk를 따르면서도 antd의 기본 동작 두 가지(무한
12
+ 순환, 자동 재생 기본값)를 의도적으로 걷어낸다.
13
+
14
+ ## 일반화한 계약
15
+
16
+ ### collection 기본 계약과의 대응
17
+
18
+ 각 슬라이드는 stable string `id`와 보이는 `label`(슬라이드의 접근 가능한 이름, 예: "8/19
19
+ 두산 vs LG")만 가진다. 슬라이드의 실제 시각 콘텐츠는 제품이 소유한다 — Statistic이
20
+ 숫자를 포맷하지 않는 것과 같은 경계로, Carousel도 카드 내부를 렌더링하지 않는다.
21
+ `textValue`, selection mode, 비동기 `idle|loading|empty` 상태는 Steps·Timeline과 같은
22
+ 이유로 가져오지 않는다 — 슬라이드 집합은 검색 대상이 아니고, 로딩/empty는 제품이
23
+ Carousel을 마운트할지 말지로 먼저 판단한다(빈 배열은 `validateCarouselDescriptor`가
24
+ 던진다).
25
+
26
+ ### 현재 위치는 stable key로, 순서는 그 위에서 유도한다
27
+
28
+ 브리프는 "value의 현재 인덱스"라 표현하지만, 이 저장소의 다른 모든 controlled selection은
29
+ raw 인덱스가 아니라 stable string key를 값으로 쓴다(`SelectSelection`, `TabsSelection`,
30
+ `RadioGroupSelection` — [[collection]], `src/behaviors.ts`). 슬라이드 배열이 필터링되거나
31
+ 재정렬돼도 "지금 보고 있던 그 카드"가 사라지지 않게 하려면 인덱스보다 key가 안전하다.
32
+ 그래서 `ControlledCarouselSelection`/`UncontrolledCarouselSelection`은 다른 컴포넌트와
33
+ 같은 `currentKey`/`defaultCurrentKey`/`onCurrentKeyChange` 모양을 쓰고, 순수 계산
34
+ 함수들(`resolveCarouselDescriptor`, `getCarouselNavigationTarget`)은 항상 이미 해석된
35
+ `CarouselDescriptor.currentKey: Id`(controlled/uncontrolled 분기가 끝난 값)를 받는다 —
36
+ `SelectState`가 렌더러 prop 모양이고 `reconcileSelectSelection`은 이미 해석된
37
+ `selectedKey`를 받는 것과 같은 분리다. "몇 개 중 몇 번째"라는 **인덱스 기반 정보**는
38
+ 버리지 않는다 — `resolveCarouselDescriptor`가 매 슬라이드에 1-based `position`/`total`을
39
+ 유도해서 붙인다. 즉 정체성은 key로, 위치 발화는 인덱스로 — 둘 다 필요하고 서로 다른
40
+ 축이다.
41
+
42
+ ### 현재 위치의 발화 가능한 이름 (필수)
43
+
44
+ 각 resolved 슬라이드는 `position`, `total`, 그리고 제품이 공급한
45
+ `composeAccessibleName({ position, total, label })`으로 만든 `accessibleName`을 가진다 —
46
+ Steps·Timeline과 같은 이유(한국어/영어 어순 차이, RN에 순서 semantics가 없음)로 같은
47
+ 해법을 재사용한다. 이 이름 **하나**가 세 곳에 재사용된다: 각 슬라이드 group의 접근
48
+ 가능한 이름, 각 indicator dot 버튼의 label, 사용자가 직접 넘겼을 때의 발화 문구. 점
49
+ 표시만으로 위치를 말하지 않는다는 브리프 요구를 이 세 재사용처가 함께 충족한다.
50
+
51
+ ### 탭 순서: 사라지지도, 숨어서 받지도 않는다
52
+
53
+ 두 실패 모드를 구분해야 한다 — (a) 슬라이드가 탭 순서에서 통째로 사라져 키보드
54
+ 사용자가 다음/이전 컨트롤 없이는 절대 도달할 수 없는 경우, (b) 화면에 보이지 않는
55
+ 슬라이드의 내부 링크가 여전히 tab 순서에 남아 포커스를 받는 경우(사용자가 보이지 않는
56
+ 곳으로 포커스가 튀는 것을 경험한다 — 이게 더 나쁘다). 계약은 각 resolved 슬라이드에
57
+ `inert: boolean`(현재 슬라이드가 아니면 항상 true)을 붙여 렌더러가 (b)를 만들 수
58
+ 없게 한다. (a)는 별도로 막는다 — `previousControl`/`nextControl`/dot indicator는
59
+ `inert`의 영향을 받지 않는 별도 슬롯이라 항상 tab 순서에 남고, 사용자는 이 컨트롤을
60
+ 통해 결국 모든 슬라이드에 도달할 수 있다. 이 판단은 Tabs의 `mount policy`가 비활성
61
+ panel을 `hidden`/`inert` 처리하는 것과 같은 결까다(`docs/architecture.md`의 keyed Tabs
62
+ mount policy 참고) — 새로 만든 개념이 아니라 이미 검증된 패턴을 재사용한다.
63
+
64
+ ### 순환 없음, 자동 재생은 opt-in
65
+
66
+ - `getCarouselNavigationTarget`은 첫/마지막 슬라이드에서 클램프한다 — `loop` 파라미터
67
+ 자체가 없다(`getCollectionNavigationTarget`의 `loop` 인자와 달리, Carousel에는 그
68
+ 스위치가 아예 존재하지 않는다). 무한 순환은 "끝"이 어디인지 말할 수 없게 만들고,
69
+ 스크린 리더 사용자가 이미 본 슬라이드로 되돌아왔는지 구분할 방법이 없어진다.
70
+ - `CarouselDescriptor.autoplay?`는 기본적으로 없다(`carouselBehaviorDefaults.autoplay ===
71
+ false`). 넣더라도 `isCarouselAutoplayActive`가 세 가지 조건을 모두 통과해야만 재생을
72
+ 허용한다 — reduce motion이 꺼져 있고, hover/focus/drag로 인한 `paused`가 아닐 때뿐.
73
+ 세 조건을 한 함수에 모아 렌더러가 그중 하나를 빼먹고 타이머를 돌릴 수 없게 한다.
74
+ 자동 전환은 announce하지 않는다 — 사용자가 관여하지 않은 전환으로 스크린 리더
75
+ 발화를 방해하지 않는다. 사용자가 직접 넘겼을 때만 새 `accessibleName`을 발표한다.
76
+
77
+ ## HJM 기본값
78
+
79
+ - indicator dot 지름 8px, hit target은 `control.minTouchTarget`(44)로 시각 크기와 별개다
80
+ (Slider의 thumb-vs-hit-target 분리와 같은 이유). 비활성 dot은
81
+ `semanticColors.border.strong`, 현재 dot은 `semanticColors.content.brand` — Steps의
82
+ `current` 마커와 같은 이유로 `primary` fill이 아니라 `contentBrand`를 쓴다(위치 표시는
83
+ 행동이 아니다, `identity.md`).
84
+ - previous/next 컨트롤은 새 버튼 시각을 만들지 않고 기존 `iconButtonRecipe`(ghost tone)를
85
+ 그대로 합성한다 — `chevronStart`/`chevronEnd`(논리 방향, RTL에서 자동 mirror)만
86
+ 지정한다.
87
+ - 슬라이드 전환은 새 모션 토큰을 만들지 않고 기존 `motionPreset.context`(큰 화면 요소
88
+ 전환, reduce motion에서 opacity로 대체)를 그대로 쓴다. 자동 재생은 reduce motion에서
89
+ 아예 실행되지 않으므로 "느려진 자동 재생" 같은 중간 상태가 없다.
90
+ - `dragged`는 `interaction` 축의 값이고 `states.draggedOpacity`(기존 `opacity.dragged`,
91
+ Slider와 동일)로 드러난다.
92
+
93
+ ## 플랫폼 번역
94
+
95
+ - Web: root는 `region`, 각 슬라이드는 `group`(APG carousel 패턴의 `aria-roledescription`
96
+ 관례를 따른다 — 값 자체는 현지화 카피이므로 렌더러가 채운다). `focus: "roving"` —
97
+ Tab은 previous → dots → next → **현재 슬라이드 내부의 인터랙티브 콘텐츠** 순으로만
98
+ 이동하고, `inert` 슬라이드의 내부 콘텐츠는 완전히 건너뛴다. `ArrowLeft`/`ArrowRight`는
99
+ 캐러셀 영역이나 컨트롤에 포커스가 있을 때 이전/다음으로 이동한다(브리프의 "좌우
100
+ 화살표와 탭 순서" 요구).
101
+ - Native: root는 `accessibilityRole="adjustable"`이고
102
+ `accessibilityValue={{min:1, max:total, now:position, text:accessibleName}}`,
103
+ `increment`/`decrement` action이 다음/이전에 대응한다 — 이건 Slider와 같은 선택이고,
104
+ 우연이 아니다. VoiceOver의 좌우 스와이프는 화면의 다음/이전 접근성 엘리먼트로
105
+ 이동하는 제스처로 이미 예약되어 있어서, 캐러셀 자체를 좌우로 "스와이프해 넘기는" 것과
106
+ 충돌한다. `adjustable` role의 위/아래 스와이프(rotor 조정 제스처)를 쓰면 이 충돌이
107
+ 없다. 화면을 직접 손가락으로 미는 시각적 스와이프(비-VoiceOver 사용자)는 렌더러의
108
+ pan gesture가 처리하고, 같은 `getCarouselNavigationTarget`으로 판정한다. `inert`
109
+ 슬라이드는 `accessibilityElementsHidden`/`importantForAccessibility="no-hide-
110
+ descendants"`.
111
+ - Reduce Motion: 수동 전환은 `motionPreset.context`의 `reducedMotion: "opacity"`를 따라
112
+ 크로스페이드로 대체된다. 자동 재생은 켜지지 않는다(위 자동 재생 절 참고).
113
+
114
+ ## 공개한 축 / 배제한 축
115
+
116
+ | 축 | 상태 |
117
+ | --- | --- |
118
+ | `value`(현재 슬라이드, stable key) + 유도된 `position`/`total` | 공개 — 위 "stable key vs 인덱스" 판단 참고 |
119
+ | `interaction`의 `dragged` | 공개 |
120
+ | previous/next/dot 컨트롤 tab 순서, 비활성 슬라이드 `inert` | 공개(필수 계약) |
121
+ | autoplay(opt-in) + reduce-motion/hover/focus/drag guard | 공개 — 기본 꺼짐 |
122
+ | 무한 순환(loop) | **배제** — "끝"을 말할 수 없게 되고 발화가 무너진다(브리프 지침) |
123
+ | `textValue`, selection mode, 비동기 `idle/loading/empty` | **배제** — Steps·Timeline과 같은 이유 |
124
+ | 슬라이드별 `disabled` | **배제** — 측정된 요구가 없다. 카드를 보여줄지 여부는 제품이 슬라이드 배열에 넣을지로 결정한다 |
125
+ | 한 화면에 여러 슬라이드를 보여주는 `slidesToShow` 류 설정 | **배제** — 실사용처(경기 스트립)는 한 번에 하나만 보여주는 페이저다. 필요해지면 그때 이 계약을 넓힌다 |
126
+
127
+ ## 검증 화면
128
+
129
+ 아직 없음. `planned → beta` 승격은 실제 제품 vertical slice 이후 리드가 진행한다(로드맵
130
+ maturity gate). 유력 후보: 야잘알 홈의 내 구단 경기 스트립.
@@ -0,0 +1,93 @@
1
+ # Cascader — 별도 컴포넌트를 만들지 않는다
2
+
3
+ ## 문제로 제기된 것
4
+
5
+ 계층 데이터에서 경로 하나를 고른다(시/도 → 구 → 동). Ant Design `Cascader`와 `direct`
6
+ crosswalk를 따른다.
7
+
8
+ `src/tree-select.ts`와 `docs/tree-select.md`(다른 저작자가 완료)를 직접 읽고 판정한다.
9
+ 그 문서의 결론: 단일 선택 TreeSelect는 "Select의 표면 + Tree의 collection"만으로
10
+ 완결되고(`SelectSelection.selectedKey`가 노드 하나), 다중 선택(체크박스)에서 정말
11
+ 새로 필요했던 조각은 부모의 tri-state 집계뿐이었다 — 그래서 `TreeCheckedKeys<Id> =
12
+ ReadonlySet<Id>`는 **리프 id만** 담도록 validator가 강제하고, 부모 자신의 체크 상태는
13
+ `resolveTreeCheckedStates`가 항상 유도하며 결코 저장하지 않는다(“부모를 값으로도 저장할
14
+ 수 있게 하면 antd가 `checkStrictly`로 풀어야 했던 모호함이 생긴다”는 것이 그 판정의
15
+ 근거). Cascader가 이 완결된 계약 위에 흡수되는지, 아니면 독립된 셋째 계약이 필요한지를
16
+ 판정한다.
17
+
18
+ ## 판정: TreeSelect의 축 하나로 흡수된다 — 만들지 않는다
19
+
20
+ Cascader와 TreeSelect가 갈라 보이는 지점은 둘이다. 각각을 조사했다.
21
+
22
+ ### 1. "값이 경로 전체인가, 노드 하나인가" — 흡수된다
23
+
24
+ antd Cascader의 값은 `(string | number)[]`(루트→선택 노드의 경로)이고, TreeSelect의 값은
25
+ 노드 id 하나(단일 선택) 또는 leaf id의 집합(다중 선택, 위에서 확인한 대로 항상 리프만)이다.
26
+ 그런데 경로는 이미 있는 데이터에서 **파생**된다 — `src/tree.ts`의 `resolveTreeDescriptor`가
27
+ 모든 노드에 `parentId`를 붙이므로, 선택된 노드에서 `parentId`를 따라 루트까지 올라가면
28
+ 경로가 나온다. 새 상태를 저장할 필요가 없고, `Id | null`을 `readonly Id[] | null`로 바꾸는
29
+ 값 표현의 문제일 뿐이다 — 새 축 `valueMode: "node" | "path"` 하나로 대응된다.
30
+
31
+ 이 축이 TreeSelect가 방금 닫은 모호함(부모 자신을 값으로 저장할 수 있게 하는 것)을
32
+ 다시 여는지 확인했다 — **아니다.** `valueMode: "path"`는 이미 커밋된 노드 id(리프든
33
+ 아니든, 단일 선택 한정)의 조상 사슬을 읽기 전용으로 풀어 보여주는 것일 뿐, "이 부모를
34
+ 그 자신의 값으로 저장할 수 있는가"라는 질문과는 직교한다 — 다중 선택 체크박스의 리프
35
+ 전용 저장 규칙을 건드리지 않는다. 즉 `valueMode`는 TreeSelect가 닫아 둔 자리를 다시
36
+ 열지 않고, TreeSelect가 이미 갖고 있는 `parentId` 파생 능력에 값 표현 축 하나만 얹는다.
37
+
38
+ ### 2. "열 단위로 펼쳐지는가, 들여쓰기로 펼쳐지는가" — 이것도 흡수된다
39
+
40
+ 여기가 진짜 검토할 지점이었다. Cascader의 열(column) 레이아웃은 Tree의 임의 개수
41
+ `expandedKeys: ReadonlySet<Id>`(형제 여러 갈래를 동시에 펼칠 수 있음)와 다르게, **한 번에
42
+ 경로 하나만** 열려 있다 — 어떤 깊이에서든 활성 경로가 정확히 하나뿐이라는 제약이 있다.
43
+ 이 제약 자체는 새로운 탐색 모델처럼 보였지만, 뜯어보면 "펼침 집합이 항상 루트에서 활성
44
+ 노드까지의 단일 사슬로 강제된다"는 **`expandedKeys`의 부분집합 제약**이지 새 자료구조가
45
+ 아니다 — 노드를 고르면 그 노드의 경로만 펼치고 다른 사슬은 접는 함수 하나로 표현된다.
46
+
47
+ 열 레이아웃 자체도 이 제약된 펼침 상태의 **Web 렌더러 선택**일 뿐이다. antd Cascader도
48
+ 검색 모드에서는 후보를 "항저우/시후구" 같은 전체 경로 텍스트가 붙은 평평한 목록으로
49
+ 보여준다 — 이건 이미 Select/Combobox의 listbox 그 자체다. 즉 Cascader에는 "열 UI"가
50
+ 필수인 지점이 하나도 없다 — 모바일에서는 드릴다운(경로 스택 + 뒤로가기, 한 화면에 한
51
+ 깊이)으로 번역해도 같은 상태 계약을 만족한다. Select가 Web popup과 Native Sheet로
52
+ 갈리는 것과 같은 종류의 adaptive 렌더러 선택이다.
53
+
54
+ ## 결론
55
+
56
+ Cascader가 실제로 풀던 문제는 TreeSelect 계약에 두 축만 더하면 완결된다.
57
+
58
+ 1. `valueMode: "node" | "path"` — 커밋되는 값이 노드 id인지, 루트부터의 경로인지.
59
+ `path`일 때 값 타입은 `readonly Id[] | null`이고 기존 `parentId` 파생으로 채운다.
60
+ 2. `commitAt: "leaf" | "any"` — antd의 `changeOnSelect`에 대응. 리프에서만 커밋을
61
+ 허용할지, 중간 노드에서도 커밋을 허용할지.
62
+
63
+ `valueMode: "path"`일 때 브라우즈 중 펼침 상태는 항상 활성 노드까지의 단일 사슬로
64
+ 제한된다는 규칙이 추가로 필요하지만, 이것도 `expandedKeys`를 다시 계산하는 함수 하나로
65
+ 표현되지 새 상태 축이 아니다. 열 레이아웃 대 들여쓰기 대 드릴다운은 렌더러가 고른다.
66
+
67
+ `src/cascader.ts`, `test/cascader.test.ts`는 만들지 않는다. `antDesignReferenceComponents`의
68
+ `Cascader → Cascader` crosswalk(`relationship: "direct"`)는 리드가 조정할 대상이다 — 아래
69
+ 배선 제안대로 target을 `tree-select`로 바꾸면 `Dropdown → Menu`, `VirtualList → List`
70
+ alias 판정과 같은 자리가 된다.
71
+
72
+ ## 배선 명세 제안 (리드 적용)
73
+
74
+ - `src/component-references.ts`의 `Cascader` 항목: `targets: ["Cascader"]` →
75
+ `targets: ["tree-select"]`, `relationship: "direct"` → `"adapted"`(같은 문제를 TreeSelect의
76
+ 이름과 축으로 번역하는 것이므로).
77
+ - `src/catalog.ts`의 `Cascader` planned row: 제거하거나(TreeSelect가 `valueMode`/`commitAt`
78
+ 두 축을 얻은 뒤) `aliases: ["Cascader"]`를 TreeSelect row에 추가하는 쪽을 권한다 — 이
79
+ 저작자 판단은 후자다, `Dropdown`이 `Menu`의 alias로 흡수된 선례와 같은 이유.
80
+ - `src/tree-select.ts`는 이 저작자가 고치지 않는다(다른 저작자의 파일). 그 저작자 또는
81
+ 리드가 `valueMode`/`commitAt` 축과 경로 파생 헬퍼(`resolveTreeNodePath` 같은 이름)를
82
+ 추가할 자리라는 것만 이 문서에 남긴다.
83
+
84
+ ## 뒤집힐 조건
85
+
86
+ 다음 중 하나가 실제로 측정되면 이 판정을 다시 연다.
87
+
88
+ 1. 실제 제품에서 "여러 사슬을 동시에 펼친 채 경로를 비교/편집"해야 하는 화면이 나와
89
+ `expandedKeys`의 단일-사슬 제약이 부족해진다.
90
+ 2. 열 레이아웃이 아니면 스크린 리더/키보드 사용자가 감당할 수 없을 만큼 깊이가 깊고
91
+ 폭이 넓은 실제 계층 데이터가 나와, "렌더러 선택"이라던 전제가 깨진다.
92
+ 3. `changeOnSelect`형 중간 노드 커밋이 `commitAt` 하나로 표현하기엔 부족한 추가 규칙
93
+ (예: 중간 노드 커밋 시 하위 요약값 표시)이 실제 화면에서 요구된다.
@@ -0,0 +1,306 @@
1
+ # Catalog이 "만들지 않기로 확정함"을 표현하지 못하는 문제 — 설계 제안
2
+
3
+ ## 문제
4
+
5
+ `ComponentStatus`는 `"stable" | "beta" | "planned" | "deprecated"` 넷뿐이다(`src/catalog.ts`).
6
+ `planned`은 "아직 구현하지 않았지만 구현할 것"을 뜻해 왔다. 그런데 저작 과정에서 이 뜻이
7
+ 거짓인 행이 실제로 쌓였다:
8
+
9
+ - `AppProvider` — 런타임(Context+훅)뿐이라 계약할 값 타입조차 남지 않는다(`docs/app-provider.md`).
10
+ - `Utility` — antd `Util`이 가리키는 문제(토큰을 코드에서 읽는 법) 자체가 이 패키지의
11
+ 기존 정적 export로 이미 해소돼 있다(`docs/utility.md`).
12
+ - `BorderBeam` — antd에 실재하는 컴포넌트이지만(crosswalk은 정확하다, 정정: 이전 판은
13
+ "오염됐다"고 잘못 주장했었다), 상시 반복 장식 모션이 `docs/identity.md`와 정면으로
14
+ 충돌해 만들지 않기로 확정했다(`docs/border-beam.md`).
15
+
16
+ 세 행 모두 **"언젠가 화면이 생기면 만든다"가 아니라 "화면이 생겨도 안 만든다"**다. 그런데
17
+ 행을 지울 수도 없다 — `component-references.test.ts`의
18
+ `"maps every external reference to a real HJM catalog target"`가 모든 crosswalk
19
+ `targets`가 실존하는 catalog 항목을 가리킬 것을 강제하고, 세 항목의 crosswalk source
20
+ (`App`, `Util`, `BorderBeam`)는 각자 **자기 자신**을 target으로 가리킨다(`Notification`→
21
+ `Toast`, `Dropdown`→`Menu`처럼 흡수해 줄 다른 이름이 없다). `ContextPanel`이 안전하게
22
+ 삭제될 수 있었던 건 애초에 그걸 가리키는 crosswalk source가 **하나도 없었기** 때문이고
23
+ (`docs/context-panel.md`), 이 셋은 그 조건을 만족하지 않는다.
24
+
25
+ 즉 두 사실이 동시에 참이어야 한다: **행은 남아야 하고, 그런데 `planned`이 거짓말을
26
+ 한다.** 지금 `ComponentStatus`에는 이 조합을 표현할 자리가 없다.
27
+
28
+ ## 검토한 세 옵션
29
+
30
+ ### 옵션 A — `ComponentStatus`에 새 값 추가(예: `"declined"`)
31
+
32
+ `status`가 "구현 성숙도"와 "만들 것인가"를 하나의 축으로 합치는 방법이다. **영향
33
+ 범위를 실제로 세었다:**
34
+
35
+ | 파일 | 필요한 변경 |
36
+ | --- | --- |
37
+ | `src/catalog.ts` | `ComponentStatus` union에 값 추가 |
38
+ | `src/showcase.ts:196,219` | `getRequiredShowcaseScenarios`/`getRequiredShowcaseEvidence`의 `status === "planned" \|\| status === "deprecated"` 분기에 새 값 추가(또는 `status !== "stable" && status !== "beta"`로 재작성) |
39
+ | `src/showcase.ts:295-305` | `summarizeShowcaseMaturity`의 accumulator 리터럴 `{ stable: 0, beta: 0, planned: 0, deprecated: 0 }`에 새 키 추가 — 안 하면 `Record<ComponentStatus, number>` 타입 에러로 typecheck에서 잡히긴 한다(안전망은 있다) |
40
+ | `test/showcase.test.ts:49` | `summary.stable + summary.beta + summary.planned + summary.deprecated` 합계 단정에 새 값 추가 안 하면 실패 |
41
+ | `docs/architecture.md`(「지원 단계」) | 새 상태의 정의 문단 추가 |
42
+ | `docs/showcase.md:36` | "planned 컴포넌트는 contract 문서만 제공" 문장에 새 값 포함 |
43
+ | `docs/ant-design-coverage.md` | 「두 종류」 표에 세 번째 종류 추가 |
44
+ | `showcase/web/src/components/ComponentExplorer.stories.tsx:121-124,144,149` | 필터 `<option>` 4개→5개, `isPreviewable` 분기, pill 렌더 |
45
+ | `showcase/web/src/showcase-contract.test.ts:21` | `status === "planned"` 필터가 새 값을 놓친다 — 갱신 필요 |
46
+ | `showcase/web/src/showcase.css:120-138` | `[data-status="..."]` 색상 규칙 4개→5개 |
47
+
48
+ **6개 파일, 10곳 이상.** `ComponentStatus`를 소비하는 모든 exhaustive 분기가 새 값을
49
+ 알아야 하고, 그중 다수가 Showcase(웹 UI + 테스트)까지 뻗어 있다.
50
+
51
+ ### 옵션 B — status와 직교하는 필드 (권고)
52
+
53
+ `status`는 계속 "구현 성숙도"만 뜻한다 — `planned`은 그대로 "아직 구현되지 않음"만
54
+ 뜻하고, "구현할 의도가 있는가"라는 **완전히 다른 질문**은 별도 필드가 답한다.
55
+
56
+ ```ts
57
+ // src/catalog.ts — ComponentCatalogEntry에 필드 하나 추가
58
+ export type ComponentCatalogEntry = Readonly<{
59
+ name: string;
60
+ category: ComponentCategory;
61
+ platform: ComponentPlatform;
62
+ status: ComponentStatus;
63
+ aliases?: readonly string[];
64
+ recipe?: RecipeName;
65
+ behavior?: BehaviorName;
66
+ /**
67
+ * `status: "planned"`인 행이 실제로는 "화면이 생겨도 만들지 않기로 확정"인
68
+ * 경우에만 채운다. crosswalk source가 자기 자신을 target으로 가리켜 행을
69
+ * 지울 수 없는 경우(alias로 흡수할 다른 이름이 없음)가 대상이다. 짧은 한 줄
70
+ * 요약이고, 전체 판정과 뒤집힐 조건은 `docs/<component-id>.md`에 있다.
71
+ */
72
+ declinedReason?: string;
73
+ }>;
74
+ ```
75
+
76
+ 세 행에 적용:
77
+
78
+ ```ts
79
+ { name: "AppProvider", category: "provider", platform: "adaptive", status: "planned", aliases: ["App"], declinedReason: "런타임 배선뿐 — message/notification/modal은 이미 Toast·Dialog·AlertDialog" },
80
+ { name: "BorderBeam", category: "utility", platform: "web", status: "planned", declinedReason: "상시 반복 장식 모션이 identity.md와 충돌" },
81
+ { name: "Utility", category: "utility", platform: "web", status: "planned", aliases: ["Util"], declinedReason: "antd Util은 useToken 문서일 뿐 — 토큰은 이미 정적 export" },
82
+ ```
83
+
84
+ **영향 범위:**
85
+
86
+ | 파일 | 필요한 변경 |
87
+ | --- | --- |
88
+ | `src/catalog.ts` | 타입에 optional 필드 1개, 행 3개에 값 추가 |
89
+ | `src/component-definitions.ts` | `ComponentDefinition.contract`에 `declinedReason?: string` passthrough(선택 — 안 해도 `componentCatalog`에서 직접 읽을 수 있다) |
90
+ | `docs/ant-design-coverage.md` | 「두 종류」 표에 세 번째 종류 추가(아래) |
91
+ | `docs/architecture.md` | 「지원 단계」에 한 문단 |
92
+ | (선택) `showcase/web` UI | pill 옆에 조그만 배지/tooltip — **필수 아님**, 없어도 정확성엔 문제없다 |
93
+
94
+ `getRequiredShowcaseScenarios`/`getRequiredShowcaseEvidence`, `summarizeShowcaseMaturity`,
95
+ `test/showcase.test.ts:49`, `showcase-contract.test.ts`, `ComponentExplorer.stories.tsx`의
96
+ 필터/드롭다운, `showcase.css`의 상태별 색 — **전부 무변경**이다. `status`가 여전히
97
+ `"planned"`이므로 기존 "planned은 contract 문서만 요구한다"는 로직이 이미 정확한 답을
98
+ 낸다. `declinedReason`이 있는지는 그 로직에 아무 영향이 없다 — 정확히 그래야 한다,
99
+ declined 행도 Web/Native 스토리를 요구하면 안 되기 때문이다.
100
+
101
+ **타입만으로는 못 막는 조합을 테스트가 막는다:** `declinedReason`이 있는데
102
+ `status !== "planned"`인 행(예: `beta`인데 declined라고 적는 모순)은 값으로만 표현
103
+ 가능하므로 새 테스트로 막는다.
104
+
105
+ ```ts
106
+ it("declinedReason only ever appears on a planned row", () => {
107
+ for (const entry of componentCatalog) {
108
+ if (entry.declinedReason) expect(entry.status).toBe("planned");
109
+ }
110
+ });
111
+
112
+ it("every declined row has a doc explaining the reversal condition", async () => {
113
+ for (const entry of componentCatalog) {
114
+ if (!entry.declinedReason) continue;
115
+ const id = componentIds[entry.name as ComponentName];
116
+ await expect(readFile(`docs/${id}.md`, "utf8")).resolves.toBeTruthy();
117
+ }
118
+ });
119
+ ```
120
+
121
+ 두 번째 테스트는 이미 `component-references.test.ts`가 `readFile`로 `package.json`을
122
+ 읽는 것과 같은 패턴이라 새 인프라가 필요 없다.
123
+
124
+ ### 옵션 C — 아무것도 바꾸지 않는다
125
+
126
+ `docs/<name>.md`만으로 충분하다는 입장이다. 검토했지만 권고하지 않는다: `planned`이
127
+ 계속 "만들 것"으로 오독될 수 있고, 실제로 이번 라운드에서 내가 그 오독으로 `Utility`·
128
+ `BorderBeam` 행을 **삭제하라고 잘못 권고**했다(crosswalk의 self-reference 제약을 놓쳤다).
129
+ 문서만으로는 이 실수를 막지 못한다 — catalog을 보는 사람이 매번 대응하는 문서를 찾아
130
+ 읽어야만 알 수 있고, 옵션 B의 필드 자체가 "이 행은 다르다"는 신호를 catalog 안에 직접
131
+ 남긴다.
132
+
133
+ ## 권고: 옵션 B
134
+
135
+ 이유를 한 문장으로: **"얼마나 완성됐는가"(status)와 "만들 것인가"(declinedReason)는
136
+ 서로 다른 질문이고, 다른 질문은 같은 필드에 욱여넣지 않는다** — 이 저장소가 이미
137
+ 반복해서 쓰는 판단 방식(Select의 `selectedKey`/`open`을 안 섞는 것, DatePicker의
138
+ `selectedDate`/`focusedMonth`를 안 섞는 것)과 같은 자리다. 옵션 A는 정확히 그 반대 —
139
+ 서로 다른 두 질문을 한 축에 합치고, 그 대가로 Showcase 웹 UI까지 포함해 6개 파일·
140
+ 10곳 이상을 건드려야 한다.
141
+
142
+ ## `docs/ant-design-coverage.md`에 반영할 세 번째 종류
143
+
144
+ ```markdown
145
+ | 종류 | 뜻 | 예 | 재검토 신호 |
146
+ |---|---|---|---|
147
+ | **흡수됨** | 그 문제를 이미 다른 컴포넌트가 완결한다 | `Notification`→Toast, `Dropdown`→Menu, `ContextPanel`→SidePanel/Sheet, `Flex`→Stack, `TimePicker`→Select 조합, `Rating`→Slider/Statistic | 흡수한 쪽이 못 푸는 요구가 나올 때 |
148
+ | **검증할 화면이 없음** | 계약 자체는 유효하나 이를 확인할 제품 화면이 없다 | `Anchor`, `Calendar` | 그 화면이 실제로 생길 때 |
149
+ | **거절됨** | 계약을 만들 수는 있지만 정체성·아키텍처 경계와 충돌하거나(`BorderBeam`), 문제 자체가 이 패키지의 기존 구조(export, 다른 컴포넌트)로 이미 해소돼 있는데 흡수 대상 이름이 없어 alias를 걸 수 없다(`Utility`, `AppProvider`) | `BorderBeam`, `Utility`, `AppProvider` | 원칙(identity)이나 이 패키지의 아키텍처 경계(런타임 허용 여부) 자체가 바뀔 때 — **제품 화면이 늘어나는 것만으로는 안 뒤집힌다**, 그 점이 "검증할 화면이 없음"과 다르다 |
150
+ ```
151
+
152
+ catalog 행 처리: **행을 유지한다.** `status: "planned"` 그대로 두고 `declinedReason`만
153
+ 채운다 — crosswalk target을 자기 자신이 가리켜 삭제가 구조적으로 불가능하기 때문이다.
154
+
155
+ ## 남은 것 — 이 설계가 결정하지 않는 것
156
+
157
+ `Grid`/`Masonry`/`Space`/`TreeSelect`/`Anchor`/`Rating` 같은 다른 `planned` 행들이 실제로
158
+ "거절됨"에 속하는지는 각각 별도로 검토해야 한다(이 문서는 그 판단을 내리지 않는다) —
159
+ `Rating`은 이미 `docs/rating.md`가, `Anchor`는 `docs/anchor.md`가, `TreeSelect`는
160
+ `docs/tree-select.md`가 각자 있으니 그 문서들을 다시 읽고 `declinedReason`이 필요한지
161
+ 가리는 감사가 이 설계 적용 이후의 자연스러운 다음 작업이다.
162
+
163
+ ## 적용 순서
164
+
165
+ 1. `src/catalog.ts`: `ComponentCatalogEntry`에 `declinedReason?: string` 추가, `AppProvider`/
166
+ `Utility`/`BorderBeam` 세 행에 값 채우기.
167
+ 2. `src/component-definitions.ts`: (선택) `ComponentDefinition.contract`에 passthrough.
168
+ 3. `docs/ant-design-coverage.md`: 「두 종류」→「세 종류」표 교체.
169
+ 4. `docs/architecture.md`: 「지원 단계」에 `declinedReason`이 뜻하는 것 한 문단.
170
+ 5. `component-references.test.ts`(또는 새 테스트 파일)에 위 두 단정(`status`
171
+ 일관성, 대응 doc 존재) 추가.
172
+ 6. (선택, 필수 아님) `showcase/web` UI에 declined 배지 — Showcase 정확성에는 영향 없으므로
173
+ 급하지 않다.
174
+
175
+ 1~5는 서로 독립적으로 검증 가능하다(각 단계 후 `pnpm typecheck && pnpm test`가 계속
176
+ 통과해야 한다) — 옵션 A처럼 여러 파일을 한 번에 바꿔야 typecheck이 통과하는 상황이
177
+ 생기지 않는다.
178
+
179
+ ---
180
+
181
+ ## 감사 — `status: "planned"` + recipe 없는 행 전수 조사
182
+
183
+ 옵션 B가 적용된 뒤, 실제로 이 신호가 필요한 다른 행이 있는지 감사했다. `src/catalog.ts`
184
+ 에서 `status: "planned"`이면서 `recipe`가 없는 행을 전부 골랐다(14개) — 이미 recipe가
185
+ 있는 행은 "만들지 말지"가 아니라 "이미 만들었고 승격만 남았다"는 뜻이라 대상이 아니다.
186
+
187
+ | 행 | 대응 antd | 조사 | 판정 | catalog 처리 |
188
+ |---|---|---|---|---|
189
+ | `Grid` | `Grid` | Yajalal 전수 검색(`columns`/`numColumns`/grid) — 있는 모든 "여러 열" 요구는 `Statistic`이 이미 자체 `columns: 1\|2\|3\|4` 축으로 흡수했다(`statisticRecipe`). 범용 반응형 Grid를 쓰는 화면은 없다. BurnTok은 이 머신에 저장소가 없어 확인 못함(`docs/color-picker.md`와 같은 제약). | **검증할 화면이 없음** — CSS Grid(Web)와 RN Flexbox+`numColumns`(Native)가 이름·단위를 공유하지 않는다는 점도 `docs/virtual-list.md`/`docs/affix.md`의 "공유 semantic 없음" 논증과 같다. 다만 실제 반응형 다단 레이아웃 화면이 나오면 계약이 정당화될 수 있어(정체성 충돌이 아님) 거절됨은 아니다. | 변경 없음 |
190
+ | `Masonry` | `Masonry` | 동일 검색, masonry/waterfall 코드 없음. | **검증할 화면이 없음** — BurnTok(사진/영상 피드)에는 미래에 타당할 수 있는 문제라 거절 근거가 없다. | 변경 없음 |
191
+ | `Space` | `Space` | antd `Space`(자식 사이 일정한 간격+wrap+정렬)는 `Stack`이 이미 가진 축(`stackRecipe.defaults`의 `gap`/`align`/`justify`/`wrap`, `axes: {block,inline}`)과 겹친다. antd 자체도 `Space`/`Flex`가 서로 거의 같은 문제라는 지적이 흔하다 — `Flex`는 이미 `Stack`에 흡수됐다(`aliases: ["Flex"]`). `Space`만의 나머지 둘: `split`(자식 사이에 구분선 삽입)은 이미 존재하는 `Divider`를 자식 사이에 끼워 넣는 조합으로 그대로 되고, `Space.Compact`(인접 필드의 테두리를 시각적으로 합치는 것)는 spacing이 아니라 별개의 좁은 시각 패턴이라 이 흡수 여부와 무관하다(필요해지면 그때 별도로 연다). | **흡수됨 → Stack** | 아래 diff |
192
+ | `TimePicker` | `TimePicker` | 이미 감사됨(`docs/time-picker.md`, 이 저작자 작성) — Select 두 개(시·분)로 흡수. | **흡수됨(다중 대상 조합)** — 이미 올바르게 처리돼 있다. | 변경 없음(확인만) |
193
+ | `ColorPicker` | `ColorPicker` | 이미 감사됨(`docs/color-picker.md`). 실사용처 없음, 뒤집힐 조건이 전부 제품 화면 등장이다. | **검증할 화면이 없음** — 이미 올바르게 처리돼 있다. | 변경 없음(확인만) |
194
+ | `Cascader` | `Cascader` | 이미 감사됨(`docs/cascader.md`) — TreeSelect에 `valueMode`/`commitAt` 두 축이 더해지면 흡수된다는 판정. **그 두 축이 아직 `src/tree-select.ts`에 없다.** | **흡수 대기(선결 축 없음)** — 아래 「세 종류 다듬기」 참고, 넷째 종류가 필요했다. | 지금은 행·crosswalk 둘 다 그대로 둔다(아래 근거) |
195
+ | `Rating` | `Rate` | 이미 감사됨(`docs/rating.md`) — Slider(입력)/Statistic(표시) 조합으로 흡수. | **흡수됨(다중 대상 조합)** — 이미 올바르게 처리돼 있다. | 변경 없음(확인만) |
196
+ | `TreeSelect` | `TreeSelect` | 계약 모듈이 실재한다(`src/tree-select.ts`, 테스트·문서 포함) — recipe가 없는 건 select/tree/checkbox recipe 셋을 그대로 합성하기 때문이지 "안 만들기로 함"이 아니다. | **감쇠 분류 대상이 아니다** — 아래 「세 종류 다듬기」 참고. 별도의 작은 카탈로그 위생 문제만 있다. | 아래 참고(선택적 diff) |
197
+ | `Anchor` | `Anchor` | 이미 감사됨(`docs/anchor.md`). | **검증할 화면이 없음** — 이미 올바르게 처리돼 있다. | 변경 없음(확인만) |
198
+ | `VirtualList` | `List`(lifecycle) | 이미 감사됨(`docs/virtual-list.md`) — "공유할 semantic이 없다"는 강한 논증이지만, 뒤집힐 조건 셋 다 제품/엔지니어링 증거 등장이다(정체성 충돌 아님). | **검증할 화면이 없음** — 이미 올바르게 처리돼 있다. | 변경 없음(확인만) |
199
+ | `QRCode` | `QRCode` | 이미 감사됨(`docs/qrcode.md`). | **검증할 화면이 없음** — 이미 올바르게 처리돼 있다. | 변경 없음(확인만). **별도 발견**: 문서 파일명이 `docs/qrcode.md`인데 `componentIds.QRCode`는 `"qr-code"`다 — canonical 파일명은 `docs/qr-code.md`여야 한다. 지금 당장 필요한 변경은 아니지만(QRCode는 `declinedReason`이 없어 파일 존재를 강제하는 새 테스트의 대상이 아니다), 이름 규칙 감사 때 함께 고칠 것을 권한다. |
200
+ | `Watermark` | `Watermark` | 이미 감사됨(`docs/watermark.md`). | **검증할 화면이 없음** — 이미 올바르게 처리돼 있다. | 변경 없음(확인만) |
201
+ | `ConfirmPopover` | `Popconfirm` | 이미 감사됨(`docs/popover.md` 말미) — Popover(표면)+AlertDialog(confirm session) 조합으로 흡수. | **흡수됨(다중 대상 조합)** — 이미 올바르게 처리돼 있다. | 변경 없음(확인만) |
202
+ | `Affix` | `Affix` | 이미 감사됨(`docs/affix.md`). | **검증할 화면이 없음** — 이미 올바르게 처리돼 있다. | 변경 없음(확인만) |
203
+
204
+ 핵심 결과: **14개 중 13개는 이미 올바르게 분류돼 있었다.** 실제로 재분류가 필요했던 것은
205
+ `Space`(흡수됨으로 새로 판정) 하나이고, `Cascader`는 이미 옳게 "흡수됨"으로 판정됐지만
206
+ 그 판정을 catalog에 지금 적용하면 안 된다는 것을 발견했다(아래). `TreeSelect`는 애초에
207
+ 이 감사의 대상(만들지 않기로 한 것)이 아니었다는 것을 확인했다.
208
+
209
+ ### `Space` 적용 diff
210
+
211
+ ```ts
212
+ // src/catalog.ts:50 — Stack의 aliases에 Space 추가
213
+ { name: "Stack", category: "layout", platform: "shared", status: "planned", recipe: "stackRecipe", aliases: ["Flex", "Space"] },
214
+
215
+ // src/catalog.ts:54 — Space 행 삭제
216
+ // { name: "Space", category: "layout", platform: "shared", status: "planned", aliases: ["Inline"] }, ← 제거
217
+
218
+ // src/component-references.ts:61
219
+ { name: "Space", category: "layout", targets: ["Stack"], relationship: "adapted" }, // was targets: ["Space"], relationship: "direct"
220
+ ```
221
+
222
+ `Flex`가 같은 방식으로 이미 흡수됐을 때와 마찬가지로 별도 `docs/space.md`를 새로 만들지
223
+ 않았다 — `Flex`의 흡수도 전용 문서 없이 crosswalk+alias만으로 기록돼 있다(`docs/*.md`에
224
+ `flex.md`가 없다). 이 절 자체가 그 기록 역할을 한다. `aliases: ["Inline"]`(Space의
225
+ 원래 별칭)은 Stack에 옮기지 않았다 — "Inline"은 antd Space가 아니라 일반 명사라 Stack
226
+ 검색에 그대로 두면 오히려 혼란을 준다는 판단이다. 리드가 원하면 `aliases: ["Flex",
227
+ "Space", "Inline"]`로 그대로 옮기는 쪽도 무리는 아니다.
228
+
229
+ ## 「세 종류」 표에 대한 다듬기 — 감사가 드러낸 것
230
+
231
+ ### 1. 「흡수됨」은 두 갈래로 나뉜다 — catalog 처리가 다르다
232
+
233
+ `Notification`→`Toast`, `Dropdown`→`Menu`, `Flex`/`Space`→`Stack`처럼 **흡수 대상이
234
+ 정확히 하나**면 행을 지우고 그 대상에 `aliases`를 단다. 그런데 `TimePicker`→Select×2,
235
+ `Rating`→Slider/Statistic, `ConfirmPopover`→Popover+AlertDialog처럼 **흡수 대상이 둘
236
+ 이상의 조합**이면 alias를 걸 단일 이름이 없다 — 그래서 이 셋은 (이미 정확하게) 행을
237
+ 그대로 두고 있었다. 이건 실수가 아니라 이 저장소가 이미 세 번 반복해서 도달한 정답이다.
238
+ `docs/ant-design-coverage.md`의 「흡수됨」 처리 문장에 이 구분을 추가해야 한다:
239
+
240
+ > **흡수됨**은 행을 지우고 흡수한 쪽에 `aliases`로 이름을 남깁니다 — 남겨 두면 "아직
241
+ > 만들 계획"으로 잘못 읽힙니다. **다만 흡수 대상이 둘 이상의 조합이면**(예:
242
+ > `TimePicker`→Select 둘, `Rating`→Slider/Statistic, `ConfirmPopover`→Popover+
243
+ > AlertDialog) alias를 걸 단일 이름이 없으므로 행을 그대로 둡니다 — 조합 방법은 각
244
+ > 컴포넌트의 판정 문서에 남습니다.
245
+
246
+ ### 2. 네 번째 종류가 실제로 필요했다 — 「흡수 대기(선결 축 없음)」
247
+
248
+ `Cascader`가 그 사례다. 판정 자체(TreeSelect + `valueMode`/`commitAt` 두 축 = Cascader)는
249
+ `docs/cascader.md`에서 이미 끝났고 재검토가 필요 없다. 그런데 그 축이 **아직
250
+ `src/tree-select.ts`에 없다.** 지금 crosswalk를 `tree-select`로 돌리고 catalog 행을
251
+ 지우면, 실제로는 만들지 않은 해결책(`valueMode`/`commitAt`)을 "이미 흡수 완료"로
252
+ 표시하는 거짓말이 된다 — `AppProvider`를 `planned`으로 두는 것과 반대 방향의 같은
253
+ 문제다(전자는 "안 만들 것"을 "만들 것"처럼, 후자는 "아직 안 만든 것"을 "이미 다
254
+ 됐다"처럼 보이게 한다).
255
+
256
+ 기존 세 종류 어디에도 안 맞는다: **흡수됨**은 흡수 대상이 그 기능을 이미 갖고
257
+ 있다고 전제한다(지금은 아니다). **검증할 화면이 없음**은 계약 자체가 유효하다고
258
+ 전제한다(맞지만, 재검토 신호가 "제품 화면 등장"이 아니라 "엔지니어링 선행 조건
259
+ 충족"이라 다른 신호다). **거절됨**은 아예 아니다(만들 것이다, 그것도 곧).
260
+
261
+ ```markdown
262
+ | **흡수 대기(선결 축 없음)** | 판정은 "다른 컴포넌트에 흡수된다"로 이미 끝났지만, 그 흡수를 실제로 가능하게 할 축이 흡수 대상에 아직 없다 | `Cascader`(TreeSelect의 `valueMode`/`commitAt` 대기) | 그 축이 흡수 대상에 실제로 추가될 때 — **제품 화면과 무관한 엔지니어링 선행 조건**이라는 점이 "검증할 화면이 없음"과 다르다 |
263
+ ```
264
+
265
+ catalog 처리: **행과 crosswalk 둘 다 지금은 건드리지 않는다** — 결과적으로 "검증할
266
+ 화면이 없음"과 같은 처리(가만히 둔다)이지만, 재검토 신호가 다르므로 표에서는 구별해
267
+ 둔다. `valueMode`/`commitAt`이 `tree-select.ts`에 실제로 추가되면 그때
268
+ `docs/cascader.md`가 이미 제안한 배선(행 삭제 또는 `aliases: ["Cascader"]`를
269
+ TreeSelect에 추가)을 적용한다.
270
+
271
+ ### 3. `TreeSelect`는 억지로 끼워 맞추지 않는다 — 넷 중 어디에도 안 들어가는 게 맞다
272
+
273
+ `TreeSelect`를 이 audit에 넣은 것 자체가 처음엔 틀린 전제였다. `src/tree-select.ts`가
274
+ 실재하고(`resolveTreeCheckedStates`/`toggleTreeCheckedSelection`/
275
+ `validateTreeCheckedSelection`), 테스트·문서까지 갖췄다 — **이건 "안 만들기로 한 것"이
276
+ 아니라 "이미 만든 것"이다.** recipe가 없는 이유도 카탈로그 상 흔한 패턴과 같다(`Radio`→
277
+ `selectionControlRecipe`, `TextArea`→`fieldRecipe`, `Mentions`→`comboboxRecipe`처럼
278
+ 기존 recipe 재사용) — 다만 `TreeSelect`는 재사용하는 recipe가 **셋**(select+tree+
279
+ checkbox)이라 `recipe?: RecipeName` 단일 필드로는 어느 것도 대표로 못 고른다. 이건
280
+ "만들지 않기로 확정함" 분류 문제가 아니라 **catalog 위생 문제**다: 소비자가 recipe
281
+ 없는 행을 보고 "아직 아무것도 안 만들어졌다"로 오독할 수 있다.
282
+
283
+ 이 감사의 범위(만들지 않기로 한 것의 분류)와는 다른 문제라 `declinedReason` 메커니즘을
284
+ 적용하지 않았고, 새 넷째 종류도 만들지 않았다 — 억지로 끼워 맞추면 "이미 완성된
285
+ 계약"과 "안 만들기로 확정한 것"을 같은 표에 섞게 된다. 대신 별도로 짧게 제안한다
286
+ (적용 여부는 리드 판단):
287
+
288
+ ```ts
289
+ // src/catalog.ts:86 — 참고용, 이 audit의 diff는 아니다
290
+ { name: "TreeSelect", category: "input", platform: "web", status: "planned", recipe: "selectRecipe", behavior: "select" },
291
+ ```
292
+
293
+ `selectRecipe`/`select`를 대표로 고른 이유는 트리거+오버레이 chrome이 사용자가 가장
294
+ 먼저 보는 조각이기 때문이다(Mentions가 comboboxRecipe를 대표로 고른 것과 같은
295
+ 근거) — `treeRecipe`/체크박스 recipe도 함께 쓴다는 사실은 `docs/tree-select.md`가
296
+ 이미 "HJM 기본값" 절에서 설명하고 있으니 catalog 필드가 그 셋을 전부 못 담아도
297
+ 정보 손실은 아니다.
298
+
299
+ ### 4. 부수적 발견 — `QRCode` 문서 파일명이 canonical id와 다르다
300
+
301
+ `componentIds.QRCode`는 `"qr-code"`인데 문서는 `docs/qrcode.md`다. 지금은 `QRCode`에
302
+ `declinedReason`이 없어 이 감사가 새로 세운 "declined 행마다 `docs/<id>.md`가
303
+ 실존해야 한다" 테스트의 대상이 아니라 당장 깨지는 것은 없다. 다만 이름 규칙이
304
+ 어긋난 채로 있으면 나중에 `QRCode`가 정말 `declinedReason`을 갖게 될 때(또는 다른
305
+ 자동화가 파일명 규칙에 의존하게 될 때) 조용히 깨질 자리라 남겨 둔다 — 이 audit의
306
+ 범위 밖이라 고치지 않았다.
@@ -0,0 +1,72 @@
1
+ # ColorPicker — 지금은 만들지 않는다
2
+
3
+ ## 문제로 제기된 것
4
+
5
+ 임의의 색을 고른다(hex/RGB/HSB 입력, 팔레트, 최근 사용 색). Ant Design `ColorPicker`와
6
+ `direct` crosswalk를 따른다. catalog에는 이미 `{ name: "ColorPicker", category: "input",
7
+ platform: "web", status: "planned" }` 자리가 예약돼 있다.
8
+
9
+ ## 판정: 실사용처 근거 없이 만들지 않는다
10
+
11
+ 먼저 가정하지 말라는 지시대로 두 제품에 색을 고르는 화면이 있는지 확인했다.
12
+
13
+ - **Yajalal RN**(`modules/app-rn/src`): `grep -rli "colorpicker\|color picker\|color-picker"`가
14
+ 0건이다. 코드베이스 전체에 색을 고르는 화면이 없다.
15
+ - **BurnTok Web**: 이 머신에 저장소가 없어 코드로 직접 확인할 수 없었다. 로드맵
16
+ (`docs/expansion-roadmap.md`)의 제품 적용 순서·완료 슬라이스 기록 어디에도 색 선택
17
+ 화면이 등장하지 않는다 — 언급되는 색 관련 작업은 전부 "제품이 이미 고정한 semantic
18
+ color를 렌더러가 어떻게 적용하는가"(Toast tone mark, Statistic trend mark, 구단 색
19
+ adapter)이고, "사용자가 임의의 색을 고르는" 문제는 한 번도 나오지 않는다.
20
+
21
+ 측정된 vertical slice가 없다는 뜻이다. `docs/expansion-roadmap.md`의 maturity gate는
22
+ "실제 제품 vertical slice 없이 승격하지 않는다"고 정하고 있고, 이 원칙은 계약을 쓸지
23
+ 말지에도 그대로 적용된다 — `docs/notification.md`·`docs/dropdown.md`·
24
+ `docs/virtual-list.md`가 이미 "측정된 요구가 없으면 만들지 않는다"로 판정한 것과 같은
25
+ 근거다.
26
+
27
+ ## 이 판정을 더 무겁게 만드는 이유
28
+
29
+ ColorPicker는 만들면 가벼운 계약이 아니다. 로드맵의 상태 축 표 어디에도 없는 완전히
30
+ 새로운 세 갈래 문제를 한 번에 열어야 한다.
31
+
32
+ 1. **색 공간 표현.** hex/RGB/HSB/alpha 중 무엇을 공개 API로 삼을지, 변환 규칙을 어디
33
+ 소유할지부터 새로 정해야 한다 — 이 저장소의 어떤 기존 컴포넌트도 색 값 자체를
34
+ 입력 데이터로 다루지 않는다(`semanticColors`/`ColorReference`는 전부 제품이 아니라
35
+ 시스템이 미리 고정한 토큰이다).
36
+ 2. **대비·접근성.** 사용자가 고른 색이 텍스트/배경 대비 4.5:1을 만족하는지, 선택 UI
37
+ 자체의 포커스 인디케이터가 색상 스와치 배경과 충분한 대비를 유지하는지를 계약이
38
+ 보장해야 한다 — Statistic의 trend, UploadItem의 상태처럼 이 저장소는 "색으로만
39
+ 말하지 않는다"를 지켜왔는데, ColorPicker는 그 규칙과 정반대로 색 자체가 선택 결과인
40
+ 컴포넌트라 이 원칙을 어떻게 지킬지부터 새로 설계해야 한다(스와치에 값 텍스트를
41
+ 병기하는 것 정도는 쉽지만, 색맹 사용자를 위한 팔레트 순서·명도 대비 규칙은 가볍지
42
+ 않다).
43
+ 3. **입력 방식.** 팔레트 탭, 텍스트 hex 입력, 슬라이더(hue/saturation/brightness) 최소
44
+ 셋을 하나의 컴포넌트가 조율해야 하고, 셋 사이의 값 동기화(hex 입력 중 오타 상태를
45
+ 팔레트에 어떻게 반영하는지)는 NumberField/Slider보다 훨씬 큰 상태 기계다.
46
+
47
+ 수요 없이 이 세 갈래를 먼저 짜면 다음 사람이 "실제로 무엇을 위해 이렇게 무거운가"를
48
+ 또 물어야 하는 계약이 된다. 반대로 실사용처가 나오면, 그 화면이 실제로 필요한 것은
49
+ 이 셋 중 일부뿐일 가능성이 높다(예: 구단 색 하나를 브랜드 팔레트에서만 고르는 화면이면
50
+ 색 공간 변환도 hex 입력도 필요 없다) — 지금 전체를 설계하면 그 실제 요구보다 큰 계약이
51
+ 된다.
52
+
53
+ ## 만들지 않은 것
54
+
55
+ `src/color-picker.ts`, `test/color-picker.test.ts`는 없다. catalog의
56
+ `{ name: "ColorPicker", category: "input", platform: "web", status: "planned" }` row와
57
+ `antDesignReferenceComponents`의 `ColorPicker → ColorPicker` crosswalk(`relationship:
58
+ "direct"`)는 건드리지 않는다 — 이름 자리를 지우는 것이 아니라, 지금 채울 계약이 없다는
59
+ 것이다.
60
+
61
+ ## 뒤집힐 조건
62
+
63
+ 다음 중 하나가 실제로 측정되면 이 판정을 다시 연다.
64
+
65
+ 1. BurnTok 또는 Yajalal에 사용자가 임의의 색을 고르는 실제 화면 요구가 나온다(예: 커스텀
66
+ 테마, 사용자 지정 하이라이트 색).
67
+ 2. 요구가 팔레트에서 미리 정의된 색 중 하나만 고르는 좁은 범위로 확인되면, ColorPicker
68
+ 전체가 아니라 `Chip` 또는 `RadioGroup`의 시각 변형으로 더 가볍게 흡수될 수 있는지부터
69
+ 먼저 검토한다 — hex 입력·색 공간 변환이 필요 없는 한 별도 컴포넌트를 열 이유가
70
+ 약해진다.
71
+ 3. 요구가 hex/RGB 자유 입력까지 포함하면, 이 문서의 세 갈래(색 공간/대비/입력 방식)를
72
+ 실제 화면의 좁은 요구에 맞춰 하나씩만 계약하고 나머지는 열지 않는다.