@repobit/dex-system-design 0.25.11 → 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 +195 -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,197 @@
1
+ /**
2
+ * The findings of the 🔬 Dark mode study, as data, each with a check that runs
3
+ * against the code. The numbers are the Figma page's, so the two can be read
4
+ * side by side; 6 and 7 are observations rather than findings, below.
5
+ *
6
+ * What a finding SAYS -- its status, its wording, which commit fixed it -- is a
7
+ * record of a decision, and is written here by hand. What it CLAIMS about the
8
+ * code is checked: a fixed finding verifies the fix is still in place, an open
9
+ * one verifies it still fails. When a check disagrees with the status, the page
10
+ * says so, which is how a reverted fix or a quietly-resolved finding shows up
11
+ * instead of lying in prose.
12
+ *
13
+ * A check receives { decl, src, row }: the token declarations, the raw source
14
+ * of the component stylesheets it names, and a lookup into the derived dark
15
+ * values. It returns { holds, detail } -- holds is whether the status is still
16
+ * true. Not a story file. Pure, so it runs under Node.
17
+ */
18
+
19
+ import { contrast } from './dark-derivation.js';
20
+
21
+ const has = (s, needle) => (s || '').includes(needle);
22
+ const reads = (decl, token, role) => (decl.get(token) || '').trim() === `var(${role})`;
23
+ const r2 = n => (n == null ? '—' : n.toFixed(2));
24
+
25
+ export const FINDINGS = [
26
+ {
27
+ n : 1, status : 'fixed', commit : '591c695a', kind : 'role',
28
+ title : 'Button labels borrowed text-inverse',
29
+ summary: 'Six filled-button labels read --color-text-inverse, which a dark theme flips to near-black on the fill. They read --color-action-on-fill now: white in both themes, 4.53 to 10.51:1.',
30
+ files : [],
31
+ check : ({ decl }) => {
32
+ const t = ['primary-text-default', 'accent-text-default', 'secondary-text-default', 'tertiary-text-active', 'ghost-text-active', 'outline-white-text-default'].map(x => `--button-${x}`);
33
+ const off = t.filter(x => !reads(decl, x, '--color-action-on-fill'));
34
+ return { holds: !off.length, detail: off.length ? `still borrowing: ${off.join(', ')}` : 'all six label tokens read --color-action-on-fill' };
35
+ }
36
+ },
37
+ {
38
+ n : 2, status : 'fixed', kind : 'palette',
39
+ title : 'The dark end of the neutral ramp was too coarse',
40
+ summary: 'Light has four grounds within 1.29:1 of the page; dark had neutral-950, then 900, then 800, so container and subtle landed on one step. neutral-925 and 850 sit at the light steps, 1.04 and 1.12 off the dark page.',
41
+ check : ({ row }) => {
42
+ const g = ['--color-surface-container', '--color-surface-subtle', '--color-surface-muted'].map(row);
43
+ const apart = new Set(g.map(r => r.dark)).size === g.length;
44
+ return { holds: apart, detail: g.map(r => `${r.token.slice(16)} ${r.dark}`).join(', ') + (apart ? '' : ': two share a step again') };
45
+ }
46
+ },
47
+ {
48
+ n : 3, status : 'fixed', kind : 'palette',
49
+ title : 'text-subtle had no dark step that kept both its jobs',
50
+ summary: 'It must stay quieter than text-muted and clear 4.5:1, and no neutral step did both. neutral-450 does: 4.93:1 on the dark page and 4.75 on a dark card, 1.22:1 from muted.',
51
+ check : ({ row }) => {
52
+ const r = row('--color-text-subtle'), m = row('--color-text-muted');
53
+ const ok = r.ok === true && r.dark !== m.dark;
54
+ return { holds: ok, detail: `derives to ${r.dark}, ${r2(r.darkRatio)}:1 on the dark page; muted is ${m.dark}` };
55
+ }
56
+ },
57
+ {
58
+ n : 4, status : 'decided', kind : 'design',
59
+ title : 'The action ramp darkens in dark too, and the page bar is for the resting fill',
60
+ summary: 'On the dark page, label-on-fill times fill-on-page is 19.45 for every fill, so a white label at 4.5:1 and a fill at 3:1 leave a band 1.44:1 wide -- accent and secondary have one step in it, so no ramp keeps both bars in three states. Decided: hover and press darken as in light; every state keeps its white label at 4.5:1; a resting fill keeps 3:1 on the page by choice; hover and press are supplemental, as WCAG 1.4.11 says, and a text button needs no boundary at all.',
61
+ check : ({ row }) => {
62
+ const t = ['primary', 'accent', 'secondary'].flatMap(k => ['', '-hover', '-active'].map(s => `--color-action-${k}${s}`)).map(row)
63
+ .filter(r => r.token);
64
+ const label = t.filter(r => r.darkRatio < 4.5), rest = t.filter(r => !r.state && r.groundRatio < 3);
65
+ const ok = t.length === 9 && !label.length && !rest.length;
66
+ return { holds : ok, detail: ok
67
+ ? `every label clears 4.5 (weakest ${r2(Math.min(...t.map(r => r.darkRatio)))}:1); resting fills ${t.filter(r => !r.state).map(r => r2(r.groundRatio))
68
+ .join(', ')}:1 on the page`
69
+ : [label.length && `label under 4.5: ${label.map(r => r.token.slice(15)).join(', ')}`, rest.length && `resting fill under 3: ${rest.map(r => r.token.slice(15)).join(', ')}`, t.length !== 9 && `${t.length} of 9 fill rows`].filter(Boolean).join(' · ') };
70
+ }
71
+ },
72
+ {
73
+ n : 5, status : 'fixed', kind : 'palette',
74
+ title : 'Success and warning tints ran three times heavier than in light',
75
+ summary: 'green-950 and yellow-950 are not near-black, so those tints sat about 1.55:1 off the dark page against 1.05 in light, and read as coloured bands on an alert. green-975 and yellow-975 sit at 1.04 and 1.03.',
76
+ check : ({ row }) => {
77
+ const g = row('--status-success-surface'), y = row('--status-warning-surface');
78
+ const light = g.darkRatio / g.lightRatio <= 1.3 && y.darkRatio / y.lightRatio <= 1.3;
79
+ return { holds: light, detail: `success ${r2(g.lightRatio)} → ${r2(g.darkRatio)}, warning ${r2(y.lightRatio)} → ${r2(y.darkRatio)} off the page` };
80
+ }
81
+ },
82
+ {
83
+ n : 8, status : 'fixed', kind : 'primitive',
84
+ title : 'Secondary and pressed ghost buttons read primitives, and vanished on a dark page',
85
+ summary: 'Their fills were neutral-950/800/1000 from the palette, which has no dark value, and the dark page is neutral-950 too. They read --color-action-secondary now. The white preset mixed a fixed fill with a switching label; its label is a fixed blue-600 now, like its fill.',
86
+ check : ({ decl }) => {
87
+ const t = ['--button-secondary-background-default', '--button-secondary-background-hover', '--button-secondary-background-active', '--button-ghost-background-active'];
88
+ const prim = t.filter(x => !/var\(--color-action-secondary(-hover|-active)?\)/.test(decl.get(x) || ''));
89
+ const white = reads(decl, '--button-white-text-default', '--color-blue-600');
90
+ const ok = !prim.length && white;
91
+ return { holds: ok, detail: ok ? 'four fills read --color-action-secondary; the white preset label is fixed' : [prim.length && `not on the role: ${prim.join(', ')}`, !white && 'the white preset label switches again'].filter(Boolean).join(' · ') };
92
+ }
93
+ },
94
+ {
95
+ n : 9, status : 'fixed', commit : '415fc57c', kind : 'light-bug',
96
+ title : 'outline-white lost AA on hover and press, in light',
97
+ summary: 'Its white wash lightened the blue under a white label (3.91 and 3.38:1). It darkens now, and the label passes on every ground the preset sits on.',
98
+ check : ({ decl }) => {
99
+ const ok = ['--button-outline-white-background-hover', '--button-outline-white-background-active'].every(t => has(decl.get(t), '--color-neutral-1000'));
100
+ return { holds: ok, detail: ok ? 'hover and press wash with neutral-1000' : 'a white wash is back' };
101
+ }
102
+ },
103
+ {
104
+ n : 10, status : 'fixed', commit : '591c695a', kind : 'primitive',
105
+ title : 'The required mark read a primitive',
106
+ summary: 'It was red-600 from the palette, 3.57:1 in dark. It reads --status-error-accent now, like the error help text.',
107
+ check : ({ decl }) => { const ok = reads(decl, '--form-label-required-color', '--status-error-accent'); return { holds: ok, detail: ok ? '--form-label-required-color reads --status-error-accent' : `reads ${decl.get('--form-label-required-color')}` }; }
108
+ },
109
+ {
110
+ n : 11, status : 'fixed', commit : '415fc57c', kind : 'light-bug',
111
+ title : 'Placeholders failed on the error and success tints, in light',
112
+ summary: 'Every placeholder was text-subtle, 4.22 and 4.43:1 on the tints. Error and success fields take text-muted now. The plain field\'s dark placeholder is finding 3.',
113
+ files : ['form-field'],
114
+ check : ({ src }) => { const ok = has(src['form-field'], '.form-input--error::placeholder'); return { holds: ok, detail: ok ? 'form-field.css.js sets tinted placeholders to text-muted' : 'the tinted-placeholder rule is gone' }; }
115
+ },
116
+ {
117
+ n : 12, status : 'fixed', commit : '591c695a', kind : 'role',
118
+ title : 'The checkmark, radio dot and switch thumb read the page colour',
119
+ summary: 'They were --color-surface-default, which is "the page", not "white", and turns near-black on blue in dark. They read --color-action-on-fill now.',
120
+ files : ['checkbox', 'radio', 'switch'],
121
+ check : ({ src }) => { const off = ['checkbox', 'radio', 'switch'].filter(k => !has(src[k], 'var(--color-action-on-fill)')); return { holds: !off.length, detail: off.length ? `not on action-on-fill: ${off.join(', ')}` : 'all three read --color-action-on-fill' }; }
122
+ },
123
+ {
124
+ n : 13, status : 'fixed', commit : '591c695a', kind : 'role',
125
+ title : 'A link\'s hover borrowed a button fill as text',
126
+ summary: 'Primary and bold hovers read --color-action-primary-active; they read --color-text-brand-hover now. The subtle link at rest failed in dark too, until finding 3 gave text-subtle a dark step.',
127
+ files : ['link'],
128
+ check : ({ src }) => { const ok = !has(src.link, 'action-primary-active') && has(src.link, 'var(--color-text-brand-hover)'); return { holds: ok, detail: ok ? 'link hovers read --color-text-brand-hover' : 'a hover reads an action fill again' }; }
129
+ },
130
+ {
131
+ n : 14, status : 'decided', kind : 'primitive',
132
+ title : 'Back and Avatar read primitives, and failed in dark',
133
+ summary: 'Back\'s pressed ground was neutral-200 under a label that switches, 1.48:1 in dark; it reads --color-surface-pressed now. Avatar\'s disc was blue-100 from the palette, 2.68:1 in dark under its initials. Decided: the disc reads --status-brand-surface, chosen for meaning (brand identity), not for value -- the only blue-100 role is info\'s, and an avatar is not info. The initials read 5.72:1 in light and 5.10 in dark; the disc is fainter on the page (1.08:1), and the initials, not the edge, identify it.',
134
+ files : ['back', 'avatar'],
135
+ check : ({ src }) => {
136
+ const b = has(src.back, 'var(--color-surface-pressed)'), a = has(src.avatar, 'var(--status-brand-surface)') && !has(src.avatar, 'var(--color-blue-100)');
137
+ return { holds: b && a, detail: [b ? 'Back reads --color-surface-pressed' : 'Back reads a primitive again', a ? 'Avatar reads --status-brand-surface' : 'Avatar reads a primitive again'].join(' · ') };
138
+ }
139
+ },
140
+ {
141
+ n : 15, status : 'decided', kind : 'design',
142
+ title : 'Video and terminal frames used surface-inverse to mean "dark"',
143
+ summary: 'Inverse means the other polarity, so in dark the frame turned light and its ink dark: a play icon at 3.13:1, and a terminal palette (blue-400, the traffic-light green) that only works on a dark ground. Decided: --color-surface-media, dark in every theme, with --color-text-on-media and -muted fixed alongside it. The tooltip stays on surface-inverse: a bubble that flips is what inverse means.',
144
+ files : ['video-player', 'video', 'terminal-message', 'tooltip'],
145
+ check : ({ src }) => {
146
+ const frames = ['video-player', 'video', 'terminal-message'];
147
+ const off = frames.filter(k => !has(src[k], 'var(--color-surface-media)') || has(src[k], 'var(--color-surface-inverse)') || has(src[k], 'var(--color-text-inverse)'));
148
+ const tip = has(src.tooltip, 'var(--color-surface-inverse)');
149
+ const ok = !off.length && tip;
150
+ return { holds: ok, detail: ok ? 'video, bd-video and the terminal read surface-media and its ink; the tooltip stays inverse' : [off.length && `still on inverse: ${off.join(', ')}`, !tip && 'the tooltip left surface-inverse'].filter(Boolean).join(' · ') };
151
+ }
152
+ },
153
+ {
154
+ n : 16, status : 'fixed', commit : '591c695a', kind : 'primitive',
155
+ title : 'The citation reference read the palette',
156
+ summary: 'blue-600 and blue-700 from the ramp, 3.15 and 2.27:1 in dark. It reads --color-text-brand and --color-text-brand-hover now.',
157
+ check : ({ decl }) => { const ok = reads(decl, '--citation-reference-color', '--color-text-brand') && reads(decl, '--citation-reference-hover-color', '--color-text-brand-hover'); return { holds: ok, detail: ok ? 'both citation tokens read brand text roles' : 'a citation token reads a primitive again' }; }
158
+ },
159
+ {
160
+ n : 17, status : 'decided', commit : '415fc57c', kind : 'light-bug',
161
+ title : 'text-subtle failed on a card, in light, and on bands and tints in both themes',
162
+ summary: 'neutral-500 moved one step for the card (415fc57c, 4.61:1). On surface-subtle and the status tints it was still 4.29 to 4.46, and no value can be quieter than muted and clear 4.5 on every ground -- on surface-muted it would have to be muted. Decided: subtle text sits on the page or a card; on a band or a tint it steps up to text-muted. neutral-450 is measured on the dark card (4.75), and the compatibility table\'s blue band moved its labels to muted (6.10).',
163
+ files : ['compatibility-table'],
164
+ check : ({ decl, src }) => {
165
+ // what the rule is FOR: subtle clears 4.5 on a card in both themes, and the band that broke it no longer carries it
166
+ const c = t => (decl.get(t) || '').trim();
167
+ const light = contrast(c('--color-neutral-500'), c('--color-neutral-25')), dark = contrast(c('--color-neutral-450'), c('--color-neutral-925'));
168
+ const band = has(src['compatibility-table'], ':host([background="blue"]) .bd-compattable-lightlabel');
169
+ const ok = light >= 4.5 && dark >= 4.5 && band;
170
+ return { holds: ok, detail: `subtle on a card ${light.toFixed(2)}:1 in light, ${dark.toFixed(2)} in dark${band ? '; the blue-band label reads muted' : '; the blue-band label is subtle again'}` };
171
+ }
172
+ },
173
+ {
174
+ n : 18, status : 'fixed', commit : '591c695a', kind : 'role',
175
+ title : 'The list check borrowed its pair',
176
+ summary: 'Disc and tick read the status accent and text-inverse; they read the badge\'s own fill and on-fill now.',
177
+ files : ['list-item'],
178
+ check : ({ src }) => { const ok = has(src['list-item'], 'var(--status-success-fill)') && has(src['list-item'], 'var(--status-success-on-fill)'); return { holds: ok, detail: ok ? 'the check reads --status-success-fill and -on-fill' : 'the check borrows again' }; }
179
+ }
180
+ ];
181
+
182
+ /** Observations from the study that are not defects. Shown, not checked. */
183
+ export const OBSERVATIONS = [
184
+ { n: 6, title: 'What carries over unchanged', summary: 'muted and disabled share a value in both themes. In light, border-container is surface-muted\'s step, so a card edge disappears on a muted band; in dark it lands on surface-subtle\'s step instead (neutral-850), so the band it disappears on moves from muted to subtle.' },
185
+ { n: 7, title: 'Badges and tags hold in dark', summary: 'Every part passes in both modes, on the page and on a card. A badge and a tag of one tone read further apart in dark than in light, except brand, which is the same ground in both by design.' }
186
+ ];
187
+
188
+ /** Every check, run. */
189
+ export function runChecks({ decl, src, rows }) {
190
+ const byToken = new Map(rows.map(r => [r.token, r]));
191
+ const row = t => byToken.get(t) || {};
192
+ return FINDINGS.map(f => {
193
+ let res;
194
+ try { res = f.check({ decl, src, row }); } catch (e) { res = { holds: false, detail: `check failed to run: ${e.message}` }; }
195
+ return { ...f, ...res };
196
+ });
197
+ }
@@ -0,0 +1,207 @@
1
+ import { html, unsafeCSS } from 'lit';
2
+ import { copyToken, docsPage, refSection } from './story-chrome.js';
3
+ import { declarations, derive } from './dark-derivation.js';
4
+ import { OBSERVATIONS, runChecks } from './dark-findings.js';
5
+ /* The generated stylesheet itself, not a copy: every value on this page is
6
+ derived from it when the page opens, so the page follows the palette. */
7
+ import tokensCss from './tokens.css?raw';
8
+
9
+ import '../components/alert/alert.js';
10
+ import '../components/badge/badge.js';
11
+ import '../components/button/button.js';
12
+ import '../components/checkbox/checkbox.js';
13
+ import '../components/heading/heading.js';
14
+ import '../components/input/input.js';
15
+ import '../components/link/link.js';
16
+ import '../components/paragraph/paragraph.js';
17
+ import '../components/radio/radio.js';
18
+ import '../components/switch/switch.js';
19
+ import '../components/tag/tag.js';
20
+
21
+ export default {
22
+ title: 'Foundations/Theming/Dark mode',
23
+ tags : ['autodocs']
24
+ };
25
+
26
+ /* The stylesheets the findings' checks read. Lazy, so they only load here. */
27
+ const SOURCES = {
28
+ 'form-field' : () => import('../components/shared/form-field.css.js?raw'),
29
+ checkbox : () => import('../components/checkbox/checkbox.css.js?raw'),
30
+ radio : () => import('../components/radio/radio.css.js?raw'),
31
+ switch : () => import('../components/switch/switch.css.js?raw'),
32
+ link : () => import('../components/link/link.css.js?raw'),
33
+ 'list-item' : () => import('../components/list/list-item/list-item.css.js?raw'),
34
+ back : () => import('../components/back/back.css.js?raw'),
35
+ avatar : () => import('../components/avatar/avatar.css.js?raw'),
36
+ 'video-player' : () => import('../components/video-player/video-player.css.js?raw'),
37
+ video : () => import('../components/video/video.css.js?raw'),
38
+ 'terminal-message' : () => import('../components/terminal/terminal-message.css.js?raw'),
39
+ tooltip : () => import('../components/tooltip/tooltip.css.js?raw'),
40
+ 'compatibility-table': () => import('../components/compatibility-table/compatibility-table.css.js?raw')
41
+ };
42
+
43
+ async function load() {
44
+ const src = Object.fromEntries(await Promise.all(Object.entries(SOURCES).map(async ([k, f]) => [k, (await f()).default])));
45
+ const decl = declarations(tokensCss);
46
+ const derived = derive(tokensCss);
47
+ return { decl, derived, findings: runChecks({ decl, src, rows: derived.rows }) };
48
+ }
49
+
50
+ /* A container that re-declares the WHOLE token sheet, with each semantic
51
+ colour role swapped for its derived dark value. Component tokens are
52
+ re-declared too, so a --button-* that reads a role re-resolves inside the
53
+ container -- and one that reads a primitive stays put, exactly as it would
54
+ under a real dark theme. Custom properties inherit into shadow roots, so
55
+ the real components below render with it. */
56
+ function darkScope(decl, rows) {
57
+ const dark = new Map(rows.filter(r => r.darkHex).map(r => [r.token, r.darkHex]));
58
+ dark.set('--color-border-container-inverse', 'color-mix(in srgb, var(--color-neutral-1000) 12%, transparent)');
59
+ const body = [...decl].map(([k, v]) => `${k}: ${dark.get(k) ?? v};`).join('\n');
60
+ return unsafeCSS(`.dm-dark { ${body} }`);
61
+ }
62
+
63
+ const STATUS = {
64
+ 'fixed' : ['Fixed', 'text/success'],
65
+ 'partly fixed': ['Partly fixed', 'text/warning'],
66
+ 'open' : ['Open', 'text/error'],
67
+ 'decision' : ['Needs a decision', 'text/brand'],
68
+ 'decided' : ['Decided', 'text/success']
69
+ };
70
+ const chip = s => html`<span style="font-size:var(--typography-fontSize-xxs);font-weight:var(--typography-fontWeight-semibold);letter-spacing:var(--typography-letterSpacing-wide);text-transform:uppercase;color:var(--color-${STATUS[s][1].replace('/', '-')})">${STATUS[s][0]}</span>`;
71
+
72
+ const finding = f => html`
73
+ <div style="padding:var(--spacing-12) 0;border-top:var(--border-width-1) solid var(--color-border-subtle);max-width:80ch">
74
+ <div style="display:flex;gap:var(--spacing-12);align-items:baseline">
75
+ <b>${f.n}</b><b>${f.title}</b>${chip(f.status)}${f.commit ? html`<code class="muted">${f.commit}</code>` : ''}
76
+ </div>
77
+ <p style="margin:var(--spacing-4) 0">${f.summary}</p>
78
+ <p style="margin:0;font-size:var(--typography-fontSize-xs);color:${f.holds ? 'var(--color-text-muted)' : 'var(--color-text-error)'}">
79
+ ${f.holds ? html`<b>Checked against the code:</b> ${f.detail}.` : html`<b>The code disagrees with this status:</b> ${f.detail}. Update the finding.`}
80
+ </p>
81
+ </div>`;
82
+
83
+ /* the same set of real components, rendered twice */
84
+ const specimen = () => html`
85
+ <div style="display:grid;gap:var(--spacing-16)">
86
+ <bd-h as="h3" visual-level="h4">A section title</bd-h>
87
+ <bd-p>Body copy on the page ground, the primary ink.</bd-p>
88
+ <div style="display:flex;gap:var(--spacing-16);flex-wrap:wrap">
89
+ <!-- The ink is inline because bd-p has no colour API: its properties are
90
+ variant and the deprecated kind, both typographic. color="muted" set
91
+ nothing, so this row -- the one place the page shows the text roles --
92
+ rendered all four in primary, and check:attributes failed on it. -->
93
+ ${['muted', 'subtle', 'brand', 'error'].map(r => html`<bd-p style="color:var(--color-text-${r})">${r}</bd-p>`)}
94
+ </div>
95
+ <div style="display:flex;gap:var(--spacing-8);flex-wrap:wrap">
96
+ <bd-button variant="primary" label="Primary"></bd-button>
97
+ <bd-button variant="secondary" label="Secondary"></bd-button>
98
+ <bd-button variant="tertiary" label="Tertiary"></bd-button>
99
+ <bd-button variant="ghost" label="Ghost"></bd-button>
100
+ <bd-button variant="accent" label="Buy now"></bd-button>
101
+ </div>
102
+ <div style="display:flex;gap:var(--spacing-8);flex-wrap:wrap;align-items:center">
103
+ ${['neutral', 'brand', 'info', 'success', 'warning', 'error'].map(t => html`<bd-badge tone=${t}>${t}</bd-badge>`)}
104
+ ${['neutral', 'success', 'error'].map(t => html`<bd-tag tone=${t}>${t}</bd-tag>`)}
105
+ </div>
106
+ ${['info', 'success', 'warning', 'error'].map(t => html`<bd-alert tone=${t} heading=${`${t[0].toUpperCase()}${t.slice(1)} alert`}>The description stays the primary ink.</bd-alert>`)}
107
+ <div style="display:flex;gap:var(--spacing-24);flex-wrap:wrap">
108
+ <bd-checkbox label="Checked" checked></bd-checkbox>
109
+ <bd-radio label="Selected" checked></bd-radio>
110
+ <bd-switch checked>On</bd-switch>
111
+ <bd-link href="#" variant="primary">A primary link</bd-link>
112
+ </div>
113
+ <bd-custom-input label="Work email" placeholder="you@example.com"></bd-custom-input>
114
+ <bd-custom-input label="Work email" placeholder="you@example.com" invalid></bd-custom-input>
115
+ </div>`;
116
+
117
+ const valueRow = r => html`<tr>
118
+ <td>${copyToken(r.token)}</td>
119
+ <td class="num"><span style="display:inline-block;width:var(--dimension-12);height:var(--dimension-12);margin-right:var(--spacing-6);vertical-align:middle;border-radius:var(--radius-sm);border:var(--border-width-1) solid var(--color-border-subtle);background:${r.lightHex ?? 'transparent'}"></span>${r.light ?? '—'}</td>
120
+ <td class="num">${r.lightRatio == null ? '—' : r.lightRatio.toFixed(2)}</td>
121
+ <td class="num"><span style="display:inline-block;width:var(--dimension-12);height:var(--dimension-12);margin-right:var(--spacing-6);vertical-align:middle;border-radius:var(--radius-sm);border:var(--border-width-1) solid var(--color-border-subtle);background:${r.darkHex ?? 'transparent'}"></span>${r.dark ?? '—'}</td>
122
+ <td class="num" style="${r.ok === false ? 'color:var(--color-text-error)' : ''}">${r.darkRatio == null ? '—' : r.darkRatio.toFixed(2)}</td>
123
+ <td>${r.against}</td>
124
+ <td>${r.ok === null ? html`<span class="muted">—</span>` : r.ok ? 'ok' : html`<b style="color:var(--color-text-error)">fails</b>`}</td>
125
+ <td style="max-width:44ch">${r.rule}</td>
126
+ </tr>`;
127
+
128
+ export const DarkMode = (_args, { loaded: { data } }) => {
129
+ const { decl, derived, findings } = data;
130
+ const count = s => findings.filter(f => f.status === s).length;
131
+ const disagree = findings.filter(f => !f.holds);
132
+ const fails = derived.rows.filter(r => r.ok === false).length;
133
+ return docsPage({
134
+ title: 'Dark mode',
135
+ lede : `There is no dark theme in code. This page is the evidence for one: what each colour role would
136
+ become, measured; what that would break, and what has already been fixed so it will not.
137
+ Everything here is computed from tokens.css when the page opens, and every finding is checked
138
+ against the code, so the page cannot describe a palette that has moved on.`,
139
+ note: html`<b>Where this came from.</b> The 🔬 Dark mode study page in the Figma foundation file
140
+ measured every atom in both modes. The method and the derived values now live in
141
+ <code>dark-derivation.js</code>, the findings in <code>dark-findings.js</code>, and this page reads
142
+ both. <b>A proposal, not a theme:</b> nothing here ships until the open findings are decided —
143
+ adding these values to tokens.js today would ship the failures listed below.`,
144
+ children: html`
145
+ ${refSection({
146
+ name : 'Findings',
147
+ meta : `${count('fixed')} fixed · ${count('partly fixed')} partly · ${count('open')} open · ${count('decision')} need a decision · ${count('decided')} decided`,
148
+ children: html`
149
+ ${disagree.length
150
+ ? html`<p style="color:var(--color-text-error);max-width:70ch"><b>${disagree.length} finding${disagree.length === 1 ? '' : 's'}
151
+ no longer match${disagree.length === 1 ? 'es' : ''} the code</b> — a fix was reverted, or an open finding was resolved without
152
+ updating it. Each one says which below.</p>`
153
+ : ''}
154
+ ${['open', 'decision', 'partly fixed', 'decided', 'fixed'].map(s => findings.filter(f => f.status === s).map(finding))}
155
+ ${OBSERVATIONS.map(o => html`
156
+ <div style="padding:var(--spacing-12) 0;border-top:var(--border-width-1) solid var(--color-border-subtle);max-width:80ch">
157
+ <div style="display:flex;gap:var(--spacing-12)"><b>${o.n}</b><b>${o.title}</b><span class="muted">observation</span></div>
158
+ <p style="margin:var(--spacing-4) 0 0">${o.summary}</p>
159
+ </div>`)}`
160
+ })}
161
+
162
+ ${refSection({
163
+ name : 'Real components, both ways',
164
+ meta : 'the shipped tokens, and the derived dark values',
165
+ children: html`
166
+ <style>${darkScope(decl, derived.rows)}</style>
167
+ <p style="max-width:70ch">These are the library's own components, not drawings. On the right, a container
168
+ re-declares the whole token sheet with each semantic colour role swapped for its derived dark value.
169
+ Whatever reads a role switches; whatever reads a primitive does not — which is why the secondary button
170
+ disappeared there until it read --color-action-secondary (finding 8), and why Avatar's disc still stays
171
+ light (finding 14). Text styles, shadows and focus rings are not derived, and keep their values.</p>
172
+ <div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(min(100%, 30rem), 1fr));gap:var(--spacing-24)">
173
+ <div style="padding:var(--spacing-24);border-radius:var(--radius-xl);border:var(--border-width-1) solid var(--color-border-subtle);background:var(--color-surface-default)">
174
+ <p class="muted" style="margin:0 0 var(--spacing-16)">Shipped tokens</p>${specimen()}
175
+ </div>
176
+ <div class="dm-dark" style="padding:var(--spacing-24);border-radius:var(--radius-xl);background:var(--color-surface-default);color:var(--color-text-primary)">
177
+ <p style="margin:0 0 var(--spacing-16);color:var(--color-text-muted)">Derived dark values</p>${specimen()}
178
+ </div>
179
+ </div>`
180
+ })}
181
+
182
+ ${refSection({
183
+ name : 'How each dark value is chosen',
184
+ meta : 'keep what the light value measures',
185
+ children: html`<ul style="max-width:72ch;line-height:var(--typography-lineHeight-normal)">
186
+ <li><b>Text</b> keeps its contrast on the ground it is read on — the step nearest its light ratio, never under 4.5:1 where light clears it, in hierarchy order.</li>
187
+ <li><b>Grounds</b> keep their tonal step from the page, with a step lighter than the dark page (neutral-950, near-black so a lighter container exists).</li>
188
+ <li><b>Edges</b> keep their ratio class: under 3:1 stays decorative, 3:1 and over stays a boundary.</li>
189
+ <li><b>Status</b> tints sit at the dark end of the tone's ramp; ink clears 4.5:1 on the tint a badge uses; accent clears 3:1 on the page and on its own tint; a fill inverts only if it would vanish into the page.</li>
190
+ <li><b>Action</b> fills keep their value; their ink, --color-action-on-fill, stays white and clears 4.5:1 in every state. A resting fill keeps 3:1 on the page; hover and press are supplemental (finding 4).</li>
191
+ <li>A role is measured on every ground code draws it on, not only the page. Only existing primitives are used: where no step meets a rule, the nearest is shown and the row fails.</li>
192
+ </ul>`
193
+ })}
194
+
195
+ ${refSection({
196
+ name : 'Every proposed value',
197
+ meta : `${derived.rows.length} roles · ${fails} fail their rule`,
198
+ children: html`<div class="table-scroll"><table>
199
+ <caption>Each semantic colour role, its light value and the derived dark value, with WCAG contrast against the named ground</caption>
200
+ <thead><tr><th scope="col">Role</th><th scope="col">Light</th><th scope="col">ratio</th><th scope="col">Dark</th><th scope="col">ratio</th><th scope="col">Against</th><th scope="col">Verdict</th><th scope="col">Why this value</th></tr></thead>
201
+ <tbody>${derived.rows.map(valueRow)}</tbody>
202
+ </table></div>`
203
+ })}`
204
+ });
205
+ };
206
+ DarkMode.storyName = 'Dark mode';
207
+ DarkMode.loaders = [async () => ({ data: await load() })];