@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,132 @@
1
+ import { flattenTreeNodes, validateTreeNodes } from "./tree.js";
2
+ function leafIdsOf(nodes) {
3
+ return new Set(flattenTreeNodes(nodes)
4
+ .filter((node) => node.children === undefined)
5
+ .map((node) => node.id));
6
+ }
7
+ /**
8
+ * A parent's own id is never meaningfully "in" the checked set — its
9
+ * checked/unchecked/mixed state is always derived from its descendant
10
+ * leaves, the same way a DataTable header is never itself a row in
11
+ * `selectedKeys`. Storing a parent id here would be ambiguous (does it mean
12
+ * "all children" or "just this category, independent of children"?) with no
13
+ * measured product need to resolve that ambiguity, so it is rejected instead
14
+ * of guessed at.
15
+ */
16
+ export function validateTreeCheckedSelection(nodes, checkedKeys) {
17
+ validateTreeNodes(nodes);
18
+ const leafIds = leafIdsOf(nodes);
19
+ for (const key of checkedKeys) {
20
+ if (!leafIds.has(key)) {
21
+ throw new RangeError(`TreeSelect checkedKeys must reference an existing leaf node id, not a parent or unknown id: ${key}`);
22
+ }
23
+ }
24
+ }
25
+ function combineCoverage(a, b) {
26
+ return { enabled: a.enabled + b.enabled, checked: a.checked + b.checked };
27
+ }
28
+ function coverageToState(coverage) {
29
+ if (coverage.enabled === 0 || coverage.checked === 0)
30
+ return false;
31
+ return coverage.checked === coverage.enabled ? true : "mixed";
32
+ }
33
+ /**
34
+ * One depth-first pass computing every node's tri-state at once — the same
35
+ * "resolve the whole tree once, filter afterward" shape `resolveTreeDescriptor`
36
+ * already uses for depth/position, rather than re-walking per node.
37
+ *
38
+ * A parent's aggregate is computed from *enabled descendant leaf coverage*,
39
+ * exactly generalizing `resolveDataTableSelectAllState`'s "disabled rows are
40
+ * excluded from both the denominator and the count." A disabled leaf keeps
41
+ * its own literal `checkedKeys.has(id)` as its individual displayed state
42
+ * (a disabled checkbox can still visibly be checked), but never counts
43
+ * toward any ancestor's true/false/mixed — otherwise a single
44
+ * disabled-and-unchecked leaf would permanently pin every ancestor to
45
+ * "mixed" even once all of its enabled siblings are checked.
46
+ */
47
+ export function resolveTreeCheckedStates(nodes, checkedKeys) {
48
+ validateTreeCheckedSelection(nodes, checkedKeys);
49
+ const states = new Map();
50
+ const visit = (node) => {
51
+ if (node.children === undefined) {
52
+ const checked = checkedKeys.has(node.id);
53
+ states.set(node.id, checked);
54
+ return node.disabled
55
+ ? { enabled: 0, checked: 0 }
56
+ : { enabled: 1, checked: checked ? 1 : 0 };
57
+ }
58
+ const coverage = node.children
59
+ .map(visit)
60
+ .reduce(combineCoverage, { enabled: 0, checked: 0 });
61
+ states.set(node.id, coverageToState(coverage));
62
+ return coverage;
63
+ };
64
+ for (const node of nodes)
65
+ visit(node);
66
+ return states;
67
+ }
68
+ /**
69
+ * Checking or unchecking any node (leaf or parent) sets every enabled
70
+ * descendant leaf to the opposite of "fully checked" — the same
71
+ * mixed-defaults-to-checked convention `getCheckboxNextState` already uses
72
+ * for a single aggregate Checkbox (`src/behaviors.ts`). Disabled leaves are
73
+ * left untouched, mirroring `toggleCheckboxSelection`'s own disabled guard.
74
+ * This is "the rule where selecting a parent selects the children together,"
75
+ * made unambiguous by the fact that parents are never independently stored.
76
+ */
77
+ export function toggleTreeCheckedSelection(nodes, checkedKeys, targetId) {
78
+ const states = resolveTreeCheckedStates(nodes, checkedKeys);
79
+ const currentState = states.get(targetId);
80
+ if (currentState === undefined) {
81
+ throw new RangeError(`Tree node must exist: ${targetId}`);
82
+ }
83
+ const flattened = flattenTreeNodes(nodes);
84
+ const target = flattened.find((node) => node.id === targetId);
85
+ const descendantLeaves = (target.children ? flattenTreeNodes(target.children) : [target]).filter((node) => node.children === undefined);
86
+ const nextChecked = currentState !== true;
87
+ const next = new Set(checkedKeys);
88
+ for (const leaf of descendantLeaves) {
89
+ if (leaf.disabled)
90
+ continue;
91
+ if (nextChecked)
92
+ next.add(leaf.id);
93
+ else
94
+ next.delete(leaf.id);
95
+ }
96
+ return next;
97
+ }
98
+ /**
99
+ * Drops leaf ids that left the tree or stopped being leaves — the same
100
+ * shape as `reconcileCheckboxSelection`/`reconcileTreeExpansion`. Returns the
101
+ * same reference when nothing changed.
102
+ */
103
+ export function reconcileTreeCheckedSelection(nodes, checkedKeys) {
104
+ validateTreeNodes(nodes);
105
+ const leafIds = leafIdsOf(nodes);
106
+ const reconciled = [...checkedKeys].filter((id) => leafIds.has(id));
107
+ return reconciled.length === checkedKeys.size ? checkedKeys : new Set(reconciled);
108
+ }
109
+ /**
110
+ * No new recipe file: the surface composes three *existing* recipes
111
+ * unchanged — Select's trigger/frame (`selectRecipe`), Tree's row anatomy
112
+ * (`treeRecipe.node`/`.toggle`/`.indentPerLevel`) for each popup row, and the
113
+ * existing Checkbox mark for the tri-state indicator this module derives.
114
+ * Declaring a fourth "treeSelectRecipe" would just alias tokens the other
115
+ * three already own.
116
+ *
117
+ * No new behaviorRegistry entry either: TreeSelect's open/dismiss reasons,
118
+ * roles, and keyboard model are `behaviorRegistry.select` verbatim — this
119
+ * module only adds the functions above, which a Select+Tree renderer calls
120
+ * to compute per-row checkbox state and to interpret a row's checkbox
121
+ * activation.
122
+ */
123
+ export const treeSelectBehaviorScenarios = [
124
+ "single-select-mode-needs-none-of-this-module-selectedkey-and-flat-items-are-enough",
125
+ "checking-an-unchecked-parent-checks-every-enabled-descendant-leaf",
126
+ "unchecking-a-fully-checked-parent-unchecks-every-enabled-descendant-leaf",
127
+ "a-parent-with-some-but-not-all-descendant-leaves-checked-renders-mixed",
128
+ "disabled-leaves-are-skipped-by-cascade-toggle-but-still-counted-by-derivation",
129
+ "checkedkeys-containing-a-parent-or-unknown-id-is-rejected-not-silently-ignored",
130
+ "removing-a-checked-leaf-from-the-tree-reconciles-it-out-without-touching-siblings",
131
+ ];
132
+ //# sourceMappingURL=tree-select.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-select.js","sourceRoot":"","sources":["../src/tree-select.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CheckboxState } from \"./behaviors.js\";\nimport { flattenTreeNodes, validateTreeNodes, type TreeNodeDescriptor } from \"./tree.js\";\n\n/**\n * `docs/tree.md`'s own \"TreeSelect 판정\" concludes TreeSelect is Select's\n * surface (trigger + popup/sheet, committed value) plus Tree's collection\n * (depth/sibling announcement, arrow-key expand/collapse, `expandedKeys`\n * reconciliation) — no third primitive. That verdict is right for\n * **single-select** TreeSelect: picking exactly one node needs nothing\n * beyond Select's existing `selectedKey: Id | null` and Tree's flat item\n * list, because there is nothing to aggregate.\n *\n * It is incomplete for **multiple-select** TreeSelect (checkboxes at every\n * depth). Neither file solves \"a parent's checkbox must show a third,\n * aggregate state when only some of its descendant leaves are checked\" —\n * that is not a new selection model, it is `resolveDataTableSelectAllState`\n * (`src/data-table.ts`) generalized from one level (header ↔ rows) to\n * recursive depth (parent ↔ every descendant leaf), reusing the exact same\n * `CheckboxState` (`boolean | \"mixed\"`) the header checkbox already renders.\n * This module is that one missing piece — nothing else.\n */\nexport type TreeCheckedKeys<Id extends string = string> = ReadonlySet<Id>;\n\nfunction leafIdsOf<Id extends string>(\n nodes: readonly TreeNodeDescriptor<Id>[],\n): ReadonlySet<Id> {\n return new Set(\n flattenTreeNodes(nodes)\n .filter((node) => node.children === undefined)\n .map((node) => node.id),\n );\n}\n\n/**\n * A parent's own id is never meaningfully \"in\" the checked set — its\n * checked/unchecked/mixed state is always derived from its descendant\n * leaves, the same way a DataTable header is never itself a row in\n * `selectedKeys`. Storing a parent id here would be ambiguous (does it mean\n * \"all children\" or \"just this category, independent of children\"?) with no\n * measured product need to resolve that ambiguity, so it is rejected instead\n * of guessed at.\n */\nexport function validateTreeCheckedSelection<Id extends string>(\n nodes: readonly TreeNodeDescriptor<Id>[],\n checkedKeys: TreeCheckedKeys<Id>,\n): void {\n validateTreeNodes(nodes);\n const leafIds = leafIdsOf(nodes);\n for (const key of checkedKeys) {\n if (!leafIds.has(key)) {\n throw new RangeError(\n `TreeSelect checkedKeys must reference an existing leaf node id, not a parent or unknown id: ${key}`,\n );\n }\n }\n}\n\ntype LeafCoverage = Readonly<{ enabled: number; checked: number }>;\n\nfunction combineCoverage(a: LeafCoverage, b: LeafCoverage): LeafCoverage {\n return { enabled: a.enabled + b.enabled, checked: a.checked + b.checked };\n}\n\nfunction coverageToState(coverage: LeafCoverage): CheckboxState {\n if (coverage.enabled === 0 || coverage.checked === 0) return false;\n return coverage.checked === coverage.enabled ? true : \"mixed\";\n}\n\n/**\n * One depth-first pass computing every node's tri-state at once — the same\n * \"resolve the whole tree once, filter afterward\" shape `resolveTreeDescriptor`\n * already uses for depth/position, rather than re-walking per node.\n *\n * A parent's aggregate is computed from *enabled descendant leaf coverage*,\n * exactly generalizing `resolveDataTableSelectAllState`'s \"disabled rows are\n * excluded from both the denominator and the count.\" A disabled leaf keeps\n * its own literal `checkedKeys.has(id)` as its individual displayed state\n * (a disabled checkbox can still visibly be checked), but never counts\n * toward any ancestor's true/false/mixed — otherwise a single\n * disabled-and-unchecked leaf would permanently pin every ancestor to\n * \"mixed\" even once all of its enabled siblings are checked.\n */\nexport function resolveTreeCheckedStates<Id extends string>(\n nodes: readonly TreeNodeDescriptor<Id>[],\n checkedKeys: TreeCheckedKeys<Id>,\n): ReadonlyMap<Id, CheckboxState> {\n validateTreeCheckedSelection(nodes, checkedKeys);\n\n const states = new Map<Id, CheckboxState>();\n const visit = (node: TreeNodeDescriptor<Id>): LeafCoverage => {\n if (node.children === undefined) {\n const checked = checkedKeys.has(node.id);\n states.set(node.id, checked);\n return node.disabled\n ? { enabled: 0, checked: 0 }\n : { enabled: 1, checked: checked ? 1 : 0 };\n }\n const coverage = node.children\n .map(visit)\n .reduce(combineCoverage, { enabled: 0, checked: 0 });\n states.set(node.id, coverageToState(coverage));\n return coverage;\n };\n for (const node of nodes) visit(node);\n return states;\n}\n\n/**\n * Checking or unchecking any node (leaf or parent) sets every enabled\n * descendant leaf to the opposite of \"fully checked\" — the same\n * mixed-defaults-to-checked convention `getCheckboxNextState` already uses\n * for a single aggregate Checkbox (`src/behaviors.ts`). Disabled leaves are\n * left untouched, mirroring `toggleCheckboxSelection`'s own disabled guard.\n * This is \"the rule where selecting a parent selects the children together,\"\n * made unambiguous by the fact that parents are never independently stored.\n */\nexport function toggleTreeCheckedSelection<Id extends string>(\n nodes: readonly TreeNodeDescriptor<Id>[],\n checkedKeys: TreeCheckedKeys<Id>,\n targetId: Id,\n): TreeCheckedKeys<Id> {\n const states = resolveTreeCheckedStates(nodes, checkedKeys);\n const currentState = states.get(targetId);\n if (currentState === undefined) {\n throw new RangeError(`Tree node must exist: ${targetId}`);\n }\n\n const flattened = flattenTreeNodes(nodes);\n const target = flattened.find((node) => node.id === targetId)!;\n const descendantLeaves = (\n target.children ? flattenTreeNodes(target.children) : [target]\n ).filter((node) => node.children === undefined);\n\n const nextChecked = currentState !== true;\n const next = new Set(checkedKeys);\n for (const leaf of descendantLeaves) {\n if (leaf.disabled) continue;\n if (nextChecked) next.add(leaf.id);\n else next.delete(leaf.id);\n }\n return next;\n}\n\n/**\n * Drops leaf ids that left the tree or stopped being leaves — the same\n * shape as `reconcileCheckboxSelection`/`reconcileTreeExpansion`. Returns the\n * same reference when nothing changed.\n */\nexport function reconcileTreeCheckedSelection<Id extends string>(\n nodes: readonly TreeNodeDescriptor<Id>[],\n checkedKeys: TreeCheckedKeys<Id>,\n): TreeCheckedKeys<Id> {\n validateTreeNodes(nodes);\n const leafIds = leafIdsOf(nodes);\n const reconciled = [...checkedKeys].filter((id) => leafIds.has(id));\n return reconciled.length === checkedKeys.size ? checkedKeys : new Set(reconciled);\n}\n\n/**\n * No new recipe file: the surface composes three *existing* recipes\n * unchanged — Select's trigger/frame (`selectRecipe`), Tree's row anatomy\n * (`treeRecipe.node`/`.toggle`/`.indentPerLevel`) for each popup row, and the\n * existing Checkbox mark for the tri-state indicator this module derives.\n * Declaring a fourth \"treeSelectRecipe\" would just alias tokens the other\n * three already own.\n *\n * No new behaviorRegistry entry either: TreeSelect's open/dismiss reasons,\n * roles, and keyboard model are `behaviorRegistry.select` verbatim — this\n * module only adds the functions above, which a Select+Tree renderer calls\n * to compute per-row checkbox state and to interpret a row's checkbox\n * activation.\n */\nexport const treeSelectBehaviorScenarios = [\n \"single-select-mode-needs-none-of-this-module-selectedkey-and-flat-items-are-enough\",\n \"checking-an-unchecked-parent-checks-every-enabled-descendant-leaf\",\n \"unchecking-a-fully-checked-parent-unchecks-every-enabled-descendant-leaf\",\n \"a-parent-with-some-but-not-all-descendant-leaves-checked-renders-mixed\",\n \"disabled-leaves-are-skipped-by-cascade-toggle-but-still-counted-by-derivation\",\n \"checkedkeys-containing-a-parent-or-unknown-id-is-rejected-not-silently-ignored\",\n \"removing-a-checked-leaf-from-the-tree-reconciles-it-out-without-touching-siblings\",\n] as const;\n"]}
package/dist/tree.d.ts ADDED
@@ -0,0 +1,206 @@
1
+ import type { AsyncCollectionState, CollectionItemDescriptor, CollectionSelectionModel, SelectionDirection, WebKeyboardKey } from "./behaviors.js";
2
+ import { type CollectionNavigationIntent, type CollectionTypeaheadOptions } from "./collection.js";
3
+ /**
4
+ * Ant Design Tree's drag-to-reorder is intentionally not reproduced — no
5
+ * measured product need exists yet, and it is a materially harder contract
6
+ * (drop-position semantics, cross-parent move validation) that should not be
7
+ * guessed at ahead of a real screen. See docs/tree.md.
8
+ */
9
+ export type TreeNodeDescriptor<Id extends string = string> = Omit<CollectionItemDescriptor<Id>, "shortcut" | "tone"> & Readonly<{
10
+ /** Absent on a leaf. Never an empty array — a node either has no children or at least one. */
11
+ children?: readonly TreeNodeDescriptor<Id>[];
12
+ }>;
13
+ export type TreeSelectionModel<Id extends string = string> = CollectionSelectionModel<Id>;
14
+ export type TreeAsyncState = AsyncCollectionState;
15
+ /** Checks one node's own fields; does not recurse — validateTreeNodes walks the whole tree. */
16
+ export declare function validateTreeNode<Id extends string>(node: TreeNodeDescriptor<Id>): void;
17
+ /** Depth-first flatten of every node at every depth, parents before children. */
18
+ export declare function flattenTreeNodes<Id extends string>(nodes: readonly TreeNodeDescriptor<Id>[]): readonly TreeNodeDescriptor<Id>[];
19
+ /**
20
+ * `TreeNodeDescriptor` is `CollectionItemDescriptor` plus recursion, so the
21
+ * flattened list is passed straight into the existing `validateCollection`
22
+ * for base-contract checks (non-empty label/textValue, globally unique id)
23
+ * instead of re-implementing them. Only the structural checks that
24
+ * `validateCollection`'s fixed two-level `{items} | {sections}` shape cannot
25
+ * express — id whitespace, the empty-children rule, and the recursion itself
26
+ * — are new here.
27
+ */
28
+ export declare function validateTreeNodes<Id extends string>(nodes: readonly TreeNodeDescriptor<Id>[]): void;
29
+ /**
30
+ * Drops expanded keys that no longer identify an expandable node — removed
31
+ * since the last render, or a leaf that never had children — the same way
32
+ * `reconcileCheckboxSelection` drops selected ids that left the collection.
33
+ * Returns the same reference when nothing changed.
34
+ */
35
+ export declare function reconcileTreeExpansion<Id extends string>(nodes: readonly TreeNodeDescriptor<Id>[], expandedKeys: ReadonlySet<Id>): ReadonlySet<Id>;
36
+ export type TreeAccessibleNameInfo = Readonly<{
37
+ /** 1-indexed; root nodes are depth 1. */
38
+ depth: number;
39
+ /** 1-indexed position among this node's own siblings, not the whole flattened tree. */
40
+ position: number;
41
+ siblingCount: number;
42
+ label: string;
43
+ hasChildren: boolean;
44
+ expanded: boolean;
45
+ }>;
46
+ /**
47
+ * Products own phrase order and whether/how "펼쳐짐"/"collapsed" is said for
48
+ * a leaf (`hasChildren` lets the composer skip that clause entirely) — same
49
+ * reason Steps, Timeline, and Carousel do not assemble their own accessible
50
+ * name.
51
+ */
52
+ export type ComposeTreeAccessibleName = (info: TreeAccessibleNameInfo) => string;
53
+ export type ResolvedTreeNodeDescriptor<Id extends string = string> = Omit<TreeNodeDescriptor<Id>, "children"> & Readonly<{
54
+ depth: number;
55
+ position: number;
56
+ siblingCount: number;
57
+ parentId: Id | null;
58
+ hasChildren: boolean;
59
+ /** Always false when `hasChildren` is false — a leaf has no expansion state. */
60
+ expanded: boolean;
61
+ /** False when any ancestor is collapsed. A renderer may still choose to keep it mounted-but-hidden. */
62
+ visible: boolean;
63
+ accessibleName: string;
64
+ }>;
65
+ export type ResolveTreeOptions = Readonly<{
66
+ composeAccessibleName: ComposeTreeAccessibleName;
67
+ }>;
68
+ /**
69
+ * Flattens the whole tree (not only the visible part) and attaches depth,
70
+ * sibling position, parent linkage, expansion, visibility, and the composed
71
+ * accessible name. Callers filter `.visible` for rendering and for the
72
+ * navigation helpers below — one resolved array is the single source both
73
+ * consult, the same relationship Tabs' resolved panel list has to its mount
74
+ * policy.
75
+ */
76
+ export declare function resolveTreeDescriptor<Id extends string>(nodes: readonly TreeNodeDescriptor<Id>[], expandedKeys: ReadonlySet<Id>, options: ResolveTreeOptions): readonly ResolvedTreeNodeDescriptor<Id>[];
77
+ export type TreeArrowKeyIntent = "expand" | "collapse";
78
+ /**
79
+ * ArrowRight/ArrowLeft carry opposite meaning in RTL, the same translation
80
+ * `getSelectionNavigationIntent` already applies for CheckboxGroup/RadioGroup
81
+ * orientation. Every other key (Up/Down/Home/End/Typeahead) is a linear-list
82
+ * problem Tree does not re-solve — see getVisibleTreeNodes below.
83
+ */
84
+ export declare function getTreeArrowKeyIntent(key: WebKeyboardKey, direction: SelectionDirection): TreeArrowKeyIntent | undefined;
85
+ export type TreeArrowResult<Id extends string = string> = Readonly<{
86
+ action: "expand" | "collapse";
87
+ }> | Readonly<{
88
+ action: "moveFocus";
89
+ targetId: Id;
90
+ }> | Readonly<{
91
+ action: "none";
92
+ }>;
93
+ /**
94
+ * Resolves the WAI-ARIA tree pattern for the pressed arrow: expand or move
95
+ * into the first child; collapse or move to the parent. `disabled` never
96
+ * gates this — it only gates selection eligibility (CollectionItemDescriptor
97
+ * precedent), a node's children stay explorable either way.
98
+ */
99
+ export declare function getTreeArrowResult<Id extends string>(visible: readonly ResolvedTreeNodeDescriptor<Id>[], currentId: Id, intent: TreeArrowKeyIntent): TreeArrowResult<Id>;
100
+ /**
101
+ * Vertical movement across the currently visible nodes only — a collapsed
102
+ * subtree's descendants are absent from `visible`, so this needs no Tree-
103
+ * specific math at all: `getCollectionNavigationTarget` (disabled-skip, no
104
+ * loop) already does exactly this for Menu/Select, and a resolved,
105
+ * `.visible`-filtered node list satisfies `CollectionItemDescriptor` because
106
+ * it is one, plus extra fields.
107
+ */
108
+ export declare function getVisibleTreeNavigationTarget<Id extends string>(visible: readonly ResolvedTreeNodeDescriptor<Id>[], currentId: Id | null | undefined, intent: CollectionNavigationIntent): Id | undefined;
109
+ /** Same reuse for typeahead: only currently visible nodes are matchable. */
110
+ export declare function getVisibleTreeTypeaheadMatch<Id extends string>(visible: readonly ResolvedTreeNodeDescriptor<Id>[], query: string, options?: CollectionTypeaheadOptions<Id>): Id | undefined;
111
+ export declare const treeBehaviorDefaults: {
112
+ readonly loop: false;
113
+ };
114
+ export declare const treeBehavior: {
115
+ readonly controlled: readonly ["expandedKeys", "defaultExpandedKeys", "onExpandedKeysChange", "selection", "asyncState"];
116
+ readonly defaults: {
117
+ readonly loop: false;
118
+ };
119
+ readonly stateAxes: {
120
+ readonly availability: readonly ["enabled", "disabled"];
121
+ readonly value: readonly ["selected", "expanded"];
122
+ readonly content: readonly ["idle", "loading", "loadingMore", "empty", "error"];
123
+ };
124
+ readonly web: {
125
+ readonly roles: readonly ["tree", "treeitem", "group"];
126
+ readonly keyboard: readonly ["Tab", "Enter", "Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "Typeahead"];
127
+ readonly focus: "roving";
128
+ };
129
+ /** Tree is Web-only per catalog platform classification — no native surface, matching Breadcrumb and Tooltip. */
130
+ readonly native: {
131
+ readonly roles: readonly [];
132
+ readonly states: readonly [];
133
+ readonly actions: readonly [];
134
+ };
135
+ readonly scenarios: readonly ["depth-and-sibling-position-are-announced-not-only-visual-indent", "arrow-expands-or-moves-to-the-first-child-arrow-collapses-or-moves-to-the-parent-respecting-rtl", "up-down-home-end-move-across-visible-nodes-only-collapsed-subtrees-are-skipped", "typeahead-matches-only-currently-visible-nodes", "disabled-gates-selection-only-expand-and-collapse-remain-available", "empty-children-array-is-rejected-a-node-either-omits-children-or-lists-at-least-one", "one-tab-stop-per-node-the-expand-collapse-glyph-is-decorative-not-a-nested-control", "drag-reorder-is-out-of-scope-until-a-real-product-need-exists"];
136
+ };
137
+ export declare const treeRecipe: {
138
+ readonly slots: readonly ["root", "node", "toggle", "indent", "label", "description"];
139
+ readonly indentPerLevel: 20;
140
+ /** Reuses the same row chrome Menu/Select items already use instead of inventing new selection-row visuals. */
141
+ readonly node: {
142
+ readonly minHeight: 44;
143
+ readonly paddingHorizontal: 12;
144
+ readonly gap: 12;
145
+ readonly radius: "md";
146
+ readonly label: {
147
+ readonly color: Readonly<{
148
+ source: "theme";
149
+ key: "textBody";
150
+ alpha?: number;
151
+ }>;
152
+ readonly textVariant: "body";
153
+ };
154
+ readonly description: {
155
+ readonly color: Readonly<{
156
+ source: "theme";
157
+ key: "textMuted";
158
+ alpha?: number;
159
+ }>;
160
+ readonly textVariant: "label";
161
+ };
162
+ readonly highlightedBackground: Readonly<{
163
+ source: "theme";
164
+ key: "text";
165
+ alpha?: number;
166
+ }>;
167
+ readonly focus: {
168
+ readonly color: Readonly<{
169
+ source: "theme";
170
+ key: "contentBrand";
171
+ alpha?: number;
172
+ }>;
173
+ readonly width: 2;
174
+ readonly offset: 2;
175
+ };
176
+ readonly selectedBackground: Readonly<{
177
+ source: "theme";
178
+ key: "primary";
179
+ alpha?: number;
180
+ }>;
181
+ readonly selectedIndicator: Readonly<{
182
+ source: "theme";
183
+ key: "contentBrand";
184
+ alpha?: number;
185
+ }>;
186
+ readonly danger: Readonly<{
187
+ source: "theme";
188
+ key: "danger";
189
+ alpha?: number;
190
+ }>;
191
+ };
192
+ readonly toggle: {
193
+ readonly size: "sm";
194
+ readonly icons: {
195
+ readonly collapsed: "chevronEnd";
196
+ readonly expanded: "chevronDown";
197
+ };
198
+ readonly color: Readonly<{
199
+ source: "theme";
200
+ key: "textMuted";
201
+ alpha?: number;
202
+ }>;
203
+ readonly hitTarget: 44;
204
+ };
205
+ };
206
+ //# sourceMappingURL=tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../src/tree.ts"],"names":[],"mappings":"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"}
package/dist/tree.js ADDED
@@ -0,0 +1,223 @@
1
+ import { getCollectionNavigationTarget, getCollectionTypeaheadMatch, validateCollection, } from "./collection.js";
2
+ import { collectionItemContract } from "./component-contracts.js";
3
+ import { control, spacing } from "./foundations.js";
4
+ import { semanticColors } from "./semantic-colors.js";
5
+ function assertNonEmpty(value, field) {
6
+ if (value.trim().length === 0) {
7
+ throw new TypeError(`Tree ${field} must not be empty`);
8
+ }
9
+ }
10
+ /** Checks one node's own fields; does not recurse — validateTreeNodes walks the whole tree. */
11
+ export function validateTreeNode(node) {
12
+ assertNonEmpty(node.id, "node id");
13
+ if (node.id !== node.id.trim()) {
14
+ throw new TypeError("Tree node id must not start or end with whitespace");
15
+ }
16
+ assertNonEmpty(node.label, "node label");
17
+ assertNonEmpty(node.textValue, "node textValue");
18
+ if (node.description !== undefined) {
19
+ assertNonEmpty(node.description, "node description");
20
+ }
21
+ if (node.children !== undefined && node.children.length === 0) {
22
+ throw new TypeError(`Tree node ${node.id} children must not be an empty array — omit children instead`);
23
+ }
24
+ }
25
+ /** Depth-first flatten of every node at every depth, parents before children. */
26
+ export function flattenTreeNodes(nodes) {
27
+ return nodes.flatMap((node) => [
28
+ node,
29
+ ...(node.children ? flattenTreeNodes(node.children) : []),
30
+ ]);
31
+ }
32
+ /**
33
+ * `TreeNodeDescriptor` is `CollectionItemDescriptor` plus recursion, so the
34
+ * flattened list is passed straight into the existing `validateCollection`
35
+ * for base-contract checks (non-empty label/textValue, globally unique id)
36
+ * instead of re-implementing them. Only the structural checks that
37
+ * `validateCollection`'s fixed two-level `{items} | {sections}` shape cannot
38
+ * express — id whitespace, the empty-children rule, and the recursion itself
39
+ * — are new here.
40
+ */
41
+ export function validateTreeNodes(nodes) {
42
+ if (nodes.length === 0) {
43
+ throw new RangeError("Tree must contain at least one root node");
44
+ }
45
+ const flattened = flattenTreeNodes(nodes);
46
+ for (const node of flattened) {
47
+ validateTreeNode(node);
48
+ }
49
+ validateCollection({ items: flattened });
50
+ }
51
+ /**
52
+ * Drops expanded keys that no longer identify an expandable node — removed
53
+ * since the last render, or a leaf that never had children — the same way
54
+ * `reconcileCheckboxSelection` drops selected ids that left the collection.
55
+ * Returns the same reference when nothing changed.
56
+ */
57
+ export function reconcileTreeExpansion(nodes, expandedKeys) {
58
+ const expandable = new Set(flattenTreeNodes(nodes)
59
+ .filter((node) => node.children !== undefined)
60
+ .map((node) => node.id));
61
+ const reconciled = [...expandedKeys].filter((id) => expandable.has(id));
62
+ return reconciled.length === expandedKeys.size ? expandedKeys : new Set(reconciled);
63
+ }
64
+ /**
65
+ * Flattens the whole tree (not only the visible part) and attaches depth,
66
+ * sibling position, parent linkage, expansion, visibility, and the composed
67
+ * accessible name. Callers filter `.visible` for rendering and for the
68
+ * navigation helpers below — one resolved array is the single source both
69
+ * consult, the same relationship Tabs' resolved panel list has to its mount
70
+ * policy.
71
+ */
72
+ export function resolveTreeDescriptor(nodes, expandedKeys, options) {
73
+ validateTreeNodes(nodes);
74
+ if (typeof options.composeAccessibleName !== "function") {
75
+ throw new TypeError("Tree composeAccessibleName must be a function");
76
+ }
77
+ const resolved = [];
78
+ const walk = (siblings, depth, parentId, parentVisible) => {
79
+ siblings.forEach((node, index) => {
80
+ const hasChildren = node.children !== undefined;
81
+ const expanded = hasChildren && expandedKeys.has(node.id);
82
+ const position = index + 1;
83
+ const siblingCount = siblings.length;
84
+ const accessibleName = options.composeAccessibleName({
85
+ depth,
86
+ position,
87
+ siblingCount,
88
+ label: node.label,
89
+ hasChildren,
90
+ expanded,
91
+ });
92
+ if (typeof accessibleName !== "string" || accessibleName.trim().length === 0) {
93
+ throw new TypeError("Tree composeAccessibleName must return a non-empty string");
94
+ }
95
+ const { children, ...rest } = node;
96
+ resolved.push({
97
+ ...rest,
98
+ depth,
99
+ position,
100
+ siblingCount,
101
+ parentId,
102
+ hasChildren,
103
+ expanded,
104
+ visible: parentVisible,
105
+ accessibleName,
106
+ });
107
+ if (children) {
108
+ walk(children, depth + 1, node.id, parentVisible && expanded);
109
+ }
110
+ });
111
+ };
112
+ walk(nodes, 1, null, true);
113
+ return resolved;
114
+ }
115
+ /**
116
+ * ArrowRight/ArrowLeft carry opposite meaning in RTL, the same translation
117
+ * `getSelectionNavigationIntent` already applies for CheckboxGroup/RadioGroup
118
+ * orientation. Every other key (Up/Down/Home/End/Typeahead) is a linear-list
119
+ * problem Tree does not re-solve — see getVisibleTreeNodes below.
120
+ */
121
+ export function getTreeArrowKeyIntent(key, direction) {
122
+ if (key === "ArrowRight")
123
+ return direction === "rtl" ? "collapse" : "expand";
124
+ if (key === "ArrowLeft")
125
+ return direction === "rtl" ? "expand" : "collapse";
126
+ return undefined;
127
+ }
128
+ /**
129
+ * Resolves the WAI-ARIA tree pattern for the pressed arrow: expand or move
130
+ * into the first child; collapse or move to the parent. `disabled` never
131
+ * gates this — it only gates selection eligibility (CollectionItemDescriptor
132
+ * precedent), a node's children stay explorable either way.
133
+ */
134
+ export function getTreeArrowResult(visible, currentId, intent) {
135
+ const node = visible.find((candidate) => candidate.id === currentId);
136
+ if (!node) {
137
+ throw new RangeError(`Tree node must be visible to receive arrow input: ${String(currentId)}`);
138
+ }
139
+ if (intent === "expand") {
140
+ if (!node.hasChildren)
141
+ return { action: "none" };
142
+ if (!node.expanded)
143
+ return { action: "expand" };
144
+ const firstChild = visible.find((candidate) => candidate.parentId === node.id && candidate.position === 1);
145
+ return firstChild ? { action: "moveFocus", targetId: firstChild.id } : { action: "none" };
146
+ }
147
+ if (node.hasChildren && node.expanded)
148
+ return { action: "collapse" };
149
+ if (node.parentId !== null)
150
+ return { action: "moveFocus", targetId: node.parentId };
151
+ return { action: "none" };
152
+ }
153
+ /**
154
+ * Vertical movement across the currently visible nodes only — a collapsed
155
+ * subtree's descendants are absent from `visible`, so this needs no Tree-
156
+ * specific math at all: `getCollectionNavigationTarget` (disabled-skip, no
157
+ * loop) already does exactly this for Menu/Select, and a resolved,
158
+ * `.visible`-filtered node list satisfies `CollectionItemDescriptor` because
159
+ * it is one, plus extra fields.
160
+ */
161
+ export function getVisibleTreeNavigationTarget(visible, currentId, intent) {
162
+ return getCollectionNavigationTarget({ items: visible }, currentId, intent, false);
163
+ }
164
+ /** Same reuse for typeahead: only currently visible nodes are matchable. */
165
+ export function getVisibleTreeTypeaheadMatch(visible, query, options) {
166
+ return getCollectionTypeaheadMatch({ items: visible }, query, options);
167
+ }
168
+ export const treeBehaviorDefaults = {
169
+ loop: false,
170
+ };
171
+ export const treeBehavior = {
172
+ controlled: ["expandedKeys", "defaultExpandedKeys", "onExpandedKeysChange", "selection", "asyncState"],
173
+ defaults: treeBehaviorDefaults,
174
+ stateAxes: {
175
+ availability: ["enabled", "disabled"],
176
+ value: ["selected", "expanded"],
177
+ content: ["idle", "loading", "loadingMore", "empty", "error"],
178
+ },
179
+ web: {
180
+ roles: ["tree", "treeitem", "group"],
181
+ keyboard: [
182
+ "Tab",
183
+ "Enter",
184
+ "Space",
185
+ "ArrowUp",
186
+ "ArrowDown",
187
+ "ArrowLeft",
188
+ "ArrowRight",
189
+ "Home",
190
+ "End",
191
+ "Typeahead",
192
+ ],
193
+ focus: "roving",
194
+ },
195
+ /** Tree is Web-only per catalog platform classification — no native surface, matching Breadcrumb and Tooltip. */
196
+ native: { roles: [], states: [], actions: [] },
197
+ scenarios: [
198
+ "depth-and-sibling-position-are-announced-not-only-visual-indent",
199
+ "arrow-expands-or-moves-to-the-first-child-arrow-collapses-or-moves-to-the-parent-respecting-rtl",
200
+ "up-down-home-end-move-across-visible-nodes-only-collapsed-subtrees-are-skipped",
201
+ "typeahead-matches-only-currently-visible-nodes",
202
+ "disabled-gates-selection-only-expand-and-collapse-remain-available",
203
+ "empty-children-array-is-rejected-a-node-either-omits-children-or-lists-at-least-one",
204
+ "one-tab-stop-per-node-the-expand-collapse-glyph-is-decorative-not-a-nested-control",
205
+ "drag-reorder-is-out-of-scope-until-a-real-product-need-exists",
206
+ ],
207
+ };
208
+ export const treeRecipe = {
209
+ slots: ["root", "node", "toggle", "indent", "label", "description"],
210
+ indentPerLevel: spacing.lg,
211
+ /** Reuses the same row chrome Menu/Select items already use instead of inventing new selection-row visuals. */
212
+ node: collectionItemContract,
213
+ toggle: {
214
+ size: "sm",
215
+ icons: {
216
+ collapsed: "chevronEnd",
217
+ expanded: "chevronDown",
218
+ },
219
+ color: semanticColors.content.secondary,
220
+ hitTarget: control.minTouchTarget,
221
+ },
222
+ };
223
+ //# sourceMappingURL=tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.js","sourceRoot":"","sources":["../src/tree.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AsyncCollectionState,\n BehaviorContract,\n CollectionItemDescriptor,\n CollectionSelectionModel,\n SelectionDirection,\n WebKeyboardKey,\n} from \"./behaviors.js\";\nimport {\n getCollectionNavigationTarget,\n getCollectionTypeaheadMatch,\n validateCollection,\n type CollectionNavigationIntent,\n type CollectionTypeaheadOptions,\n} from \"./collection.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport type { SemanticIconName } from \"./icon.js\";\nimport { collectionItemContract } from \"./component-contracts.js\";\nimport { control, spacing } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * Ant Design Tree's drag-to-reorder is intentionally not reproduced — no\n * measured product need exists yet, and it is a materially harder contract\n * (drop-position semantics, cross-parent move validation) that should not be\n * guessed at ahead of a real screen. See docs/tree.md.\n */\nexport type TreeNodeDescriptor<Id extends string = string> = Omit<\n CollectionItemDescriptor<Id>,\n \"shortcut\" | \"tone\"\n> &\n Readonly<{\n /** Absent on a leaf. Never an empty array — a node either has no children or at least one. */\n children?: readonly TreeNodeDescriptor<Id>[];\n }>;\n\nexport type TreeSelectionModel<Id extends string = string> = CollectionSelectionModel<Id>;\nexport type TreeAsyncState = AsyncCollectionState;\n\nfunction assertNonEmpty(value: string, field: string): void {\n if (value.trim().length === 0) {\n throw new TypeError(`Tree ${field} must not be empty`);\n }\n}\n\n/** Checks one node's own fields; does not recurse — validateTreeNodes walks the whole tree. */\nexport function validateTreeNode<Id extends string>(node: TreeNodeDescriptor<Id>): void {\n assertNonEmpty(node.id, \"node id\");\n if (node.id !== node.id.trim()) {\n throw new TypeError(\"Tree node id must not start or end with whitespace\");\n }\n assertNonEmpty(node.label, \"node label\");\n assertNonEmpty(node.textValue, \"node textValue\");\n if (node.description !== undefined) {\n assertNonEmpty(node.description, \"node description\");\n }\n if (node.children !== undefined && node.children.length === 0) {\n throw new TypeError(\n `Tree node ${node.id} children must not be an empty array — omit children instead`,\n );\n }\n}\n\n/** Depth-first flatten of every node at every depth, parents before children. */\nexport function flattenTreeNodes<Id extends string>(\n nodes: readonly TreeNodeDescriptor<Id>[],\n): readonly TreeNodeDescriptor<Id>[] {\n return nodes.flatMap((node) => [\n node,\n ...(node.children ? flattenTreeNodes(node.children) : []),\n ]);\n}\n\n/**\n * `TreeNodeDescriptor` is `CollectionItemDescriptor` plus recursion, so the\n * flattened list is passed straight into the existing `validateCollection`\n * for base-contract checks (non-empty label/textValue, globally unique id)\n * instead of re-implementing them. Only the structural checks that\n * `validateCollection`'s fixed two-level `{items} | {sections}` shape cannot\n * express — id whitespace, the empty-children rule, and the recursion itself\n * — are new here.\n */\nexport function validateTreeNodes<Id extends string>(\n nodes: readonly TreeNodeDescriptor<Id>[],\n): void {\n if (nodes.length === 0) {\n throw new RangeError(\"Tree must contain at least one root node\");\n }\n const flattened = flattenTreeNodes(nodes);\n for (const node of flattened) {\n validateTreeNode(node);\n }\n validateCollection({ items: flattened });\n}\n\n/**\n * Drops expanded keys that no longer identify an expandable node — removed\n * since the last render, or a leaf that never had children — the same way\n * `reconcileCheckboxSelection` drops selected ids that left the collection.\n * Returns the same reference when nothing changed.\n */\nexport function reconcileTreeExpansion<Id extends string>(\n nodes: readonly TreeNodeDescriptor<Id>[],\n expandedKeys: ReadonlySet<Id>,\n): ReadonlySet<Id> {\n const expandable = new Set(\n flattenTreeNodes(nodes)\n .filter((node) => node.children !== undefined)\n .map((node) => node.id),\n );\n const reconciled = [...expandedKeys].filter((id) => expandable.has(id));\n return reconciled.length === expandedKeys.size ? expandedKeys : new Set(reconciled);\n}\n\nexport type TreeAccessibleNameInfo = Readonly<{\n /** 1-indexed; root nodes are depth 1. */\n depth: number;\n /** 1-indexed position among this node's own siblings, not the whole flattened tree. */\n position: number;\n siblingCount: number;\n label: string;\n hasChildren: boolean;\n expanded: boolean;\n}>;\n\n/**\n * Products own phrase order and whether/how \"펼쳐짐\"/\"collapsed\" is said for\n * a leaf (`hasChildren` lets the composer skip that clause entirely) — same\n * reason Steps, Timeline, and Carousel do not assemble their own accessible\n * name.\n */\nexport type ComposeTreeAccessibleName = (info: TreeAccessibleNameInfo) => string;\n\nexport type ResolvedTreeNodeDescriptor<Id extends string = string> = Omit<\n TreeNodeDescriptor<Id>,\n \"children\"\n> &\n Readonly<{\n depth: number;\n position: number;\n siblingCount: number;\n parentId: Id | null;\n hasChildren: boolean;\n /** Always false when `hasChildren` is false — a leaf has no expansion state. */\n expanded: boolean;\n /** False when any ancestor is collapsed. A renderer may still choose to keep it mounted-but-hidden. */\n visible: boolean;\n accessibleName: string;\n }>;\n\nexport type ResolveTreeOptions = Readonly<{\n composeAccessibleName: ComposeTreeAccessibleName;\n}>;\n\n/**\n * Flattens the whole tree (not only the visible part) and attaches depth,\n * sibling position, parent linkage, expansion, visibility, and the composed\n * accessible name. Callers filter `.visible` for rendering and for the\n * navigation helpers below — one resolved array is the single source both\n * consult, the same relationship Tabs' resolved panel list has to its mount\n * policy.\n */\nexport function resolveTreeDescriptor<Id extends string>(\n nodes: readonly TreeNodeDescriptor<Id>[],\n expandedKeys: ReadonlySet<Id>,\n options: ResolveTreeOptions,\n): readonly ResolvedTreeNodeDescriptor<Id>[] {\n validateTreeNodes(nodes);\n if (typeof options.composeAccessibleName !== \"function\") {\n throw new TypeError(\"Tree composeAccessibleName must be a function\");\n }\n\n const resolved: ResolvedTreeNodeDescriptor<Id>[] = [];\n const walk = (\n siblings: readonly TreeNodeDescriptor<Id>[],\n depth: number,\n parentId: Id | null,\n parentVisible: boolean,\n ): void => {\n siblings.forEach((node, index) => {\n const hasChildren = node.children !== undefined;\n const expanded = hasChildren && expandedKeys.has(node.id);\n const position = index + 1;\n const siblingCount = siblings.length;\n const accessibleName = options.composeAccessibleName({\n depth,\n position,\n siblingCount,\n label: node.label,\n hasChildren,\n expanded,\n });\n if (typeof accessibleName !== \"string\" || accessibleName.trim().length === 0) {\n throw new TypeError(\n \"Tree composeAccessibleName must return a non-empty string\",\n );\n }\n const { children, ...rest } = node;\n resolved.push({\n ...rest,\n depth,\n position,\n siblingCount,\n parentId,\n hasChildren,\n expanded,\n visible: parentVisible,\n accessibleName,\n });\n if (children) {\n walk(children, depth + 1, node.id, parentVisible && expanded);\n }\n });\n };\n walk(nodes, 1, null, true);\n return resolved;\n}\n\nexport type TreeArrowKeyIntent = \"expand\" | \"collapse\";\n\n/**\n * ArrowRight/ArrowLeft carry opposite meaning in RTL, the same translation\n * `getSelectionNavigationIntent` already applies for CheckboxGroup/RadioGroup\n * orientation. Every other key (Up/Down/Home/End/Typeahead) is a linear-list\n * problem Tree does not re-solve — see getVisibleTreeNodes below.\n */\nexport function getTreeArrowKeyIntent(\n key: WebKeyboardKey,\n direction: SelectionDirection,\n): TreeArrowKeyIntent | undefined {\n if (key === \"ArrowRight\") return direction === \"rtl\" ? \"collapse\" : \"expand\";\n if (key === \"ArrowLeft\") return direction === \"rtl\" ? \"expand\" : \"collapse\";\n return undefined;\n}\n\nexport type TreeArrowResult<Id extends string = string> =\n | Readonly<{ action: \"expand\" | \"collapse\" }>\n | Readonly<{ action: \"moveFocus\"; targetId: Id }>\n | Readonly<{ action: \"none\" }>;\n\n/**\n * Resolves the WAI-ARIA tree pattern for the pressed arrow: expand or move\n * into the first child; collapse or move to the parent. `disabled` never\n * gates this — it only gates selection eligibility (CollectionItemDescriptor\n * precedent), a node's children stay explorable either way.\n */\nexport function getTreeArrowResult<Id extends string>(\n visible: readonly ResolvedTreeNodeDescriptor<Id>[],\n currentId: Id,\n intent: TreeArrowKeyIntent,\n): TreeArrowResult<Id> {\n const node = visible.find((candidate) => candidate.id === currentId);\n if (!node) {\n throw new RangeError(`Tree node must be visible to receive arrow input: ${String(currentId)}`);\n }\n if (intent === \"expand\") {\n if (!node.hasChildren) return { action: \"none\" };\n if (!node.expanded) return { action: \"expand\" };\n const firstChild = visible.find(\n (candidate) => candidate.parentId === node.id && candidate.position === 1,\n );\n return firstChild ? { action: \"moveFocus\", targetId: firstChild.id } : { action: \"none\" };\n }\n if (node.hasChildren && node.expanded) return { action: \"collapse\" };\n if (node.parentId !== null) return { action: \"moveFocus\", targetId: node.parentId };\n return { action: \"none\" };\n}\n\n/**\n * Vertical movement across the currently visible nodes only — a collapsed\n * subtree's descendants are absent from `visible`, so this needs no Tree-\n * specific math at all: `getCollectionNavigationTarget` (disabled-skip, no\n * loop) already does exactly this for Menu/Select, and a resolved,\n * `.visible`-filtered node list satisfies `CollectionItemDescriptor` because\n * it is one, plus extra fields.\n */\nexport function getVisibleTreeNavigationTarget<Id extends string>(\n visible: readonly ResolvedTreeNodeDescriptor<Id>[],\n currentId: Id | null | undefined,\n intent: CollectionNavigationIntent,\n): Id | undefined {\n return getCollectionNavigationTarget({ items: visible }, currentId, intent, false);\n}\n\n/** Same reuse for typeahead: only currently visible nodes are matchable. */\nexport function getVisibleTreeTypeaheadMatch<Id extends string>(\n visible: readonly ResolvedTreeNodeDescriptor<Id>[],\n query: string,\n options?: CollectionTypeaheadOptions<Id>,\n): Id | undefined {\n return getCollectionTypeaheadMatch({ items: visible }, query, options);\n}\n\nexport const treeBehaviorDefaults = {\n loop: false,\n} as const satisfies Readonly<{ loop: boolean }>;\n\nexport const treeBehavior = {\n controlled: [\"expandedKeys\", \"defaultExpandedKeys\", \"onExpandedKeysChange\", \"selection\", \"asyncState\"],\n defaults: treeBehaviorDefaults,\n stateAxes: {\n availability: [\"enabled\", \"disabled\"],\n value: [\"selected\", \"expanded\"],\n content: [\"idle\", \"loading\", \"loadingMore\", \"empty\", \"error\"],\n },\n web: {\n roles: [\"tree\", \"treeitem\", \"group\"],\n keyboard: [\n \"Tab\",\n \"Enter\",\n \"Space\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowRight\",\n \"Home\",\n \"End\",\n \"Typeahead\",\n ],\n focus: \"roving\",\n },\n /** Tree is Web-only per catalog platform classification — no native surface, matching Breadcrumb and Tooltip. */\n native: { roles: [], states: [], actions: [] },\n scenarios: [\n \"depth-and-sibling-position-are-announced-not-only-visual-indent\",\n \"arrow-expands-or-moves-to-the-first-child-arrow-collapses-or-moves-to-the-parent-respecting-rtl\",\n \"up-down-home-end-move-across-visible-nodes-only-collapsed-subtrees-are-skipped\",\n \"typeahead-matches-only-currently-visible-nodes\",\n \"disabled-gates-selection-only-expand-and-collapse-remain-available\",\n \"empty-children-array-is-rejected-a-node-either-omits-children-or-lists-at-least-one\",\n \"one-tab-stop-per-node-the-expand-collapse-glyph-is-decorative-not-a-nested-control\",\n \"drag-reorder-is-out-of-scope-until-a-real-product-need-exists\",\n ],\n} as const satisfies BehaviorContract;\n\nexport const treeRecipe = {\n slots: [\"root\", \"node\", \"toggle\", \"indent\", \"label\", \"description\"] as const,\n indentPerLevel: spacing.lg,\n /** Reuses the same row chrome Menu/Select items already use instead of inventing new selection-row visuals. */\n node: collectionItemContract,\n toggle: {\n size: \"sm\",\n icons: {\n collapsed: \"chevronEnd\",\n expanded: \"chevronDown\",\n },\n color: semanticColors.content.secondary,\n hitTarget: control.minTouchTarget,\n },\n} as const satisfies {\n slots: readonly string[];\n indentPerLevel: number;\n node: typeof collectionItemContract;\n toggle: {\n size: string;\n icons: Readonly<{ collapsed: SemanticIconName; expanded: SemanticIconName }>;\n color: ColorReference;\n hitTarget: number;\n };\n};\n"]}