@repobit/dex-system-design 0.25.12 → 0.26.0

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 (336) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +131 -8
  3. package/package.json +95 -13
  4. package/schemas/component-manifest.json +3294 -405
  5. package/schemas/manifest-omissions.json +173 -0
  6. package/src/blog/src/breakpoints.js +6 -6
  7. package/src/blog/src/content/blog-atoms-showcase.js +1 -1
  8. package/src/blog/src/content/blog-page-layout.js +2 -2
  9. package/src/blog/src/lib/article-toc.js +7 -7
  10. package/src/blog/src/shell/blog-shell.js +1 -1
  11. package/src/blog/src/tokens/blog.js +2 -2
  12. package/src/blog/src/tokens/core.js +34 -35
  13. package/src/blog/src/tokens/legacy.js +0 -9
  14. package/src/blog/tokens/blog.js +2 -2
  15. package/src/blog/tokens/core.js +34 -35
  16. package/src/blog/tokens/legacy.js +0 -9
  17. package/src/blog/tokens.css +36 -46
  18. package/src/components/accordion/accordion-bg.css.js +83 -59
  19. package/src/components/accordion/accordion-bg.js +29 -10
  20. package/src/components/accordion/accordion-bg.stories.js +67 -67
  21. package/src/components/accordion/accordion-keyboard.js +80 -0
  22. package/src/components/accordion/accordion.css.js +142 -80
  23. package/src/components/accordion/accordion.js +37 -10
  24. package/src/components/accordion/accordion.stories.js +7 -7
  25. package/src/components/agent-endpoints/agent-endpoints.css.js +36 -39
  26. package/src/components/agent-endpoints/agent-endpoints.js +30 -20
  27. package/src/components/agent-endpoints/agent-endpoints.stories.js +1 -1
  28. package/src/components/ai-guardian-demo.stories.js +91 -91
  29. package/src/components/alert/alert.css.js +198 -0
  30. package/src/components/alert/alert.js +103 -0
  31. package/src/components/alert/alert.stories.js +206 -0
  32. package/src/components/anchor/anchor-nav.css.js +213 -101
  33. package/src/components/anchor/anchor-nav.js +26 -40
  34. package/src/components/anchor/anchor.stories.js +22 -12
  35. package/src/components/annotation/annotation.css.js +58 -0
  36. package/src/components/annotation/annotation.js +67 -0
  37. package/src/components/annotation/annotation.stories.js +63 -0
  38. package/src/components/architecture.stories.js +285 -0
  39. package/src/components/atoms.stories.js +342 -0
  40. package/src/components/avatar/avatar.css.js +67 -0
  41. package/src/components/avatar/avatar.js +42 -0
  42. package/src/components/avatar/avatar.stories.js +95 -0
  43. package/src/components/awards/awards-flex.css.js +209 -23
  44. package/src/components/awards/awards-flex.js +66 -7
  45. package/src/components/awards/awards-flex.stories.js +1 -1
  46. package/src/components/awards/{awards-light.stories.js → awards-summary.stories.js} +54 -64
  47. package/src/components/awards/awards.css.js +116 -58
  48. package/src/components/awards/awards.js +102 -28
  49. package/src/components/awards/awards.stories.js +1 -1
  50. package/src/components/back/back.css.js +51 -22
  51. package/src/components/back/back.js +28 -7
  52. package/src/components/back/back.stories.js +9 -3
  53. package/src/components/badge/badge.css.js +158 -46
  54. package/src/components/badge/badge.js +62 -74
  55. package/src/components/badge/badge.stories.js +176 -248
  56. package/src/components/bento-grid/bento-grid.css.js +21 -16
  57. package/src/components/bento-grid/bento-grid.js +16 -4
  58. package/src/components/bento-grid/bento-grid.stories.js +19 -18
  59. package/src/components/bento-grid/bento-item.css.js +52 -28
  60. package/src/components/bento-grid/bento-item.js +2 -2
  61. package/src/components/blockquote/blockquote.css.js +47 -0
  62. package/src/components/blockquote/blockquote.js +29 -0
  63. package/src/components/blockquote/blockquote.stories.js +62 -0
  64. package/src/components/breadcrumb/breadcrumb.css.js +92 -22
  65. package/src/components/breadcrumb/breadcrumb.js +56 -11
  66. package/src/components/button/button.css.js +531 -161
  67. package/src/components/button/button.js +206 -214
  68. package/src/components/button/button.stories.js +322 -174
  69. package/src/components/caption/caption.css.js +21 -0
  70. package/src/components/caption/caption.js +31 -0
  71. package/src/components/caption/caption.stories.js +62 -0
  72. package/src/components/card-features/card-features.css.js +14 -22
  73. package/src/components/card-features/card-features.js +68 -9
  74. package/src/components/card-features/card-features.stories.js +11 -11
  75. package/src/components/cards/card.css.js +60 -81
  76. package/src/components/cards/card.js +56 -25
  77. package/src/components/cards/card.stories.js +39 -39
  78. package/src/components/carousel/carousel.css.js +93 -99
  79. package/src/components/carousel/carousel.js +32 -4
  80. package/src/components/checkbox/checkbox.css.js +111 -93
  81. package/src/components/checkbox/checkbox.js +64 -90
  82. package/src/components/checkbox/checkbox.stories.js +96 -29
  83. package/src/components/code/code.css.js +70 -0
  84. package/src/components/code/code.js +40 -0
  85. package/src/components/code/code.stories.js +73 -0
  86. package/src/components/compare/compare.css.js +179 -43
  87. package/src/components/compare/compare.js +52 -25
  88. package/src/components/compare/compare.stories.js +61 -4
  89. package/src/components/comparison-panel/comparison-panel.css.js +38 -51
  90. package/src/components/comparison-panel/comparison-panel.js +5 -4
  91. package/src/components/comparison-panel/comparison-panel.stories.js +1 -1
  92. package/src/components/compatibility-table/compatibility-table.css.js +180 -80
  93. package/src/components/compatibility-table/compatibility-table.js +37 -9
  94. package/src/components/control-alignment.stories.js +393 -0
  95. package/src/components/cta-banner/cta-banner.css.js +41 -13
  96. package/src/components/cta-banner/cta-banner.js +6 -5
  97. package/src/components/cta-banner/cta-banner.stories.js +43 -98
  98. package/src/components/display/display.css.js +25 -1
  99. package/src/components/display/display.js +46 -13
  100. package/src/components/display/display.stories.js +270 -40
  101. package/src/components/divider/divider-horizontal.js +21 -5
  102. package/src/components/divider/divider-vertical.js +15 -7
  103. package/src/components/divider/divider.css.js +2 -2
  104. package/src/components/divider/divider.stories.js +38 -34
  105. package/src/components/dropdown/dropdown.css.js +93 -126
  106. package/src/components/dropdown/dropdown.js +114 -34
  107. package/src/components/dropdown/dropdown.stories.js +56 -24
  108. package/src/components/features/features.css.js +24 -11
  109. package/src/components/features/features.js +7 -3
  110. package/src/components/features/features.stories.js +37 -37
  111. package/src/components/features-tab/features-tab.css.js +19 -8
  112. package/src/components/features-tab/features-tab.js +6 -5
  113. package/src/components/features-tab/features-tab.stories.js +23 -23
  114. package/src/components/fieldset/fieldset.css.js +30 -0
  115. package/src/components/fieldset/fieldset.js +28 -0
  116. package/src/components/fieldset/fieldset.stories.js +84 -0
  117. package/src/components/footer/footer-links-group.css.js +10 -14
  118. package/src/components/footer/footer-links-group.js +4 -3
  119. package/src/components/footer/footer-lp.css.js +59 -30
  120. package/src/components/footer/footer-lp.js +17 -12
  121. package/src/components/footer/footer-lp.stories.js +63 -75
  122. package/src/components/footer/footer-nav-menu.css.js +26 -4
  123. package/src/components/footer/footer-nav-menu.js +3 -2
  124. package/src/components/footer/footer.css.js +146 -264
  125. package/src/components/footer/footer.js +176 -106
  126. package/src/components/footer/footer.stories.js +9 -9
  127. package/src/components/footer/localeMap.js +1 -1
  128. package/src/components/grid/grid.css.js +1 -1
  129. package/src/components/grid/grid.js +49 -7
  130. package/src/components/grid/grid.stories.js +111 -0
  131. package/src/components/header/header-code.css.js +61 -51
  132. package/src/components/header/header-code.js +28 -14
  133. package/src/components/header/header-code.stories.js +5 -5
  134. package/src/components/header/header.css.js +655 -176
  135. package/src/components/header/header.js +269 -36
  136. package/src/components/header/header.stories.js +841 -18
  137. package/src/components/heading/heading.css.js +14 -17
  138. package/src/components/heading/heading.js +48 -33
  139. package/src/components/heading/heading.stories.js +27 -25
  140. package/src/components/help-text/help-text.css.js +21 -0
  141. package/src/components/help-text/help-text.js +25 -0
  142. package/src/components/help-text/help-text.stories.js +66 -0
  143. package/src/components/highlight/highlight.css.js +86 -64
  144. package/src/components/highlight/highlight.js +18 -27
  145. package/src/components/highlight/highlight.stories.js +14 -3
  146. package/src/components/icons/analysis-icon.js +6 -16
  147. package/src/components/icons/arrow-down-icon.js +6 -19
  148. package/src/components/icons/arrow-icon.js +44 -0
  149. package/src/components/icons/arrow-right-icon.js +30 -23
  150. package/src/components/icons/arrow-up-icon.js +19 -18
  151. package/src/components/icons/bd-ai-icon.js +1 -1
  152. package/src/components/icons/brand-marks.stories.js +279 -0
  153. package/src/components/icons/brand-signature-icon.js +42 -0
  154. package/src/components/icons/check-icon.js +27 -0
  155. package/src/components/icons/circle-info-icon.js +33 -0
  156. package/src/components/icons/close-icon.js +6 -18
  157. package/src/components/icons/family-icon.js +8 -20
  158. package/src/components/icons/globe-icon.js +6 -18
  159. package/src/components/icons/guide-icon.js +6 -18
  160. package/src/components/icons/icon-element.js +99 -0
  161. package/src/components/icons/icons.js +36 -0
  162. package/src/components/icons/icons.stories.js +184 -52
  163. package/src/components/icons/individual-icon.js +6 -18
  164. package/src/components/icons/laurel-icon.js +9 -19
  165. package/src/components/icons/machine-learning-icon.js +6 -17
  166. package/src/components/icons/marks.js +41 -0
  167. package/src/components/icons/minus-icon.js +7 -21
  168. package/src/components/icons/missing-icon.js +52 -0
  169. package/src/components/icons/plus-icon.js +7 -21
  170. package/src/components/icons/quote-icon.js +19 -19
  171. package/src/components/image/image.css.js +72 -25
  172. package/src/components/image/image.js +11 -6
  173. package/src/components/image/image.stories.js +2 -2
  174. package/src/components/input/custom-form.stories.js +214 -62
  175. package/src/components/input/input-clipboard.css.js +51 -144
  176. package/src/components/input/input-clipboard.js +31 -15
  177. package/src/components/input/input.css.js +8 -146
  178. package/src/components/input/input.js +93 -38
  179. package/src/components/install-options/install-options.css.js +84 -77
  180. package/src/components/install-options/install-options.js +33 -30
  181. package/src/components/install-options/install-options.stories.js +3 -3
  182. package/src/components/label/label.css.js +35 -0
  183. package/src/components/label/label.js +38 -0
  184. package/src/components/label/label.stories.js +82 -0
  185. package/src/components/light-carousel/light-carousel.css.js +111 -63
  186. package/src/components/light-carousel/light-carousel.js +85 -11
  187. package/src/components/light-carousel/light-carousel.stories.js +126 -82
  188. package/src/components/link/link.css.js +150 -99
  189. package/src/components/link/link.js +86 -11
  190. package/src/components/link/link.stories.js +124 -30
  191. package/src/components/list/list-item/list-item.css.js +74 -22
  192. package/src/components/list/list-item/list-item.js +32 -10
  193. package/src/components/list/list-item/list-item.stories.js +23 -23
  194. package/src/components/list/list.css.js +8 -2
  195. package/src/components/list/list.js +3 -2
  196. package/src/components/list/list.stories.js +222 -0
  197. package/src/components/modal/modal.css.js +114 -24
  198. package/src/components/modal/modal.js +83 -7
  199. package/src/components/modal/modal.stories.js +9 -9
  200. package/src/components/overview/overview.css.js +19 -14
  201. package/src/components/overview/overview.js +6 -5
  202. package/src/components/overview/overview.stories.js +10 -10
  203. package/src/components/page-switcher/page-switcher.css.js +97 -40
  204. package/src/components/page-switcher/page-switcher.js +28 -5
  205. package/src/components/page-switcher/page-switcher.stories.js +95 -0
  206. package/src/components/paragraph/paragraph.css.js +15 -10
  207. package/src/components/paragraph/paragraph.js +41 -44
  208. package/src/components/paragraph/paragraph.stories.js +56 -21
  209. package/src/components/picture/picture.css.js +103 -44
  210. package/src/components/picture/picture.js +100 -30
  211. package/src/components/picture/picture.stories.js +1 -1
  212. package/src/components/pricing-cards/new-pricing-card.js +3 -3
  213. package/src/components/pricing-cards/new-pricing.css.js +24 -15
  214. package/src/components/pricing-cards/pricing-card-actions.css.js +1 -1
  215. package/src/components/pricing-cards/pricing-card-actions.js +3 -4
  216. package/src/components/pricing-cards/pricing-card-container.css.js +3 -3
  217. package/src/components/pricing-cards/pricing-card-container.js +3 -2
  218. package/src/components/pricing-cards/pricing-card-header.css.js +10 -8
  219. package/src/components/pricing-cards/pricing-card-header.js +6 -7
  220. package/src/components/pricing-cards/pricing-card-pricing.css.js +16 -9
  221. package/src/components/pricing-cards/pricing-card-pricing.js +8 -6
  222. package/src/components/pricing-cards/pricing-card-show-more.css.js +11 -3
  223. package/src/components/pricing-cards/pricing-card-show-more.js +8 -3
  224. package/src/components/pricing-cards/pricing-card.css.js +244 -190
  225. package/src/components/pricing-cards/pricing-card.js +54 -70
  226. package/src/components/pricing-cards/pricing-feature-item.css.js +25 -8
  227. package/src/components/pricing-cards/pricing-feature-item.js +8 -3
  228. package/src/components/process-steps/process-steps.css.js +26 -14
  229. package/src/components/process-steps/process-steps.js +23 -6
  230. package/src/components/process-steps/process-steps.stories.js +1 -1
  231. package/src/components/progress/progress.css.js +80 -0
  232. package/src/components/progress/progress.js +42 -0
  233. package/src/components/progress/progress.stories.js +79 -0
  234. package/src/components/quick-answer/quick-answer.css.js +75 -13
  235. package/src/components/quick-answer/quick-answer.js +8 -3
  236. package/src/components/radio/radio.css.js +92 -128
  237. package/src/components/radio/radio.js +106 -127
  238. package/src/components/radio/radio.stories.js +54 -19
  239. package/src/components/review/review-grid.css.js +5 -6
  240. package/src/components/review/review-grid.js +14 -11
  241. package/src/components/review/review.css.js +11 -6
  242. package/src/components/review/review.js +3 -8
  243. package/src/components/shared/ensemble.css.js +93 -0
  244. package/src/components/shared/form-field.css.js +206 -0
  245. package/src/components/shared/prop-vocabulary.js +48 -0
  246. package/src/components/shared/token-scales.js +306 -0
  247. package/src/components/skeleton/skeleton.css.js +34 -0
  248. package/src/components/skeleton/skeleton.js +32 -0
  249. package/src/components/skeleton/skeleton.stories.js +70 -0
  250. package/src/components/spinner/spinner.css.js +62 -0
  251. package/src/components/spinner/spinner.js +38 -0
  252. package/src/components/spinner/spinner.stories.js +92 -0
  253. package/src/components/spotlight-section/spotlight-section.css.js +14 -13
  254. package/src/components/spotlight-section/spotlight-section.js +42 -39
  255. package/src/components/switch/switch.css.js +103 -0
  256. package/src/components/switch/switch.js +44 -0
  257. package/src/components/switch/switch.stories.js +86 -0
  258. package/src/components/tab-list/content-switcher.stories.js +189 -0
  259. package/src/components/tab-list/tab-list.css.js +250 -0
  260. package/src/components/tab-list/tab-list.js +184 -0
  261. package/src/components/tab-list/tab-list.stories.js +221 -0
  262. package/src/components/tabs/tabs.css.js +14 -123
  263. package/src/components/tabs/tabs.js +16 -19
  264. package/src/components/tabs/tabs.stories.js +95 -95
  265. package/src/components/tag/tag.css.js +54 -0
  266. package/src/components/tag/tag.js +32 -0
  267. package/src/components/tag/tag.stories.js +126 -0
  268. package/src/components/terminal/terminal-message.css.js +164 -60
  269. package/src/components/terminal/terminal-message.js +5 -5
  270. package/src/components/terminal/terminal-message.stories.js +70 -1
  271. package/src/components/termsOfUse/terms.css.js +25 -20
  272. package/src/components/termsOfUse/terms.js +13 -9
  273. package/src/components/termsOfUse/terms.stories.js +1 -1
  274. package/src/components/textarea/textarea.css.js +51 -0
  275. package/src/components/textarea/textarea.js +136 -0
  276. package/src/components/textarea/textarea.stories.js +191 -0
  277. package/src/components/tooltip/tooltip.css.js +75 -0
  278. package/src/components/tooltip/tooltip.js +156 -0
  279. package/src/components/tooltip/tooltip.stories.js +161 -0
  280. package/src/components/trust-panel/trust-panel.css.js +85 -33
  281. package/src/components/trust-panel/trust-panel.js +18 -10
  282. package/src/components/trust-panel/trust-panel.stories.js +1 -1
  283. package/src/components/ultimate-security-demo.stories.js +431 -0
  284. package/src/components/video/video.css.js +17 -0
  285. package/src/components/video/video.js +40 -0
  286. package/src/components/video/video.stories.js +106 -0
  287. package/src/components/video-player/video-player.css.js +67 -11
  288. package/src/components/video-player/video-player.js +35 -4
  289. package/src/components/video-player/video-player.stories.js +1 -1
  290. package/src/components/visual-alignment.stories.js +411 -0
  291. package/src/components/visually-hidden/visually-hidden.css.js +24 -0
  292. package/src/components/visually-hidden/visually-hidden.js +16 -0
  293. package/src/components/visually-hidden/visually-hidden.stories.js +56 -0
  294. package/src/stories/Button.stories.js +17 -17
  295. package/src/stories/Header.stories.js +10 -10
  296. package/src/stories/Page.js +1 -1
  297. package/src/stories/Page.stories.js +6 -6
  298. package/src/tokens/borders.stories.js +339 -0
  299. package/src/tokens/color-responsibilities.stories.js +227 -0
  300. package/src/tokens/dark-derivation.js +309 -0
  301. package/src/tokens/dark-findings.js +197 -0
  302. package/src/tokens/dark-mode.stories.js +207 -0
  303. package/src/tokens/dimension.stories.js +251 -0
  304. package/src/tokens/elevation.stories.js +143 -0
  305. package/src/tokens/focus.stories.js +276 -0
  306. package/src/tokens/fonts.css +20 -20
  307. package/src/tokens/fonts.stories.js +354 -374
  308. package/src/tokens/layout.css +25 -11
  309. package/src/tokens/motion.stories.js +231 -0
  310. package/src/tokens/shape.stories.js +272 -0
  311. package/src/tokens/spacing-rhythm.stories.js +359 -0
  312. package/src/tokens/spacing.stories.js +208 -703
  313. package/src/tokens/story-chrome.js +503 -0
  314. package/src/tokens/surfaces.stories.js +224 -0
  315. package/src/tokens/text-on-surface.stories.js +164 -0
  316. package/src/tokens/token-map.js +220 -0
  317. package/src/tokens/token-map.stories.js +257 -0
  318. package/src/tokens/token-purposes.js +266 -0
  319. package/src/tokens/tokens-grid.stories.js +388 -900
  320. package/src/tokens/tokens.css +1633 -1045
  321. package/src/tokens/tokens.js +1130 -492
  322. package/src/tokens/tokens.stories.js +137 -47
  323. package/src/tokens/type-ceiling-words.js +299 -0
  324. package/src/tokens/typography.stories.js +1003 -965
  325. package/src/tokens/vertical-rhythm.stories.js +224 -0
  326. package/src/components/awards/awards-icon.js +0 -56
  327. package/src/components/awards/awards-light.js +0 -240
  328. package/src/components/banner/banner.js +0 -60
  329. package/src/components/banner/banner.stories.js +0 -51
  330. package/src/components/button/icons.js +0 -24
  331. package/src/components/input/index.js +0 -0
  332. package/src/components/light-carousel/light-carousel-simple.css.js +0 -183
  333. package/src/components/light-carousel/light-carousel-simple.js +0 -74
  334. package/src/tokens/colors.js +0 -121
  335. package/src/tokens/typography.css.js +0 -207
  336. package/src/tokens/typography.js +0 -0
@@ -0,0 +1,224 @@
1
+ import { html } from 'lit';
2
+ import { docsPage, refSection, resolve, toHex } from './story-chrome.js';
3
+ /* The role text lives in token-purposes.js, so the Figma variable
4
+ descriptions are generated from the same sentences this page shows. */
5
+ import { BORDERS, SURFACES } from './token-purposes.js';
6
+
7
+ export default {
8
+ title: 'Foundations/Surfaces',
9
+ tags : ['autodocs']
10
+ };
11
+
12
+ const swatch = (token) => {
13
+ const value = resolve(token);
14
+ return html`<span
15
+ style="display:inline-block;width:var(--spacing-40);height:var(--spacing-24);
16
+ border-radius:var(--radius-sm);vertical-align:middle;
17
+ border:var(--border-width-1) solid var(--color-border-subtle);
18
+ background:${value}"
19
+ title="${toHex(value) || value}"
20
+ ></span>`;
21
+ };
22
+
23
+ const roleTable = (rows, kind, caption) => html`
24
+ <div class="table-scroll">
25
+ <table>
26
+ <caption>${caption}</caption>
27
+ <thead>
28
+ <tr>
29
+ <th scope="col">Role</th>
30
+ <th scope="col">Preview</th>
31
+ <th scope="col">Purpose</th>
32
+ <th scope="col">Use when</th>
33
+ <th scope="col">Do not use when</th>
34
+ ${kind === 'surface'
35
+ ? html`<th scope="col">Relationship</th><th scope="col">Theme expectation</th>`
36
+ : ''}
37
+ </tr>
38
+ </thead>
39
+ <tbody>
40
+ ${rows.map(
41
+ (r) => html`
42
+ <tr>
43
+ <td><code>${r.token}</code></td>
44
+ <td>${swatch(r.token)}</td>
45
+ <td>${r.purpose}</td>
46
+ <td>${r.use}</td>
47
+ <td>${r.avoid}</td>
48
+ ${kind === 'surface'
49
+ ? html`<td>${r.relation}</td><td>${r.theme}</td>`
50
+ : ''}
51
+ </tr>`
52
+ )}
53
+ </tbody>
54
+ </table>
55
+ </div>
56
+ `;
57
+
58
+ export const Surfaces = () =>
59
+ docsPage({
60
+ title: 'Surfaces',
61
+ lede : `A surface role says what a background is for. It does not say what colour it is
62
+ today, and choosing one by finding the nearest current value is how the wrong role
63
+ gets picked. Identify the job first; the role then decides the value, and the theme
64
+ decides it again.`,
65
+ note: html`<b>Same value is not same meaning.</b> Several roles resolve to the same
66
+ primitive right now — muted and disabled, for instance. They are different
67
+ questions and will not necessarily stay equal.`,
68
+ children: html`
69
+ ${refSection({
70
+ name : 'Choosing a background',
71
+ meta : 'in order',
72
+ children: html`
73
+ <ol class="docs-list">
74
+ <li>Is it the page, section or component ground? <code>--color-surface-default</code></li>
75
+ <li>Is it a self-contained object — a card, tile or panel? Ask what it sits on first.
76
+ On the page ground, <code>--color-surface-container</code> and no edge. On a
77
+ tinted section, <code>--color-surface-default</code> with a
78
+ <code>--border-width-1 solid --color-border-container</code> edge, because no
79
+ tonal step is available against a tint. See below.</li>
80
+ <li>Is it a low-emphasis wash grouping several things? <code>--color-surface-subtle</code></li>
81
+ <li>Is it a state — pressed, inactive? <code>--color-surface-muted</code>. Held down, where muted is already the hover? <code>--color-surface-pressed</code>. Disabled? <code>--color-surface-disabled</code></li>
82
+ <li>Does content on it need inverse text? <code>--color-surface-inverse</code></li>
83
+ <li>Is it making a brand statement? <code>--color-surface-brand</code></li>
84
+ <li>Does it carry a message? The matching status surface.</li>
85
+ </ol>
86
+ <p>
87
+ If two answers seem to fit, the earlier one usually wins: the ladder runs from the
88
+ page outward, and most surfaces are nearer the page than authors expect.
89
+ </p>`
90
+ })}
91
+
92
+ ${refSection({
93
+ name : 'Surface roles',
94
+ meta : `${SURFACES.length} roles`,
95
+ children: roleTable(SURFACES, 'surface', 'Surface roles, what each is for, and how it should behave under a theme')
96
+ })}
97
+
98
+ ${refSection({
99
+ name : 'Default, container and subtle',
100
+ meta : 'the distinction authors get wrong',
101
+ children: html`
102
+ <p>
103
+ These three are the ones worth being precise about, because they are adjacent and
104
+ the difference is about role rather than appearance.
105
+ </p>
106
+ <ul class="docs-list">
107
+ <li><b>Default</b> is the ground. Things sit <i>on</i> it.</li>
108
+ <li><b>Container</b> is the ground of a thing that sits on the page. A card uses it so
109
+ that it reads as an object without needing a border or a shadow to say so — as
110
+ long as the page is what it sits on.</li>
111
+ <li><b>Subtle</b> is a wash <i>behind</i> a group. It recedes; it is not the ground of
112
+ any single object.</li>
113
+ </ul>
114
+ <p>
115
+ An author who can answer <q>is this an object, or the space behind several objects?</q>
116
+ never needs to look at the values.
117
+ </p>
118
+ <h3 style="font-size: var(--typography-fontSize-base); margin: var(--spacing-32) 0 var(--spacing-8)">
119
+ A card's ground and its edge
120
+ </h3>
121
+ <p>
122
+ Two answers, and which applies depends on what the card sits on. <b>On the page
123
+ ground</b> — <code>--color-surface-container</code>, and no edge: the tonal step is
124
+ the separation, which is what the role is for, and a border there is redundant with
125
+ the role's own purpose. <b>On a tinted section</b> —
126
+ <code>--color-surface-default</code> plus a <code>--border-width-1 solid
127
+ --color-border-container</code> edge, because there is no tonal step available
128
+ against a tint and the edge earns its keep.
129
+ </p>
130
+ <p>
131
+ This was read off the components rather than decided in advance, and it is checkable:
132
+ of the eleven card-shaped objects — the ones
133
+ <code>scripts/card-shapes.json</code> lists, which is now what makes the count
134
+ checkable — six sit on the page ground with no edge, four sit on a
135
+ tinted or inverted ground and draw one, and exactly one does neither. Bento and the
136
+ Carousel item were the two that disagreed, and they were changed rather than the rule.
137
+ The Awards feature card moved the other way when its section gained a blue-50
138
+ band: on the page ground its tonal step was the separation, and on a tint it
139
+ was not — 1.034:1, across a hue change rather than a lightness step — so it
140
+ took the second clause and now reads 1.08:1 with a 1.192:1 edge.
141
+ </p>
142
+ <p>
143
+ <b>The one exception is recorded.</b> The new-pricing card keeps
144
+ <code>--color-surface-container</code> and draws an edge on it, because it has no
145
+ wrapper of its own to carry a band — its container belongs to the page — so inverting
146
+ to white would put a white card on a white page. The edge is the half of the rule that
147
+ is safe on its own, and it triples the delta to 1.233:1. That is the shape an exception
148
+ should have: the rule named, the reason measured, the alternative said out loud.
149
+ </p>
150
+ <p>
151
+ <b>Two qualifications, both measured, and both found by the Pricing card.</b>
152
+ </p>
153
+ <p>
154
+ <b>The step is quieter than the sentence above implies.</b>
155
+ <code>--color-surface-default</code>, <code>-container</code> and <code>-subtle</code>
156
+ measure 1.000, 1.044 and 1.121 against the page — three named grounds inside a 12%
157
+ band. On the page ground the step is real but faint, so an edge there is redundant
158
+ rather than useless. Where a card must be <i>sharp</i> — adjacent cards in a
159
+ comparison set, say — the step alone will not do it, and the answer is the second
160
+ clause, a tinted band with edges, not a stronger border on the first.
161
+ </p>
162
+ <p>
163
+ <b>The second clause breaks on one tint.</b> <code>--color-border-container</code>
164
+ <i>is</i> <code>--color-neutral-100</code>, and so is
165
+ <code>--color-surface-muted</code>. On a muted band the rule's own edge is the same
166
+ colour as the band and disappears at 1.000:1. The card is then defined by its tonal
167
+ step alone, which on that band is the strongest of the three at 1.287:1 — so the
168
+ result is acceptable, but the edge is decoration. On a <code>-container</code>,
169
+ <code>-subtle</code> or status-tinted band the edge reads normally. Pick the band
170
+ before picking the edge.
171
+ </p>
172
+ <p>
173
+ <b>Revisit this when a card needs an edge for a reason the tonal step cannot
174
+ carry</b> — a selected or focused card, a card over an image, a card that must read
175
+ on both grounds. That is a new need rather than a violation, and the rule should grow
176
+ to name it rather than be worked around.
177
+ </p>
178
+ <p>
179
+ <b>Container means a visual container for content.</b> It has nothing to do with CSS
180
+ container queries or any layout API.
181
+ </p>`
182
+ })}
183
+
184
+ ${refSection({
185
+ name : 'Borders, dividers and focus',
186
+ meta : 'three different jobs',
187
+ children: html`
188
+ <ul class="docs-list">
189
+ <li><b>A border</b> bounds an object. It says where a thing ends.</li>
190
+ <li><b>A divider</b> separates regions of content. Reach for
191
+ <code>bd-divider-horizontal</code> when the line is a separator between regions.
192
+ A layout border-top that participates in a grid row is a boundary, not a
193
+ separator, and native CSS is the right tool for it — do not convert one into a
194
+ divider for the sake of reuse.</li>
195
+ <li><b>A focus indicator</b> communicates keyboard focus. It has its own tokens and is
196
+ never a border role. Do not express focus by swapping a border colour.</li>
197
+ <li><b>A status border</b> communicates state and belongs with the status family.</li>
198
+ </ul>
199
+ ${roleTable(BORDERS, 'border', 'Border roles and what each one bounds')}`
200
+ })}
201
+
202
+ ${refSection({
203
+ name : 'Elevation',
204
+ meta : 'purpose, not appearance',
205
+ children: html`
206
+ <p>
207
+ Pick a shadow because of what it means, not because it looks close. The shadow scale
208
+ is a set of effect levels; the meanings below are what should drive the choice.
209
+ </p>
210
+ <ul class="docs-list">
211
+ <li><b>Resting elevation</b> — a surface raised at rest. Rare here: no audited card
212
+ uses one. A container surface is usually the better way to make an object read as
213
+ an object.</li>
214
+ <li><b>Interaction lift</b> — a shadow that appears on hover or drag. It belongs with a
215
+ transform and must be reducible under prefers-reduced-motion.</li>
216
+ <li><b>Overlay</b> — modals, popovers, dropdowns and tooltips, which sit above the page
217
+ and need to look like it.</li>
218
+ <li><b>Inset border effects</b> — an inset shadow used to draw a hairline without
219
+ affecting layout. This is a border technique, not elevation, and most of the
220
+ inset shadows in this package are exactly that.</li>
221
+ </ul>`
222
+ })}
223
+ `
224
+ });
@@ -0,0 +1,164 @@
1
+ import { html } from 'lit';
2
+ import { contrast, docsPage, levelBadge, refSection, resolve, toHex } from './story-chrome.js';
3
+
4
+ export default {
5
+ title: 'Foundations/Text on a surface',
6
+ tags : ['autodocs']
7
+ };
8
+
9
+ /* The grounds text actually lands on in this package. Two families name some of
10
+ the same values -- the surface roles and the status roles -- which is exactly
11
+ why a pairing can look considered and still have been measured against
12
+ nothing. */
13
+ const GROUNDS = [
14
+ { token: '--color-surface-default', note: 'the page ground' },
15
+ { token: '--color-surface-container', note: 'a card or tile' },
16
+ { token: '--color-surface-subtle', note: 'a wash behind a group' },
17
+ { token: '--status-brand-surface', note: 'the brand tint, and the same value as surface-info' },
18
+ { token: '--status-success-surface', note: 'the success tint' },
19
+ { token: '--status-warning-surface', note: 'the warning tint' },
20
+ { token: '--status-error-surface', note: 'the error tint' }
21
+ ];
22
+
23
+ /* Roles that are legitimately used for text somewhere in the package. */
24
+ const INKS = [
25
+ '--color-text-primary',
26
+ '--color-text-secondary',
27
+ '--color-text-muted',
28
+ '--color-text-subtle',
29
+ '--color-text-brand',
30
+ '--status-brand-text',
31
+ '--color-action-accent'
32
+ ];
33
+
34
+ const cell = (ink, ground) => {
35
+ const ratio = contrast(resolve(ink), resolve(ground));
36
+ const rounded = Math.round(ratio * 100) / 100;
37
+ return html`
38
+ <td style="background:${resolve(ground)};color:${resolve(ink)};padding:var(--spacing-8)">
39
+ <span style="font-size:var(--typography-fontSize-xs)">Aa ${rounded}</span>
40
+ ${levelBadge(ratio)}
41
+ </td>`;
42
+ };
43
+
44
+ export const TextOnSurface = () =>
45
+ docsPage({
46
+ title: 'Text on a surface',
47
+ lede : `A text role is only legible against a particular ground, and the ground is not
48
+ always the one it was measured on. Every pairing below is measured live from
49
+ the tokens, so this page cannot drift from them.`,
50
+ note: html`<b>A pass on white is not a pass anywhere.</b>
51
+ <code>--color-text-subtle</code> reaches 4.5:1 on the page ground and falls
52
+ under it on every tint in this table. That is the same trap
53
+ <code>--color-blue-500</code> already carries a warning for, and it is how
54
+ three components ended up below AA with tokens that looked deliberate.`,
55
+ children: html`
56
+ ${refSection({
57
+ name : 'Every role against every ground',
58
+ meta : `${INKS.length} roles x ${GROUNDS.length} grounds`,
59
+ children: html`
60
+ <p>
61
+ The badge is the level the pair reaches for <b>normal-size body text</b>.
62
+ <code>18pt</code> means it only clears the lower bar that large text and
63
+ non-text UI are held to — at body size it fails.
64
+ </p>
65
+ <div class="table-scroll">
66
+ <table>
67
+ <caption>Contrast of each text role against each surface it may sit on</caption>
68
+ <thead>
69
+ <tr>
70
+ <th scope="col">Text role</th>
71
+ ${GROUNDS.map(g => html`<th scope="col"><code>${g.token.replace('--color-', '').replace('--status-', '')}</code></th>`)}
72
+ </tr>
73
+ </thead>
74
+ <tbody>
75
+ ${INKS.map(ink => html`
76
+ <tr>
77
+ <td><code>${ink}</code></td>
78
+ ${GROUNDS.map(g => cell(ink, g.token))}
79
+ </tr>`)}
80
+ </tbody>
81
+ </table>
82
+ </div>`
83
+ })}
84
+
85
+ ${refSection({
86
+ name : 'The grounds',
87
+ meta : 'what each one is for',
88
+ children: html`
89
+ <div class="table-scroll">
90
+ <table>
91
+ <caption>Surface tokens used as a text ground, and their current values</caption>
92
+ <thead>
93
+ <tr><th scope="col">Token</th><th scope="col">Value</th><th scope="col">What it is</th></tr>
94
+ </thead>
95
+ <tbody>
96
+ ${GROUNDS.map(g => html`
97
+ <tr>
98
+ <td><code>${g.token}</code></td>
99
+ <td class="num">${toHex(resolve(g.token))}</td>
100
+ <td>${g.note}</td>
101
+ </tr>`)}
102
+ </tbody>
103
+ </table>
104
+ </div>`
105
+ })}
106
+
107
+ ${refSection({
108
+ name : 'Choosing',
109
+ meta : 'in order',
110
+ children: html`
111
+ <ol class="docs-list">
112
+ <li>Decide what the text <i>is</i> — body, supporting, a label, a link, a status
113
+ message. That picks the family.</li>
114
+ <li>Find the ground it will actually sit on, which is often a tint rather than
115
+ the page.</li>
116
+ <li>Read the pair off this table. If it does not reach
117
+ <code>AA</code> at the size you are using, take the next step up in the
118
+ same family rather than a colour from another one.</li>
119
+ </ol>
120
+ <p>
121
+ Supporting metadata that failed on a tint went from
122
+ <code>--color-text-subtle</code> to <code>--color-text-muted</code>, and a
123
+ brand-coloured eyebrow went from a lighter blue to
124
+ <code>--color-text-brand</code>. Both are one step inside the family that
125
+ already described the text; neither needed a new token.
126
+ </p>`
127
+ })}
128
+ ${refSection({
129
+ name : 'The inverse side',
130
+ meta : 'two steps, not a mirror of the light hierarchy',
131
+ children: html`
132
+ <p>
133
+ The table above is the light side. On an inverse surface the same hierarchy does
134
+ not exist and cannot be borrowed: <code>--color-text-secondary</code>,
135
+ <code>-muted</code> and <code>-subtle</code> measure 1.91, 2.73 and 3.85 against
136
+ <code>--color-surface-inverse</code>, so every one of them is unusable there.
137
+ </p>
138
+ <p>
139
+ There are exactly two inverse text roles.
140
+ <code>--color-text-inverse</code> is high emphasis, and
141
+ <code>--color-text-muted-inverse</code> is everything subordinate to it. Two
142
+ components had built the second one separately -- one with a white alpha, one with
143
+ a neutral step -- before it was named.
144
+ </p>
145
+ <div style="background: var(--color-surface-inverse); padding: var(--spacing-24); border-radius: var(--radius-lg); max-width: 52ch">
146
+ <p style="margin: 0 0 var(--spacing-8); color: var(--color-text-inverse)">
147
+ High emphasis on an inverse surface — 17.97:1
148
+ </p>
149
+ <p style="margin: 0; color: var(--color-text-muted-inverse); font-size: var(--typography-fontSize-sm)">
150
+ Subordinate on an inverse surface — 7.92:1
151
+ </p>
152
+ </div>
153
+ <p>
154
+ <b>An inverse ground is not always flat, and arithmetic is not measurement.</b> A
155
+ gradient or an image has a lightest point, and that point is the one that decides —
156
+ but which point the text actually sits on is a question about layout, not about the
157
+ stops. bd-cta-banner sets white at 60% opacity over a blue gradient, and calculating
158
+ against its lightest stop predicts 2.13:1. Sampling the pixels behind the rendered
159
+ text gives 4.46:1, because the radial highlight sits at <code>78% -10%</code> and is
160
+ mostly outside the banner. Sample the ground; do not infer it.
161
+ </p>`
162
+ })}
163
+ `
164
+ });
@@ -0,0 +1,220 @@
1
+ /**
2
+ * The analysis behind Foundations/Token Map, kept free of Lit and the DOM so
3
+ * the same functions run in Storybook and under Node: the page renders what
4
+ * this returns, and nothing on the page is transcribed by hand.
5
+ *
6
+ * Inputs are text, not live objects: the generated tokens.css, the tier
7
+ * registry in scripts/token-tiers.json, and the raw source of every file that
8
+ * might read a token. Component CSS sits inside shadow roots, which is why the
9
+ * other Foundations pages can only count token-to-token references; reading the
10
+ * source is what lets this page say which component reads a token.
11
+ *
12
+ * Not a story file: Storybook only globs *.stories.js.
13
+ */
14
+
15
+ /** Blank comments, keeping offsets, so commented-out CSS is never read. */
16
+ export const blankComments = text =>
17
+ text.replace(/\/\*[\s\S]*?\*\//g, c => c.replace(/[^\n]/g, ' '));
18
+
19
+ /** name -> raw declared value, in declaration order. */
20
+ export function declarationsFrom(cssText) {
21
+ const out = new Map();
22
+ for (const m of blankComments(cssText).matchAll(/(?:^|[\s;{])(--[A-Za-z0-9-]+)\s*:\s*([^;]*);/g)) {
23
+ if (!out.has(m[1])) out.set(m[1], m[2].replace(/\s+/g, ' ').trim());
24
+ }
25
+ return out;
26
+ }
27
+
28
+ /** Every custom property a value refers to. */
29
+ export const refsOf = value => [...value.matchAll(/var\(\s*(--[A-Za-z0-9-]+)/g)].map(m => m[1]);
30
+
31
+ /** The family a token belongs to: the longest matching registry prefix. */
32
+ export function familyOf(token, families) {
33
+ let best = null;
34
+ for (const f of families) {
35
+ if (token.startsWith(f.prefix) && (!best || f.prefix.length > best.prefix.length)) best = f;
36
+ }
37
+ return best;
38
+ }
39
+
40
+ /**
41
+ * The alias chain from a token down to its value. Follows the reference while
42
+ * the value is exactly one var(); a value that combines references
43
+ * (color-mix, a shorthand, a clamp) ends the chain and is reported as compound.
44
+ */
45
+ export function chainOf(token, decls) {
46
+ const hops = [token];
47
+ let value = decls.get(token);
48
+ while (value !== undefined) {
49
+ const only = value.match(/^var\(\s*(--[A-Za-z0-9-]+)\s*\)$/);
50
+ if (!only || !decls.has(only[1]) || hops.includes(only[1])) break;
51
+ hops.push(only[1]);
52
+ value = decls.get(only[1]);
53
+ }
54
+ const compound = value !== undefined && refsOf(value).length > 0;
55
+ return { hops, value: value ?? '', compound };
56
+ }
57
+
58
+ /**
59
+ * Who reads each token, from raw source text keyed by path.
60
+ *
61
+ * Three ways a token is read, and all three are real in this package:
62
+ * var(--x) the ordinary case
63
+ * '--x' a name held as data -- token-scales.js maps
64
+ * color="muted" to '--color-text-muted' this way
65
+ * '--spacing-' a prefix the scale helpers complete at runtime, so a
66
+ * step may be read without its full name appearing
67
+ * The third is reported separately: it says a token can be reached, not that
68
+ * anything reaches it.
69
+ */
70
+ export function consumersFrom(sources, ownerOf) {
71
+ const readers = new Map();
72
+ const prefixes = new Map();
73
+ const add = (map, key, owner) => {
74
+ if (!map.has(key)) map.set(key, new Set());
75
+ map.get(key).add(owner);
76
+ };
77
+ for (const [path, raw] of Object.entries(sources)) {
78
+ const text = blankComments(raw);
79
+ const owner = ownerOf(path);
80
+ for (const m of text.matchAll(/var\(\s*(--[A-Za-z0-9-]+)/g)) add(readers, m[1], owner);
81
+ for (const m of text.matchAll(/['"`](--[A-Za-z0-9-]*[A-Za-z0-9])['"`]/g)) add(readers, m[1], owner);
82
+ /* documentation looks tokens up by prefix to list them, which is not a
83
+ product being able to reach one */
84
+ if (owner === DOCS) continue;
85
+ for (const m of text.matchAll(/['"`](--[A-Za-z0-9-]+-)['"`$]/g)) add(prefixes, m[1], owner);
86
+ for (const m of text.matchAll(/var\(\s*(--[A-Za-z0-9-]+-)\$\{/g)) add(prefixes, m[1], owner);
87
+ }
88
+ return { readers, prefixes };
89
+ }
90
+
91
+ const TIER_RANK = { primitive: 0, semantic: 1, component: 2 };
92
+
93
+ /** The owner name ownerOf() should return for documentation sources. */
94
+ export const DOCS = 'stories';
95
+
96
+ /**
97
+ * Everything the page shows. `sources` is { path: text }; `ownerOf` turns a
98
+ * path into the name shown in "read by".
99
+ */
100
+ export function analyse({ cssText, registry, sources, ownerOf }) {
101
+ const decls = declarationsFrom(cssText);
102
+ const families = registry.families;
103
+ const { readers, prefixes } = consumersFrom(sources, ownerOf);
104
+
105
+ const byToken = new Map();
106
+ for (const [name, value] of decls) {
107
+ const family = familyOf(name, families);
108
+ const chain = chainOf(name, decls);
109
+ const tokenReaders = new Set();
110
+ for (const [other, v] of decls) if (other !== name && refsOf(v).includes(name)) tokenReaders.add(other);
111
+ const reachable = new Set();
112
+ for (const [prefix, owners] of prefixes) if (name.startsWith(prefix)) owners.forEach(o => reachable.add(o));
113
+ byToken.set(name, {
114
+ name,
115
+ value,
116
+ family,
117
+ tier : family?.tier ?? 'unassigned',
118
+ chain,
119
+ tokenReaders,
120
+ sourceReaders: readers.get(name) ?? new Set(),
121
+ reachable
122
+ });
123
+ }
124
+
125
+ const rows = [...byToken.values()];
126
+ const roleOf = name => name.replace(/-[A-Za-z]+$/, '');
127
+
128
+ /* Tier skips: a component token whose first hop is a primitive. Grouped by
129
+ the primitive family reached, because the answer differs by family: a
130
+ colour primitive has semantic roles above it that the token could have
131
+ asked for, and --spacing-* has none -- its roles were deleted on purpose.
132
+ Inversions: a token whose first hop is a HIGHER tier than itself. */
133
+ const tierSkips = new Map();
134
+ const inversions = [];
135
+ for (const r of rows) {
136
+ const firstHop = r.chain.hops[1] ?? refsOf(r.value)[0];
137
+ const target = firstHop && byToken.get(firstHop);
138
+ if (!target) continue;
139
+ if (r.tier === 'component' && target.tier === 'primitive') {
140
+ const key = target.family.prefix;
141
+ if (!tierSkips.has(key)) tierSkips.set(key, []);
142
+ tierSkips.get(key).push({ row: r, target });
143
+ }
144
+ if (TIER_RANK[target.tier] > TIER_RANK[r.tier]) inversions.push({ row: r, target });
145
+ }
146
+ /* For each skipped-to family, the semantic tokens that do sit above it --
147
+ the roles a skipping token could have asked for instead. A count, not a
148
+ verdict: --spacing-* has two (the card roles), and that is not a spacing
149
+ vocabulary. */
150
+ const rolesAbove = prefix => rows.filter(r => r.tier === 'semantic' &&
151
+ r.chain.hops.slice(1).some(h => byToken.get(h)?.family?.prefix === prefix));
152
+ const skips = [...tierSkips].map(([prefix, list]) => ({ prefix, list, rolesAbove: rolesAbove(prefix) }))
153
+ .sort((a, b) => b.list.length - a.list.length);
154
+
155
+ /* Unread, split by what it means:
156
+ unread semantic or component, and nothing reads it -- the
157
+ case that retired display-hero and two status tokens
158
+ docsOnly read by a story and nothing else
159
+ unreadAxes one axis of a typography role whose other axes are
160
+ read; a role declares five and a consumer may want two
161
+ reachableOnly nothing names it, but a scale helper can build its name
162
+ unreadSteps a primitive step nothing reads; a scale is allowed to
163
+ be complete */
164
+ const unread = [];
165
+ const docsOnly = [];
166
+ const unreadAxes = [];
167
+ const reachableOnly = [];
168
+ const unreadSteps = [];
169
+ const productRead = r => r.tokenReaders.size > 0 || [...r.sourceReaders].some(o => o !== DOCS);
170
+ for (const r of rows) {
171
+ if (productRead(r)) continue;
172
+ if (r.reachable.size) { reachableOnly.push(r); continue; }
173
+ if (r.sourceReaders.has(DOCS)) { docsOnly.push(r); continue; }
174
+ if (r.tier === 'primitive') { unreadSteps.push(r); continue; }
175
+ const isRoleAxis = r.tier === 'semantic' && r.name.startsWith('--typography-');
176
+ const siblingRead = isRoleAxis && rows.some(o => o !== r && roleOf(o.name) === roleOf(r.name) && productRead(o));
177
+ (siblingRead ? unreadAxes : unread).push(r);
178
+ }
179
+
180
+ /* semantic tokens declared as a literal: no primitive beneath them */
181
+ const literalSemantics = rows.filter(r => r.tier === 'semantic' && refsOf(r.value).length === 0);
182
+
183
+ /* Same colour, two semantic names in one family, differing in exactly one
184
+ segment. Split by WHICH segment differs:
185
+ sameSlot a middle segment -- --status-brand-surface and
186
+ --status-info-surface: one slot, two tones, one colour. This
187
+ is the kind that collided twice.
188
+ sameRole the last segment -- --color-surface-muted and -disabled: two
189
+ jobs that happen to share a value, which CLAUDE.md says is
190
+ expected and must not drive a choice. Listed, not flagged. */
191
+ const slots = new Map();
192
+ const sameRole = [];
193
+ const colour = rows.filter(r => r.tier === 'semantic' && !r.chain.compound &&
194
+ byToken.get(r.chain.hops.at(-1))?.family?.prefix?.startsWith('--color-') &&
195
+ byToken.get(r.chain.hops.at(-1))?.tier === 'primitive');
196
+ const segs = n => n.slice(2).split('-');
197
+ for (let i = 0; i < colour.length; i++) {
198
+ for (let j = i + 1; j < colour.length; j++) {
199
+ const a = colour[i], b = colour[j];
200
+ if (a.family !== b.family || a.chain.hops.at(-1) !== b.chain.hops.at(-1)) continue;
201
+ const sa = segs(a.name), sb = segs(b.name);
202
+ if (sa.length !== sb.length) continue;
203
+ const diff = sa.map((s, k) => s !== sb[k]);
204
+ if (diff.filter(Boolean).length !== 1) continue;
205
+ if (diff.at(-1)) { sameRole.push({ a, b, primitive: a.chain.hops.at(-1) }); continue; }
206
+ /* one line per slot: --status-*-surface -> blue-50: brand, info */
207
+ const k = diff.indexOf(true);
208
+ const pattern = `--${sa.map((s, n) => (n === k ? '*' : s)).join('-')}`;
209
+ const key = `${pattern}|${a.chain.hops.at(-1)}`;
210
+ if (!slots.has(key)) slots.set(key, { pattern, primitive: a.chain.hops.at(-1), members: new Set() });
211
+ slots.get(key).members.add(sa[k]).add(sb[k]);
212
+ }
213
+ }
214
+ const sameSlot = [...slots.values()].map(g => ({ ...g, members: [...g.members] }));
215
+
216
+ return {
217
+ rows, decls,
218
+ flags: { tierSkips: skips, inversions, unread, docsOnly, unreadAxes, reachableOnly, unreadSteps, literalSemantics, sameSlot, sameRole }
219
+ };
220
+ }