@rtkelly-labs/design-system 0.0.0-bootstrap.0 → 0.16.0-dev.0.37dedac

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 (284) hide show
  1. package/CHANGELOG.md +347 -0
  2. package/LICENSE +21 -0
  3. package/README.md +317 -2
  4. package/dist/components/AlertDialog.js +95 -0
  5. package/dist/components/AlertDialog.js.map +1 -0
  6. package/dist/components/AppShell.js +261 -0
  7. package/dist/components/AppShell.js.map +1 -0
  8. package/dist/components/AsciiDivider.js +8 -0
  9. package/dist/components/AsciiDivider.js.map +1 -0
  10. package/dist/components/AsciiGauge.js +130 -0
  11. package/dist/components/AsciiGauge.js.map +1 -0
  12. package/dist/components/Avatar.js +87 -0
  13. package/dist/components/Avatar.js.map +1 -0
  14. package/dist/components/Badge.js +25 -0
  15. package/dist/components/Badge.js.map +1 -0
  16. package/dist/components/BarChart.js +288 -0
  17. package/dist/components/BarChart.js.map +1 -0
  18. package/dist/components/BracketText.js +23 -0
  19. package/dist/components/BracketText.js.map +1 -0
  20. package/dist/components/BulletChart.js +50 -0
  21. package/dist/components/BulletChart.js.map +1 -0
  22. package/dist/components/Button.js +130 -0
  23. package/dist/components/Button.js.map +1 -0
  24. package/dist/components/Card.js +181 -0
  25. package/dist/components/Card.js.map +1 -0
  26. package/dist/components/ChartTooltip.js +54 -0
  27. package/dist/components/ChartTooltip.js.map +1 -0
  28. package/dist/components/Checkbox.js +55 -0
  29. package/dist/components/Checkbox.js.map +1 -0
  30. package/dist/components/ColophonCard.js +122 -0
  31. package/dist/components/ColophonCard.js.map +1 -0
  32. package/dist/components/DataTable.js +299 -0
  33. package/dist/components/DataTable.js.map +1 -0
  34. package/dist/components/Divider.js +42 -0
  35. package/dist/components/Divider.js.map +1 -0
  36. package/dist/components/Drawer.js +81 -0
  37. package/dist/components/Drawer.js.map +1 -0
  38. package/dist/components/EmptyState.js +39 -0
  39. package/dist/components/EmptyState.js.map +1 -0
  40. package/dist/components/ErrorSummary.js +122 -0
  41. package/dist/components/ErrorSummary.js.map +1 -0
  42. package/dist/components/Fieldset.js +37 -0
  43. package/dist/components/Fieldset.js.map +1 -0
  44. package/dist/components/Glyph.js +60 -0
  45. package/dist/components/Glyph.js.map +1 -0
  46. package/dist/components/Input.js +111 -0
  47. package/dist/components/Input.js.map +1 -0
  48. package/dist/components/LinkProvider.js +76 -0
  49. package/dist/components/LinkProvider.js.map +1 -0
  50. package/dist/components/Menu.js +144 -0
  51. package/dist/components/Menu.js.map +1 -0
  52. package/dist/components/MetadataGrid.js +184 -0
  53. package/dist/components/MetadataGrid.js.map +1 -0
  54. package/dist/components/MobileNav.js +120 -0
  55. package/dist/components/MobileNav.js.map +1 -0
  56. package/dist/components/Modal.js +79 -0
  57. package/dist/components/Modal.js.map +1 -0
  58. package/dist/components/NerdIcon.js +177 -0
  59. package/dist/components/NerdIcon.js.map +1 -0
  60. package/dist/components/NoteBlock.js +84 -0
  61. package/dist/components/NoteBlock.js.map +1 -0
  62. package/dist/components/PageHeader.js +34 -0
  63. package/dist/components/PageHeader.js.map +1 -0
  64. package/dist/components/PageTitle.js +21 -0
  65. package/dist/components/PageTitle.js.map +1 -0
  66. package/dist/components/Pagination.js +96 -0
  67. package/dist/components/Pagination.js.map +1 -0
  68. package/dist/components/Popover.js +87 -0
  69. package/dist/components/Popover.js.map +1 -0
  70. package/dist/components/Progress.js +83 -0
  71. package/dist/components/Progress.js.map +1 -0
  72. package/dist/components/RadioGroup.js +74 -0
  73. package/dist/components/RadioGroup.js.map +1 -0
  74. package/dist/components/ReportDocument.js +75 -0
  75. package/dist/components/ReportDocument.js.map +1 -0
  76. package/dist/components/SectionContainer.js +18 -0
  77. package/dist/components/SectionContainer.js.map +1 -0
  78. package/dist/components/Select.js +199 -0
  79. package/dist/components/Select.js.map +1 -0
  80. package/dist/components/SiteFooter.js +29 -0
  81. package/dist/components/SiteFooter.js.map +1 -0
  82. package/dist/components/SiteHeader.js +59 -0
  83. package/dist/components/SiteHeader.js.map +1 -0
  84. package/dist/components/SiteNav.js +141 -0
  85. package/dist/components/SiteNav.js.map +1 -0
  86. package/dist/components/Skeleton.js +51 -0
  87. package/dist/components/Skeleton.js.map +1 -0
  88. package/dist/components/SocialIcon.js +77 -0
  89. package/dist/components/SocialIcon.js.map +1 -0
  90. package/dist/components/Sparkline.js +104 -0
  91. package/dist/components/Sparkline.js.map +1 -0
  92. package/dist/components/Spinner.js +65 -0
  93. package/dist/components/Spinner.js.map +1 -0
  94. package/dist/components/StatCard.js +53 -0
  95. package/dist/components/StatCard.js.map +1 -0
  96. package/dist/components/StatusPage.js +99 -0
  97. package/dist/components/StatusPage.js.map +1 -0
  98. package/dist/components/Swatch.js +55 -0
  99. package/dist/components/Swatch.js.map +1 -0
  100. package/dist/components/Switch.js +53 -0
  101. package/dist/components/Switch.js.map +1 -0
  102. package/dist/components/TLDR.js +53 -0
  103. package/dist/components/TLDR.js.map +1 -0
  104. package/dist/components/Table.js +155 -0
  105. package/dist/components/Table.js.map +1 -0
  106. package/dist/components/Tabs.js +325 -0
  107. package/dist/components/Tabs.js.map +1 -0
  108. package/dist/components/Tag.js +19 -0
  109. package/dist/components/Tag.js.map +1 -0
  110. package/dist/components/TelemetryGauge.js +112 -0
  111. package/dist/components/TelemetryGauge.js.map +1 -0
  112. package/dist/components/ThemeProvider.js +87 -0
  113. package/dist/components/ThemeProvider.js.map +1 -0
  114. package/dist/components/TickerTape.js +196 -0
  115. package/dist/components/TickerTape.js.map +1 -0
  116. package/dist/components/Toast.js +200 -0
  117. package/dist/components/Toast.js.map +1 -0
  118. package/dist/components/Tooltip.js +69 -0
  119. package/dist/components/Tooltip.js.map +1 -0
  120. package/dist/components/TrackCard.js +206 -0
  121. package/dist/components/TrackCard.js.map +1 -0
  122. package/dist/components/blog/BlogPost.js +116 -0
  123. package/dist/components/blog/BlogPost.js.map +1 -0
  124. package/dist/components/blog/LoremIpsumPost.js +62 -0
  125. package/dist/components/blog/LoremIpsumPost.js.map +1 -0
  126. package/dist/components/blog/author.js +25 -0
  127. package/dist/components/blog/author.js.map +1 -0
  128. package/dist/components/booleanControl.js +50 -0
  129. package/dist/components/booleanControl.js.map +1 -0
  130. package/dist/components/dialogPopupRef.js +28 -0
  131. package/dist/components/dialogPopupRef.js.map +1 -0
  132. package/dist/components/dialogSurface.js +75 -0
  133. package/dist/components/dialogSurface.js.map +1 -0
  134. package/dist/components/docs/AnchorHeading.js +80 -0
  135. package/dist/components/docs/AnchorHeading.js.map +1 -0
  136. package/dist/components/docs/Breadcrumbs.js +26 -0
  137. package/dist/components/docs/Breadcrumbs.js.map +1 -0
  138. package/dist/components/docs/CodeBlock.js +63 -0
  139. package/dist/components/docs/CodeBlock.js.map +1 -0
  140. package/dist/components/docs/CodeTabs.js +71 -0
  141. package/dist/components/docs/CodeTabs.js.map +1 -0
  142. package/dist/components/docs/DocPager.js +28 -0
  143. package/dist/components/docs/DocPager.js.map +1 -0
  144. package/dist/components/docs/DocsHeader.js +124 -0
  145. package/dist/components/docs/DocsHeader.js.map +1 -0
  146. package/dist/components/docs/DocsLayout.js +61 -0
  147. package/dist/components/docs/DocsLayout.js.map +1 -0
  148. package/dist/components/docs/DocsLinkProvider.js +25 -0
  149. package/dist/components/docs/DocsLinkProvider.js.map +1 -0
  150. package/dist/components/docs/DocsSidebar.js +147 -0
  151. package/dist/components/docs/DocsSidebar.js.map +1 -0
  152. package/dist/components/docs/Prose.js +56 -0
  153. package/dist/components/docs/Prose.js.map +1 -0
  154. package/dist/components/docs/TableOfContents.js +71 -0
  155. package/dist/components/docs/TableOfContents.js.map +1 -0
  156. package/dist/components/docs/codeTabsStore.js +75 -0
  157. package/dist/components/docs/codeTabsStore.js.map +1 -0
  158. package/dist/components/docs/figures/ActivityGrid.js +116 -0
  159. package/dist/components/docs/figures/ActivityGrid.js.map +1 -0
  160. package/dist/components/docs/figures/BeforeAfter.js +64 -0
  161. package/dist/components/docs/figures/BeforeAfter.js.map +1 -0
  162. package/dist/components/docs/figures/ChangeSummary.js +74 -0
  163. package/dist/components/docs/figures/ChangeSummary.js.map +1 -0
  164. package/dist/components/docs/figures/Decision.js +34 -0
  165. package/dist/components/docs/figures/Decision.js.map +1 -0
  166. package/dist/components/docs/figures/DrawingSheet.js +37 -0
  167. package/dist/components/docs/figures/DrawingSheet.js.map +1 -0
  168. package/dist/components/docs/figures/FigureFrame.js +226 -0
  169. package/dist/components/docs/figures/FigureFrame.js.map +1 -0
  170. package/dist/components/docs/figures/FileChangeTree.js +134 -0
  171. package/dist/components/docs/figures/FileChangeTree.js.map +1 -0
  172. package/dist/components/docs/figures/FlowDiagram.js +82 -0
  173. package/dist/components/docs/figures/FlowDiagram.js.map +1 -0
  174. package/dist/components/docs/figures/GanttChart.js +79 -0
  175. package/dist/components/docs/figures/GanttChart.js.map +1 -0
  176. package/dist/components/docs/figures/IndexedSection.js +45 -0
  177. package/dist/components/docs/figures/IndexedSection.js.map +1 -0
  178. package/dist/components/docs/figures/MetricStrip.js +23 -0
  179. package/dist/components/docs/figures/MetricStrip.js.map +1 -0
  180. package/dist/components/docs/figures/Steps.js +58 -0
  181. package/dist/components/docs/figures/Steps.js.map +1 -0
  182. package/dist/components/docs/figures/SwatchCompare.js +42 -0
  183. package/dist/components/docs/figures/SwatchCompare.js.map +1 -0
  184. package/dist/components/docs/figures/TermGrid.js +22 -0
  185. package/dist/components/docs/figures/TermGrid.js.map +1 -0
  186. package/dist/components/docs/figures/Terminal.js +37 -0
  187. package/dist/components/docs/figures/Terminal.js.map +1 -0
  188. package/dist/components/docs/figures/Timeline.js +61 -0
  189. package/dist/components/docs/figures/Timeline.js.map +1 -0
  190. package/dist/components/docs/figures/TokenTable.js +42 -0
  191. package/dist/components/docs/figures/TokenTable.js.map +1 -0
  192. package/dist/components/docs/figures/TreeDiagram.js +76 -0
  193. package/dist/components/docs/figures/TreeDiagram.js.map +1 -0
  194. package/dist/components/docs/figures/UptimeStrip.js +68 -0
  195. package/dist/components/docs/figures/UptimeStrip.js.map +1 -0
  196. package/dist/components/docs/figures/index.js +24 -0
  197. package/dist/components/docs/figures/index.js.map +1 -0
  198. package/dist/components/docs/index.js +15 -0
  199. package/dist/components/docs/index.js.map +1 -0
  200. package/dist/components/docs/mdxComponents.js +108 -0
  201. package/dist/components/docs/mdxComponents.js.map +1 -0
  202. package/dist/components/experiments/DesignSandbox.js +206 -0
  203. package/dist/components/experiments/DesignSandbox.js.map +1 -0
  204. package/dist/components/experiments/ExperimentsView.js +138 -0
  205. package/dist/components/experiments/ExperimentsView.js.map +1 -0
  206. package/dist/components/fieldFrame.js +200 -0
  207. package/dist/components/fieldFrame.js.map +1 -0
  208. package/dist/components/floatingSurface.js +35 -0
  209. package/dist/components/floatingSurface.js.map +1 -0
  210. package/dist/components/marketing/CTASection.js +47 -0
  211. package/dist/components/marketing/CTASection.js.map +1 -0
  212. package/dist/components/marketing/FeatureGrid.js +49 -0
  213. package/dist/components/marketing/FeatureGrid.js.map +1 -0
  214. package/dist/components/marketing/Hero.js +44 -0
  215. package/dist/components/marketing/Hero.js.map +1 -0
  216. package/dist/components/marketing/PricingGrid.js +65 -0
  217. package/dist/components/marketing/PricingGrid.js.map +1 -0
  218. package/dist/components/marketing/SectionGrid.js +51 -0
  219. package/dist/components/marketing/SectionGrid.js.map +1 -0
  220. package/dist/components/paginationRange.js +28 -0
  221. package/dist/components/paginationRange.js.map +1 -0
  222. package/dist/components/portalTheme.js +14 -0
  223. package/dist/components/portalTheme.js.map +1 -0
  224. package/dist/components/saas/SaasLandingPage.js +201 -0
  225. package/dist/components/saas/SaasLandingPage.js.map +1 -0
  226. package/dist/components/siteNavContext.js +24 -0
  227. package/dist/components/siteNavContext.js.map +1 -0
  228. package/dist/components/slides/Slide.js +84 -0
  229. package/dist/components/slides/Slide.js.map +1 -0
  230. package/dist/components/slides/SlideDeck.js +228 -0
  231. package/dist/components/slides/SlideDeck.js.map +1 -0
  232. package/dist/components/themeInitScript.js +16 -0
  233. package/dist/components/themeInitScript.js.map +1 -0
  234. package/dist/components/themeScope.js +9 -0
  235. package/dist/components/themeScope.js.map +1 -0
  236. package/dist/components/toastLifetime.js +21 -0
  237. package/dist/components/toastLifetime.js.map +1 -0
  238. package/dist/fonts/DSSymbols-OFL.txt +98 -0
  239. package/dist/fonts/DSSymbols-Regular.woff2 +0 -0
  240. package/dist/fonts/SymbolsNerdFontMono-Regular.woff2 +0 -0
  241. package/dist/hooks/useActiveHeading.js +59 -0
  242. package/dist/hooks/useActiveHeading.js.map +1 -0
  243. package/dist/hooks/useCopyToClipboard.js +58 -0
  244. package/dist/hooks/useCopyToClipboard.js.map +1 -0
  245. package/dist/index.d.ts +6394 -0
  246. package/dist/index.js +117 -0
  247. package/dist/index.js.map +1 -0
  248. package/dist/lib/accentClasses.js +87 -0
  249. package/dist/lib/accentClasses.js.map +1 -0
  250. package/dist/lib/recipe.js +33 -0
  251. package/dist/lib/recipe.js.map +1 -0
  252. package/dist/lib/slug.js +39 -0
  253. package/dist/lib/slug.js.map +1 -0
  254. package/dist/lib/theme.js +76 -0
  255. package/dist/lib/theme.js.map +1 -0
  256. package/dist/lib/tokenRules.js +51 -0
  257. package/dist/lib/tokenRules.js.map +1 -0
  258. package/dist/prose.css +766 -0
  259. package/dist/styles.css +203 -0
  260. package/dist/theme/ansi.js +77 -0
  261. package/dist/theme/ansi.js.map +1 -0
  262. package/dist/theme/contrast.js +342 -0
  263. package/dist/theme/contrast.js.map +1 -0
  264. package/dist/theme/levels.js +234 -0
  265. package/dist/theme/levels.js.map +1 -0
  266. package/dist/theme/media.js +115 -0
  267. package/dist/theme/media.js.map +1 -0
  268. package/dist/theme/recommended.js +105 -0
  269. package/dist/theme/recommended.js.map +1 -0
  270. package/dist/theme.css +538 -0
  271. package/dist/tokens.js +1 -0
  272. package/dist/tokens.js.map +1 -0
  273. package/package.json +209 -6
  274. package/skills/design-system-usage/SKILL.md +75 -0
  275. package/terminal/rtk-midnight.alacritty.toml +33 -0
  276. package/terminal/rtk-midnight.ghostty.conf +24 -0
  277. package/terminal/rtk-midnight.itermcolors +167 -0
  278. package/terminal/rtk-midnight.windows-terminal.json +23 -0
  279. package/terminal/rtk-sketch.alacritty.toml +33 -0
  280. package/terminal/rtk-sketch.ghostty.conf +24 -0
  281. package/terminal/rtk-sketch.itermcolors +167 -0
  282. package/terminal/rtk-sketch.windows-terminal.json +23 -0
  283. package/tokens/palette.midnight.tokens.json +477 -0
  284. package/tokens/palette.sketch.tokens.json +477 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/theme/contrast.ts"],"sourcesContent":["/**\n * Contrast arithmetic over the ladder.\n *\n * Every colour in `levels.ts` is a literal, so contrast can be computed as data\n * — no browser, no screenshot, no rendering. `pnpm check:contrast` walks every\n * level and fails CI on a violation, which is the thing that makes the ladder\n * maintainable: every level times every role pair is a matrix nobody eyeballs\n * reliably twice.\n *\n * The gate is WCAG 2.1 relative luminance. APCA is the better predictor of\n * perceived contrast, but it is not yet normative and its thresholds are not\n * stable; WCAG ratios are what an audit will be run against. So APCA is\n * computed alongside and reported, never gated. It is reported because WCAG 2\n * overrates light text on a dark ground: midnight's `text.muted` passed at\n * 5.90:1 while scoring Lc -41, the weakest text pair in the system (#372).\n */\n\nimport type { Emphasis, Hue, HueRef, Intent, TextTone } from '../lib/theme';\nimport type { LevelDefinition, ThemeLevel } from './levels';\n\nexport interface Rgb {\n r: number;\n g: number;\n b: number;\n /** 0–1. Colours without an alpha channel parse as 1. */\n a: number;\n}\n\nconst HEX = /^#([0-9a-f]{3,8})$/i;\nconst RGB_FN = /^rgba?\\(\\s*([0-9.]+)[\\s,]+([0-9.]+)[\\s,]+([0-9.]+)(?:[\\s,/]+([0-9.]+))?\\s*\\)$/i;\n\n/** Parse `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()` and `rgba()`. */\nexport function parseColor(value: string): Rgb {\n const input = value.trim();\n\n const hex = HEX.exec(input);\n if (hex) {\n const digits = hex[1]!;\n const expand = (s: string) => Number.parseInt(s.length === 1 ? s + s : s, 16);\n if (digits.length === 3 || digits.length === 4) {\n return {\n r: expand(digits[0]!),\n g: expand(digits[1]!),\n b: expand(digits[2]!),\n a: digits.length === 4 ? expand(digits[3]!) / 255 : 1,\n };\n }\n if (digits.length === 6 || digits.length === 8) {\n return {\n r: expand(digits.slice(0, 2)),\n g: expand(digits.slice(2, 4)),\n b: expand(digits.slice(4, 6)),\n a: digits.length === 8 ? expand(digits.slice(6, 8)) / 255 : 1,\n };\n }\n }\n\n const fn = RGB_FN.exec(input);\n if (fn) {\n return {\n r: Number(fn[1]),\n g: Number(fn[2]),\n b: Number(fn[3]),\n a: fn[4] === undefined ? 1 : Number(fn[4]),\n };\n }\n\n throw new Error(`Unparseable colour: ${value}`);\n}\n\n/**\n * Flatten a possibly-translucent colour onto an opaque backdrop.\n *\n * Contrast is a property of what the eye receives, so a token with alpha has to\n * be composited before it can be measured. Without this step a scrim at 70%\n * would score against its own nominal colour and pass regardless of the surface\n * it actually sits on.\n */\nexport function composite(foreground: Rgb, backdrop: Rgb): Rgb {\n if (foreground.a >= 1) return foreground;\n const a = foreground.a;\n return {\n r: foreground.r * a + backdrop.r * (1 - a),\n g: foreground.g * a + backdrop.g * (1 - a),\n b: foreground.b * a + backdrop.b * (1 - a),\n a: 1,\n };\n}\n\n/** WCAG 2.1 relative luminance. */\nexport function relativeLuminance(color: Rgb): number {\n const channel = (value: number) => {\n const c = value / 255;\n return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b);\n}\n\n/**\n * WCAG contrast ratio between two colours, 1–21.\n *\n * The foreground is composited onto the background first, so translucent\n * tokens are measured as they render.\n */\nexport function contrastRatio(foreground: string, background: string): number {\n const bg = parseColor(background);\n const fg = composite(parseColor(foreground), bg);\n const lighter = Math.max(relativeLuminance(fg), relativeLuminance(bg));\n const darker = Math.min(relativeLuminance(fg), relativeLuminance(bg));\n return (lighter + 0.05) / (darker + 0.05);\n}\n\n/**\n * APCA lightness contrast (Lc), per APCA-W3 0.0.98G — roughly -108 to 106.\n *\n * Signed by polarity: positive is dark text on a light ground, negative is\n * light on dark, and the magnitude is what compares across the two. Unlike a\n * WCAG ratio it is not symmetric, which is the point: the same pair of colours\n * reads differently depending on which one is the text.\n *\n * Constants are the published 0.0.98G set; the luminance is APCA's own simple\n * 2.4 power curve, not WCAG's piecewise one. Reported, not gated — see the\n * module comment.\n */\nexport function apcaContrast(foreground: string, background: string): number {\n const bg = parseColor(background);\n const fg = composite(parseColor(foreground), bg);\n const y = (c: Rgb) =>\n 0.2126729 * (c.r / 255) ** 2.4 + 0.7151522 * (c.g / 255) ** 2.4 + 0.072175 * (c.b / 255) ** 2.4;\n const clamp = (lum: number) => (lum > 0.022 ? lum : lum + (0.022 - lum) ** 1.414);\n const text = clamp(y(fg));\n const ground = clamp(y(bg));\n if (Math.abs(ground - text) < 0.0005) return 0;\n if (ground > text) {\n const sapc = (ground ** 0.56 - text ** 0.57) * 1.14;\n return sapc < 0.1 ? 0 : (sapc - 0.027) * 100;\n }\n const sapc = (ground ** 0.65 - text ** 0.62) * 1.14;\n return sapc > -0.1 ? 0 : (sapc + 0.027) * 100;\n}\n\n/**\n * The bar each role has to clear, and why it differs.\n *\n * `text` and the accents clear WCAG AA for body copy, because they are read.\n * `borderStrong` and `borderDefault` clear the 3:1 non-text bar of WCAG 1.4.11,\n * because they are the load-bearing edges this system draws components with.\n * `borderSubtle` is a decorative divider — 1.4.11 does not cover it, so the bar\n * is only that it must be *visible* rather than accessible as a control.\n * Setting it to AA would force every hairline in the system to read as a rule.\n */\n/**\n * The Hue vocabulary, in wheel order. Declared here rather than imported so the\n * gate stays runnable by `scripts/check-contrast.mjs` without a `.ts` import\n * extension — and `satisfies` still makes a missing or misspelled Hue a\n * compile error, which is the property that matters.\n */\n/**\n * Above this, a colour declared `'neutral'` is not one. `accent.quiet` measures\n * 0.036 on `midnight` and 0.027 on `sketch` — both warm or cool greys with a\n * deliberate tint, which is why the bar is not zero.\n */\nexport const MAXIMUM_NEUTRAL_CHROMA = 0.045;\n\nexport interface HueAgreementCheck {\n readonly level: ThemeLevel;\n readonly role: string;\n readonly declared: HueRef;\n readonly value: string;\n /** The Hue's value, or null when the Role is declared `'neutral'`. */\n readonly expected: string | null;\n readonly passes: boolean;\n readonly detail: string;\n}\n\n/** OKLab chroma. Only used to prove a `'neutral'` really is one. */\nfunction oklabChroma(hex: string): number {\n const decode = (v: number) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);\n const [r, g, b] = [1, 3, 5].map((i) => decode(Number.parseInt(hex.slice(i, i + 2), 16) / 255));\n const l = Math.cbrt(0.4122214708 * r! + 0.5363325363 * g! + 0.0514459929 * b!);\n const m = Math.cbrt(0.2119034982 * r! + 0.6806995451 * g! + 0.1073969566 * b!);\n const s = Math.cbrt(0.0883024619 * r! + 0.2817188376 * g! + 0.6299787005 * b!);\n const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;\n const bb = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;\n return Math.hypot(a, bb);\n}\n\nconst PALETTE_HUES_ORDER = [\n 'red',\n 'orange',\n 'yellow',\n 'green',\n 'teal',\n 'cyan',\n 'blue',\n 'violet',\n 'magenta',\n 'pink',\n] as const satisfies readonly Hue[];\n\nexport const MINIMUM_RATIO = {\n text: 4.5,\n /** `text.inverse` sits on an accent fill, not on a surface. */\n textInverse: 4.5,\n accent: 4.5,\n intent: 4.5,\n /**\n * A Hue, above WCAG AA deliberately. An editor draws selection, current-line,\n * find-match and diff backgrounds *behind* the same tokens, so a palette\n * solved to exactly 4.5:1 has no room left to tint the ground. At 4.5 the\n * light palette has one sRGB step of headroom; at 5.5 it has twenty-two.\n * See `docs/palette-provenance.md` and #78.\n */\n palette: 5.5,\n /**\n * These numbers are the **web** Medium's, and that indexing is the point.\n *\n * ADR 0004 names this the asymmetry most likely to be got wrong later:\n * *colour values do not vary by Medium, but the floors they must clear do.* A\n * projected 1080p frame, a compression-damaged video and an unantialiased\n * terminal cell are not the browser's legibility problem, and until now this\n * was one set of numbers for every surface the system emits to.\n *\n * `MEDIA_DEFINITIONS[medium].contrastFloor` carries the per-Medium values;\n * `auditContrast` takes a Medium and reads them. The constants here remain as\n * the web Medium's, so nothing moves on merge and a caller that does not care\n * about Media keeps working.\n */\n /** `bright` Hues carry emphasis, not body text, so AA is the right bar. */\n paletteBright: 4.5,\n borderStrong: 3,\n borderDefault: 3,\n borderSubtle: 1.4,\n /**\n * A modal's dialog against the scrimmed page behind it. Not an accessibility\n * threshold — an assertion that the scrim does the one job it exists for.\n */\n overlaySeparation: 3,\n /**\n * A selection device — the accent `fill` behind a chosen tab, or the 4px\n * accent `edge` beside it — against the surface it sits on. WCAG 1.4.11's\n * non-text bar: the device is a UI component boundary, not text.\n */\n stateDevice: 3,\n} as const;\n\nexport interface ContrastCheck {\n level: ThemeLevel;\n /** e.g. `text.muted on surface.raised`. */\n pair: string;\n foreground: string;\n background: string;\n ratio: number;\n /** APCA Lc for the same pair, reported alongside the ratio and never gated. Null where the pair is a separation rather than a foreground on a ground. */\n apca: number | null;\n minimum: number;\n passes: boolean;\n}\n\n/**\n * Every pair worth asserting, for every level.\n *\n * Text, accents and intents are checked against `base`, `raised` *and*\n * `sunken`, because a component is free to sit on any of the three and a token\n * that only works on the page ground is a bug waiting for the first card.\n *\n * The ladder is passed in rather than imported so this module is pure colour\n * arithmetic over data — it can audit a consumer's own overrides, and it stays\n * runnable from a plain Node script with no bundler in the path.\n */\n/**\n * Does every Role actually hold the value of the Hue it says it is?\n *\n * `check:contrast` cannot answer this, and that is not an oversight in it — a\n * Role and a Hue are both foregrounds, both clear their own floors, and a\n * mismatch between them is not a contrast fault. It is a *drift* fault, and it\n * is the one ADR 0001 rejects its third option over:\n *\n * > the two sets then drift silently, and there is no arithmetic that can\n * > catch a `palette.cyan` that no longer matches the `accent.primary` it is\n * > supposed to be.\n *\n * This is that arithmetic. It exists because the repo shipped the fault: after\n * `palette` landed, `midnight`'s `accent.tertiary` sat at `#ec4899` (5.12:1,\n * clearing the 4.5 Role floor) while `palette.pink` sat at `#f955a4` (5.90:1,\n * clearing the 5.5 Hue floor). Both gates passed. The two values are 0.036\n * apart in OKLab — indistinguishable, so no screenshot would have caught it\n * either.\n *\n * A Role declared `'neutral'` is asserted to *be* neutral rather than skipped,\n * because \"declared as not-a-hue\" and \"quietly wrong\" must not look the same.\n */\nexport function auditHueAgreement(\n ladder: Readonly<Record<ThemeLevel, LevelDefinition>>,\n): HueAgreementCheck[] {\n const results: HueAgreementCheck[] = [];\n\n for (const level of Object.keys(ladder) as ThemeLevel[]) {\n const def = ladder[level];\n\n const check = (role: string, value: string, ref: HueRef) => {\n if (ref === 'neutral') {\n const chroma = oklabChroma(value);\n results.push({\n level,\n role,\n declared: ref,\n value,\n expected: null,\n passes: chroma <= MAXIMUM_NEUTRAL_CHROMA,\n detail: `chroma ${chroma.toFixed(3)} (max ${MAXIMUM_NEUTRAL_CHROMA})`,\n });\n return;\n }\n const expected = def.palette[ref];\n results.push({\n level,\n role,\n declared: ref,\n value,\n expected,\n passes: value.toLowerCase() === expected.toLowerCase(),\n detail: `palette.${ref} is ${expected}`,\n });\n };\n\n for (const [role, ref] of Object.entries(def.accentHue)) {\n check(`accent.${role}`, def.accent[role as Emphasis], ref);\n }\n for (const [role, ref] of Object.entries(def.intentHue)) {\n check(`intent.${role}`, def.intent[role as Intent], ref);\n }\n }\n\n return results;\n}\n\n/** The three floors a Medium supplies. See `MediumDefinition.contrastFloor`. */\nexport interface ContrastFloor {\n readonly role: number;\n readonly hue: number;\n readonly hueBright: number;\n}\n\n/** The web Medium's floors, restated so the default call is unchanged. */\nexport const WEB_FLOOR: ContrastFloor = {\n role: MINIMUM_RATIO.text,\n hue: MINIMUM_RATIO.palette,\n hueBright: MINIMUM_RATIO.paletteBright,\n};\n\nexport function auditContrast(\n ladder: Readonly<Record<ThemeLevel, LevelDefinition>>,\n /**\n * The Medium's floors. Passed in rather than read from `media.ts`, because\n * this module has to stay importable by `scripts/check-contrast.mjs`, and a\n * runtime import there needs a `.ts` extension that `tsc` rejects. The script\n * owns the Medium; this module owns the arithmetic.\n */\n floor: ContrastFloor = WEB_FLOOR,\n): ContrastCheck[] {\n const results: ContrastCheck[] = [];\n\n for (const [level, def] of Object.entries(ladder) as [ThemeLevel, LevelDefinition][]) {\n const grounds = [\n ['surface.base', def.surface.base],\n ['surface.raised', def.surface.raised],\n ['surface.sunken', def.surface.sunken],\n ] as const;\n\n const check = (pair: string, foreground: string, background: string, minimum: number) => {\n const ratio = contrastRatio(foreground, background);\n results.push({\n level,\n pair,\n foreground,\n background,\n ratio,\n apca: apcaContrast(foreground, background),\n minimum,\n passes: ratio >= minimum,\n });\n };\n\n for (const [groundName, ground] of grounds) {\n for (const tone of ['primary', 'secondary', 'muted'] as const satisfies readonly TextTone[]) {\n check(`text.${tone} on ${groundName}`, def.text[tone], ground, floor.role);\n }\n for (const tone of ['primary', 'secondary', 'tertiary', 'quiet'] as const satisfies readonly Emphasis[]) {\n check(`accent.${tone} on ${groundName}`, def.accent[tone], ground, floor.role);\n }\n for (const tone of ['info', 'success', 'warning', 'danger'] as const satisfies readonly Intent[]) {\n check(`intent.${tone} on ${groundName}`, def.intent[tone], ground, floor.role);\n }\n // ADR 0001: gate `palette` once per Hue rather than once per Role that\n // consumes it. Separate the palette, not each of its consumers.\n for (const hue of PALETTE_HUES_ORDER) {\n check(`palette.${hue} on ${groundName}`, def.palette[hue], ground, floor.hue);\n check(\n `palette.bright.${hue} on ${groundName}`,\n def.paletteBright[hue],\n ground,\n floor.hueBright,\n );\n }\n check(`border.strong on ${groundName}`, def.border.strong, ground, MINIMUM_RATIO.borderStrong);\n check(`border.default on ${groundName}`, def.border.default, ground, MINIMUM_RATIO.borderDefault);\n check(`border.subtle on ${groundName}`, def.border.subtle, ground, MINIMUM_RATIO.borderSubtle);\n }\n\n // `text.inverse` is what a filled accent button prints in, so it is measured\n // against the accents rather than against a surface.\n // `quiet` is in this list because `Tag` ships `bg-accent-quiet\n // text-content-inverse`. It was omitted while the other three were checked,\n // so a palette edit to `quiet` would have gone unmeasured — a coverage gap\n // rather than a live defect, but the README claims every pair is audited.\n for (const tone of ['primary', 'secondary', 'tertiary', 'quiet'] as const satisfies readonly Emphasis[]) {\n check(\n `text.inverse on accent.${tone}`,\n def.text.inverse,\n def.accent[tone],\n floor.role,\n );\n }\n for (const tone of ['info', 'success', 'warning', 'danger'] as const satisfies readonly Intent[]) {\n check(\n `text.inverse on intent.${tone}`,\n def.text.inverse,\n def.intent[tone],\n floor.role,\n );\n }\n // `Select`'s open list (#164), named for the component because it used to\n // be the one surface the palette did not reach: the operating system\n // painted it. The rows are `text.primary` on `surface.raised`; a disabled\n // row is `text.muted`; the highlighted row is `text.inverse` on the field's\n // accent fill, and the chosen row's mark is that accent on the list. The\n // accent is whichever the caller passes, so every Emphasis and Intent is\n // measured rather than only the default. The values repeat pairs above —\n // the point is that removing one of those cannot silently un-gate the list.\n check('select.option: text.primary on surface.raised', def.text.primary, def.surface.raised, floor.role);\n check('select.option disabled: text.muted on surface.raised', def.text.muted, def.surface.raised, floor.role);\n check('select.placeholder: text.muted on surface.base', def.text.muted, def.surface.base, floor.role);\n const listAccents = [\n ...(['primary', 'secondary', 'tertiary', 'quiet'] as const satisfies readonly Emphasis[]).map(\n (tone) => [`accent.${tone}`, def.accent[tone]] as const,\n ),\n ...(['info', 'success', 'warning', 'danger'] as const satisfies readonly Intent[]).map(\n (tone) => [`intent.${tone}`, def.intent[tone]] as const,\n ),\n ];\n for (const [name, fill] of listAccents) {\n check(`select.option highlighted: text.inverse on ${name}`, def.text.inverse, fill, floor.role);\n check(`select.option selected mark: ${name} on surface.raised`, fill, def.surface.raised, floor.role);\n }\n\n // A scrim's job is to separate the dialog from the page behind it. Nothing\n // renders text on the scrim, so the assertion is about separation — and it\n // can be satisfied by either edge of the dialog, because this system draws\n // components with a load-bearing 2px border as often as with a fill. On a\n // dark level the fill barely differs from the scrim and the white border\n // does the work; on a light level the white fill does it and the dark\n // border is the one that disappears. Requiring both would fail every level.\n const scrimOverBase = composite(\n parseColor(def.surface.overlay),\n parseColor(def.surface.base),\n );\n const scrim = `rgb(${Math.round(scrimOverBase.r)}, ${Math.round(scrimOverBase.g)}, ${Math.round(scrimOverBase.b)})`;\n const separation = Math.max(\n contrastRatio(def.surface.raised, scrim),\n contrastRatio(def.border.strong, scrim),\n );\n results.push({\n level,\n pair: 'dialog (fill or border) against scrimmed page',\n foreground: `${def.surface.raised} / ${def.border.strong}`,\n background: scrim,\n ratio: separation,\n apca: null,\n minimum: MINIMUM_RATIO.overlaySeparation,\n passes: separation >= MINIMUM_RATIO.overlaySeparation,\n });\n }\n\n return results;\n}\n\n/**\n * How far apart the text tones sit, in APCA Lc, on each ground.\n *\n * Reported, not gated. It is the other half of tuning `text.muted`: darkening it\n * for legibility is free until it lands so close to `text.secondary` that the\n * two stop reading as two tones. #372 measured `#4f4a3d` on sketch at 7 Lc from\n * secondary, which is why `#5c5546` was chosen instead.\n */\nexport interface TextHierarchyStep {\n level: ThemeLevel;\n ground: string;\n /** Lc magnitude of primary, secondary and muted on this ground. */\n lc: Readonly<Record<'primary' | 'secondary' | 'muted', number>>;\n /** secondary minus muted, in Lc magnitude. */\n mutedStep: number;\n}\n\nexport function auditTextHierarchy(\n ladder: Readonly<Record<ThemeLevel, LevelDefinition>>,\n): TextHierarchyStep[] {\n const steps: TextHierarchyStep[] = [];\n for (const [level, def] of Object.entries(ladder) as [ThemeLevel, LevelDefinition][]) {\n for (const ground of ['base', 'raised', 'sunken'] as const) {\n const lc = (tone: 'primary' | 'secondary' | 'muted') =>\n Math.abs(apcaContrast(def.text[tone], def.surface[ground]));\n const tones = { primary: lc('primary'), secondary: lc('secondary'), muted: lc('muted') };\n steps.push({ level, ground: `surface.${ground}`, lc: tones, mutedStep: tones.secondary - tones.muted });\n }\n }\n return steps;\n}\n\n/** How a component may mark a selected item. */\nexport type SelectionDevice = 'fill' | 'edge' | 'surface pair';\n\nexport interface SelectionDeviceCheck {\n level: ThemeLevel;\n device: SelectionDevice;\n /** e.g. `accent.primary on surface.raised`. */\n pair: string;\n foreground: string;\n background: string;\n ratio: number;\n minimum: number;\n /** For `fill` and `edge`: the device is usable. For `surface pair`: it would be, and that is the trap. */\n passes: boolean;\n}\n\n/**\n * The state rule, as arithmetic.\n *\n * Selected state is carried by an accent **fill** or an accent **edge**, never\n * by swapping one surface for another. The reason is not taste: the surfaces\n * exist to *layer* — a strip behind its tabs, a panel over a page — so they\n * are deliberately close in lightness, and on the light rungs they are a few\n * percent apart. A widget that marks its chosen tab `surface.base` among\n * `surface.raised` siblings reads acceptably on `midnight` and is invisible on\n * `bright`. That is how the bug ships: it passes review on the dark rung the\n * reviewer happens to be on.\n *\n * So this audits both halves. The fill and edge devices — the accent against\n * every surface a tab can sit on — must clear the non-text bar on every level,\n * which is what makes the rule safe to follow. The surface pairs are measured\n * too, and reported, so the number that makes them unusable is in the gate's\n * output rather than in someone's memory. They are not expected to pass; a\n * level on which they *did* would be the one where a surface-pair widget\n * slips through.\n */\nexport function auditSelectionDevices(\n ladder: Readonly<Record<ThemeLevel, LevelDefinition>>,\n): SelectionDeviceCheck[] {\n const results: SelectionDeviceCheck[] = [];\n\n for (const [level, def] of Object.entries(ladder) as [ThemeLevel, LevelDefinition][]) {\n const grounds = [\n ['surface.base', def.surface.base],\n ['surface.raised', def.surface.raised],\n ] as const;\n\n const check = (\n device: SelectionDevice,\n pair: string,\n foreground: string,\n background: string,\n ) => {\n const ratio = contrastRatio(foreground, background);\n results.push({\n level,\n device,\n pair,\n foreground,\n background,\n ratio,\n minimum: MINIMUM_RATIO.stateDevice,\n passes: ratio >= MINIMUM_RATIO.stateDevice,\n });\n };\n\n for (const [groundName, ground] of grounds) {\n // `quiet` is the de-emphasised step, not a fifth colour — a tab it\n // marks is meant to recede, so it is not a selection device.\n for (const tone of ['primary', 'secondary', 'tertiary'] as const satisfies readonly Emphasis[]) {\n check('fill', `accent.${tone} fill on ${groundName}`, def.accent[tone], ground);\n check('edge', `accent.${tone} edge on ${groundName}`, def.accent[tone], ground);\n }\n }\n\n check('surface pair', 'surface.raised against surface.base', def.surface.raised, def.surface.base);\n check('surface pair', 'surface.sunken against surface.base', def.surface.sunken, def.surface.base);\n check('surface pair', 'surface.sunken against surface.raised', def.surface.sunken, def.surface.raised);\n }\n\n return results;\n}\n"],"mappings":"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","names":["sapc"]}
@@ -0,0 +1,234 @@
1
+ // src/theme/levels.ts
2
+ var THEME_LEVELS = ["midnight", "sketch"];
3
+ var FIXED_COLOURS = {
4
+ black: "#000000",
5
+ white: "#ffffff",
6
+ transparent: "transparent"
7
+ };
8
+ var PALETTE_HUES = [
9
+ "red",
10
+ "orange",
11
+ "yellow",
12
+ "green",
13
+ "teal",
14
+ "cyan",
15
+ "blue",
16
+ "violet",
17
+ "magenta",
18
+ "pink"
19
+ ];
20
+ var LEVELS = {
21
+ midnight: {
22
+ label: "Midnight",
23
+ description: "Neon on blue-black. The maximal end of the ladder.",
24
+ polarity: "dark",
25
+ surface: {
26
+ base: "#0a0a1a",
27
+ raised: "#14142a",
28
+ sunken: "#050510",
29
+ overlay: "rgba(5, 5, 16, 0.82)"
30
+ },
31
+ text: {
32
+ primary: "#ffffff",
33
+ secondary: "#c3c3d4",
34
+ // Was #8b8ba3: 5.90:1, but APCA Lc -41, the weakest text pair in the
35
+ // system. WCAG 2 overrates light-on-dark. Now 7.39:1 / Lc -50, still 21 Lc
36
+ // below `secondary`. accent.quiet keeps the old value — a fill, not text.
37
+ muted: "#9d9db4",
38
+ inverse: "#0a0a1a"
39
+ },
40
+ border: {
41
+ strong: "#ffffff",
42
+ default: "#9d9db4",
43
+ subtle: "#33334d"
44
+ },
45
+ accent: {
46
+ primary: "#22d3ee",
47
+ secondary: "#facc15",
48
+ // Was #ec4899 — the brand pink, and 5.12:1, which cleared the 4.5 Role
49
+ // floor while `palette.pink` sat at 5.90:1. Two pinks 0.036 apart in one
50
+ // level, invisibly different and therefore worse than visibly. Now the
51
+ // Hue's value, per `accentHue` below.
52
+ tertiary: "#f955a4",
53
+ quiet: "#8b8ba3"
54
+ },
55
+ intent: {
56
+ info: "#22d3ee",
57
+ success: "#39ff14",
58
+ warning: "#facc15",
59
+ danger: "#f955a4"
60
+ },
61
+ accentHue: {
62
+ primary: "cyan",
63
+ secondary: "yellow",
64
+ tertiary: "pink",
65
+ quiet: "neutral"
66
+ },
67
+ intentHue: {
68
+ info: "cyan",
69
+ success: "green",
70
+ warning: "yellow",
71
+ danger: "pink"
72
+ },
73
+ palette: {
74
+ red: "#ff586e",
75
+ // 16deg — lifted from #f43f5e (dim accent.tertiary), 4.59:1
76
+ orange: "#ff8c00",
77
+ // 58deg — kept — 410 commits
78
+ yellow: "#facc15",
79
+ // 92deg — kept — 533 commits, brand
80
+ green: "#39ff14",
81
+ // 142deg — kept — 533 commits, brand
82
+ teal: "#34d399",
83
+ // 163deg — kept — 364 commits
84
+ cyan: "#22d3ee",
85
+ // 212deg — kept — 534 commits, brand
86
+ blue: "#38bdf8",
87
+ // 233deg — kept — dim accent.primary, 233deg (a sky blue by heritage)
88
+ violet: "#c3afff",
89
+ // 295deg — new — 57 tried across six years, none canonical
90
+ magenta: "#ff1cff",
91
+ // 328deg — lifted from #ff00ff, 5.37:1
92
+ pink: "#f955a4"
93
+ // 354deg — lifted from #ec4899 (brand), 4.77:1 — deltaE 0.036
94
+ },
95
+ paletteBright: {
96
+ red: "#ff939b",
97
+ orange: "#ffba85",
98
+ yellow: "#ffeaab",
99
+ green: "#c0ffb8",
100
+ teal: "#00f7ae",
101
+ cyan: "#88ebff",
102
+ blue: "#8cd7ff",
103
+ violet: "#ddd4ff",
104
+ magenta: "#ff88fd",
105
+ pink: "#ff8ebe"
106
+ },
107
+ shadow: "#ffffff"
108
+ },
109
+ sketch: {
110
+ label: "Sketch",
111
+ description: "Warm paper and pen ink. The light half of the pair.",
112
+ polarity: "light",
113
+ surface: {
114
+ base: "#f5f3ec",
115
+ // sketch paper
116
+ raised: "#ffffff",
117
+ // FIXED_COLOURS.white — a fresh sheet on the desk
118
+ sunken: "#efeadf",
119
+ // the binding ground: every value below is solved against this
120
+ overlay: "rgba(35, 38, 46, 0.72)"
121
+ },
122
+ text: {
123
+ primary: "#23262e",
124
+ // sketch ink
125
+ secondary: "#33373f",
126
+ // Was #6a6252: 5.03:1 / APCA Lc 68 on `sunken`, carried by 11px labels.
127
+ // Now 6.16:1 / Lc 73 there. Darker still (#4f4a3d) left muted 7 Lc from
128
+ // `secondary`, and the two tones stopped reading as two. accent.quiet keeps
129
+ // the old value.
130
+ muted: "#5c5546",
131
+ inverse: "#f5f3ec"
132
+ },
133
+ border: {
134
+ strong: "#23262e",
135
+ default: "#8f8672",
136
+ subtle: "#c8c3b4"
137
+ // darker than sketch's #d8d3c4, which was 1.25:1 on a pure-white `raised`
138
+ },
139
+ accent: {
140
+ primary: "#1450d7",
141
+ // palette.blue
142
+ secondary: "#006b2e",
143
+ // palette.green
144
+ tertiary: "#bd0010",
145
+ // palette.red
146
+ quiet: "#6a6252"
147
+ },
148
+ intent: {
149
+ info: "#1450d7",
150
+ success: "#006b2e",
151
+ warning: "#974503",
152
+ danger: "#bd0010"
153
+ },
154
+ accentHue: {
155
+ primary: "blue",
156
+ secondary: "green",
157
+ tertiary: "red",
158
+ quiet: "neutral"
159
+ },
160
+ intentHue: {
161
+ info: "blue",
162
+ success: "green",
163
+ warning: "orange",
164
+ danger: "red"
165
+ },
166
+ palette: {
167
+ red: "#bd0010",
168
+ // 16deg — was #dc2626, 4.03:1 — failed WCAG AA
169
+ orange: "#974503",
170
+ // 58deg — was #9a4708, 5.34:1
171
+ yellow: "#705a00",
172
+ // 92deg — new — no light yellow was ever authored
173
+ green: "#006b2e",
174
+ // 142deg — was #15803d, 4.18:1 — failed
175
+ teal: "#006859",
176
+ // 163deg — new
177
+ cyan: "#006675",
178
+ // 212deg — new
179
+ blue: "#1450d7",
180
+ // 233deg — was #2563eb, 4.31:1 — failed
181
+ violet: "#7d00f4",
182
+ // 295deg — new
183
+ magenta: "#a300ad",
184
+ // 328deg — new
185
+ pink: "#b4006c"
186
+ // 354deg — new
187
+ },
188
+ paletteBright: {
189
+ red: "#8f002a",
190
+ orange: "#6c3700",
191
+ yellow: "#534200",
192
+ green: "#024f00",
193
+ teal: "#004d34",
194
+ cyan: "#004b56",
195
+ blue: "#004e6d",
196
+ violet: "#6000bd",
197
+ magenta: "#7f0080",
198
+ pink: "#8a0052"
199
+ },
200
+ shadow: "#23262e"
201
+ }
202
+ };
203
+ var DEFAULT_LEVEL = "midnight";
204
+ var SYSTEM_LEVEL = {
205
+ dark: "midnight",
206
+ light: "sketch"
207
+ };
208
+ var INK_WEIGHT = {
209
+ dark: 400,
210
+ light: 450
211
+ };
212
+ function isThemeLevel(value) {
213
+ return typeof value === "string" && THEME_LEVELS.includes(value);
214
+ }
215
+ function nextLevel(level) {
216
+ const index = THEME_LEVELS.indexOf(level);
217
+ return THEME_LEVELS[(index + 1) % THEME_LEVELS.length];
218
+ }
219
+ function assertNever(value, message = "Unhandled theme level") {
220
+ throw new Error(`${message}: ${String(value)}`);
221
+ }
222
+ export {
223
+ DEFAULT_LEVEL,
224
+ FIXED_COLOURS,
225
+ INK_WEIGHT,
226
+ LEVELS,
227
+ PALETTE_HUES,
228
+ SYSTEM_LEVEL,
229
+ THEME_LEVELS,
230
+ assertNever,
231
+ isThemeLevel,
232
+ nextLevel
233
+ };
234
+ //# sourceMappingURL=levels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/theme/levels.ts"],"sourcesContent":["/**\n * The theme ladder — the single place a level name or a level's colour is written.\n *\n * Everything else in the system derives from this file: the CSS in\n * `theme.generated.css`, the runtime provider, the Storybook toolbar, the\n * walkthrough matrix, and the contrast gate. Adding a level here is a type\n * error everywhere it has not been handled; adding a *role* is a type error in\n * every level at once. That is the guarantee — not a convention.\n *\n * ## Why a ladder and not a polarity, at two levels\n *\n * The ladder ran to four rungs — `midnight`, `dim`, `bright`, `white` — on the\n * argument that two themes can share a polarity and still want different\n * grounds. That argument was sound; the assumption under it was not. Nothing\n * consumed the fourth rung and nothing reached the second, so three of the four\n * were paid for and never collected. The pair is `midnight` and `light`.\n *\n * Polarity nevertheless stays a *declared property of a level* rather than the\n * axis, and this is worth stating because the collapse is usually assumed to\n * end it. It does not, because **neither level is named for its polarity**:\n * `midnight` has polarity `dark`, `sketch` has polarity `light`. So\n * {@link SYSTEM_LEVEL} still has a real job — mapping two media states onto two\n * names that are neither of them — and a `Record<Polarity, T>` remains a\n * different type from a `Record<ThemeLevel, T>`. Had the levels been called\n * `dark` and `light` the two would have collapsed into one thing and polarity\n * could have gone with them.\n *\n * Both polarity variants therefore survive: `dark:` and `light:` each span a\n * polarity rather than naming a level, so neither collides with a level\n * variant. `midnight` names a ground; `sketch` names a surface and a way of\n * drawing on it. Neither is a synonym for its polarity, which is the property\n * that keeps a third level cheap.\n *\n * ## Why every value is a literal\n *\n * The previous token layer derived surfaces, text and borders from two poles\n * with `color-mix()` percentages. Percentages tuned against near-black do not\n * hold at the light end — `border-subtle` at 28% is a visible hairline on\n * black and invisible on paper. Literals also make the ladder *checkable*:\n * `pnpm check:contrast` can compute every text-on-surface ratio without a\n * browser — 260 pairs, 130 per Level, including every Hue against every\n * ground, which is what makes hand-tuned literals sustainable rather than\n * twenty times the manual review. Stated per Level deliberately: 440 was\n * written here in the commit that collapsed four Levels to two, so the total\n * was stale the moment it was typed.\n *\n * The one exception is `surface.overlay`, which is a scrim and needs alpha.\n */\n\nimport type { BorderTone, Emphasis, Hue, HueRef, Intent, Surface, TextTone } from '../lib/theme';\n\n/**\n * The ladder, ordered from darkest to lightest. Order is meaningful: it is what\n * `cycleLevel` steps through and what the Storybook toolbar and walkthrough\n * matrix render in.\n */\nexport const THEME_LEVELS = ['midnight', 'sketch'] as const;\n\n/** A level of the ladder. */\nexport type ThemeLevel = (typeof THEME_LEVELS)[number];\n\n/**\n * Whether a level reads as dark-on-light or light-on-dark. Declared per level\n * rather than inferred, because it drives things a luminance calculation should\n * not silently decide: the `color-scheme` property (which controls native form\n * controls, scrollbars and the caret), the `dark:`/`light:` Tailwind variants,\n * and the `prefers-color-scheme` mapping.\n */\nexport type Polarity = 'dark' | 'light';\n\n/**\n * The three colours that do **not** vary by level.\n *\n * Every other colour in this module is declared once per level and swaps with\n * it. These do not, and that invariance is the whole point: `#ffffff` means\n * *white*, not \"whatever this level calls its lightest ground\". The\n * `--color-black` / `--color-white` compat aliases are the counter-example —\n * they track `surface.base` and `text.primary`, so `--color-black` resolves to\n * `#ffffff` on the `white` level. A token named for an appearance, holding the\n * opposite appearance.\n *\n * Use these when true black or true white is genuinely meant: a print surface,\n * an SVG fill, a scrim. Not as a page ground — that is `surface.base`.\n */\nexport const FIXED_COLOURS = {\n black: '#000000',\n white: '#ffffff',\n transparent: 'transparent',\n} as const;\n\nexport type FixedColour = keyof typeof FIXED_COLOURS;\n\n/**\n * The Hue vocabulary, in wheel order starting at red.\n *\n * Ten is the ceiling, not a comfortable middle: the tightest pair already sits\n * at deltaE 0.049 on the light levels, and an eleventh candidate was dropped\n * during derivation for landing 13 degrees from the measured blue at deltaE\n * 0.024. Adding one means checking the separation gate, not just this array.\n */\nexport const PALETTE_HUES = [\n 'red',\n 'orange',\n 'yellow',\n 'green',\n 'teal',\n 'cyan',\n 'blue',\n 'violet',\n 'magenta',\n 'pink',\n] as const satisfies readonly Hue[];\n\n/**\n * Every colour a level must define. Adding a field here is a compile error in\n * all four levels until each one answers it — the property that a bare string\n * union cannot provide.\n */\nexport interface LevelDefinition {\n /** Human-facing name, used in the Storybook toolbar and the sandbox. */\n readonly label: string;\n /** One line on what the level is for. Rendered in the sandbox. */\n readonly description: string;\n readonly polarity: Polarity;\n readonly surface: Readonly<Record<Surface, string>>;\n readonly text: Readonly<Record<TextTone, string>>;\n readonly border: Readonly<Record<BorderTone, string>>;\n readonly accent: Readonly<Record<Emphasis, string>>;\n readonly intent: Readonly<Record<Intent, string>>;\n /**\n * The Hue vocabulary — appearance, not job. Declared *below* the Roles above\n * and referenced by them, never derived from them. Only a Target with no\n * notion of jobs addresses these: a terminal's sixteen ANSI slots, half a\n * JetBrains scheme, a generated diagram needing N distinguishable colours.\n *\n * Every entry clears 5.5:1 against the tightest of this level's three\n * grounds — above WCAG AA, so an editor still has room to tint the ground\n * behind it. `bright` fills the upper eight ANSI slots at a 4.5:1 floor,\n * since those carry emphasis rather than body text.\n */\n readonly palette: Readonly<Record<Hue, string>>;\n /** The `bright` half of each Hue. ANSI's upper eight, and nothing else yet. */\n readonly paletteBright: Readonly<Record<Hue, string>>;\n /**\n * Which Hue each Role is. **The declared lookup ADR 0001 requires.**\n *\n * The ADR rejects deriving this by inspecting hex — ambiguous when two Roles\n * share a value, lossy when no Hue sits on a Role's. So it is declared, and\n * `check:contrast` asserts the Role's value actually equals the Hue's. That\n * assertion is the point: without it the two vocabularies drift silently,\n * which is the failure the ADR's rejected \"both, independently declared\"\n * option was rejected for — and which this repo shipped for exactly one\n * commit, with two pinks on `midnight` 0.036 apart.\n */\n readonly accentHue: Readonly<Record<Emphasis, HueRef>>;\n /** As `accentHue`, for the meaning Roles. */\n readonly intentHue: Readonly<Record<Intent, HueRef>>;\n /** Colour of the hard offset shadows. Normally tracks `border.strong`. */\n readonly shadow: string;\n}\n\n/**\n * Surfaces do not move in the same direction on every level, and that is the\n * point of declaring them.\n *\n * On the dark levels a raised card sits *above* the page and so lightens. On\n * `bright` — a warm desk with paper on it — a raised card is a fresh white\n * sheet, so it also lightens, but past the ground rather than away from black.\n * On `white` there is nowhere lighter to go, so elevation is carried by the\n * hard border and shadow while the panel itself takes a faint cool grey. A\n * single derived formula cannot express those three behaviours; four literal\n * ramps can.\n */\nexport const LEVELS: Readonly<Record<ThemeLevel, LevelDefinition>> = {\n midnight: {\n label: 'Midnight',\n description: 'Neon on blue-black. The maximal end of the ladder.',\n polarity: 'dark',\n surface: {\n base: '#0a0a1a',\n raised: '#14142a',\n sunken: '#050510',\n overlay: 'rgba(5, 5, 16, 0.82)',\n },\n text: {\n primary: '#ffffff',\n secondary: '#c3c3d4',\n // Was #8b8ba3: 5.90:1, but APCA Lc -41, the weakest text pair in the\n // system. WCAG 2 overrates light-on-dark. Now 7.39:1 / Lc -50, still 21 Lc\n // below `secondary`. accent.quiet keeps the old value — a fill, not text.\n muted: '#9d9db4',\n inverse: '#0a0a1a',\n },\n border: {\n strong: '#ffffff',\n default: '#9d9db4',\n subtle: '#33334d',\n },\n accent: {\n primary: '#22d3ee',\n secondary: '#facc15',\n // Was #ec4899 — the brand pink, and 5.12:1, which cleared the 4.5 Role\n // floor while `palette.pink` sat at 5.90:1. Two pinks 0.036 apart in one\n // level, invisibly different and therefore worse than visibly. Now the\n // Hue's value, per `accentHue` below.\n tertiary: '#f955a4',\n quiet: '#8b8ba3',\n },\n intent: {\n info: '#22d3ee',\n success: '#39ff14',\n warning: '#facc15',\n danger: '#f955a4',\n },\n accentHue: {\n primary: 'cyan',\n secondary: 'yellow',\n tertiary: 'pink',\n quiet: 'neutral',\n },\n intentHue: {\n info: 'cyan',\n success: 'green',\n warning: 'yellow',\n danger: 'pink',\n },\n palette: {\n red: '#ff586e', // 16deg — lifted from #f43f5e (dim accent.tertiary), 4.59:1\n orange: '#ff8c00', // 58deg — kept — 410 commits\n yellow: '#facc15', // 92deg — kept — 533 commits, brand\n green: '#39ff14', // 142deg — kept — 533 commits, brand\n teal: '#34d399', // 163deg — kept — 364 commits\n cyan: '#22d3ee', // 212deg — kept — 534 commits, brand\n blue: '#38bdf8', // 233deg — kept — dim accent.primary, 233deg (a sky blue by heritage)\n violet: '#c3afff', // 295deg — new — 57 tried across six years, none canonical\n magenta: '#ff1cff', // 328deg — lifted from #ff00ff, 5.37:1\n pink: '#f955a4', // 354deg — lifted from #ec4899 (brand), 4.77:1 — deltaE 0.036\n },\n paletteBright: {\n red: '#ff939b',\n orange: '#ffba85',\n yellow: '#ffeaab',\n green: '#c0ffb8',\n teal: '#00f7ae',\n cyan: '#88ebff',\n blue: '#8cd7ff',\n violet: '#ddd4ff',\n magenta: '#ff88fd',\n pink: '#ff8ebe',\n },\n shadow: '#ffffff',\n },\n\n sketch: {\n label: 'Sketch',\n description: 'Warm paper and pen ink. The light half of the pair.',\n polarity: 'light',\n surface: {\n base: '#f5f3ec', // sketch paper\n raised: '#ffffff', // FIXED_COLOURS.white — a fresh sheet on the desk\n sunken: '#efeadf', // the binding ground: every value below is solved against this\n overlay: 'rgba(35, 38, 46, 0.72)',\n },\n text: {\n primary: '#23262e', // sketch ink\n secondary: '#33373f',\n // Was #6a6252: 5.03:1 / APCA Lc 68 on `sunken`, carried by 11px labels.\n // Now 6.16:1 / Lc 73 there. Darker still (#4f4a3d) left muted 7 Lc from\n // `secondary`, and the two tones stopped reading as two. accent.quiet keeps\n // the old value.\n muted: '#5c5546',\n inverse: '#f5f3ec',\n },\n border: {\n strong: '#23262e',\n default: '#8f8672',\n subtle: '#c8c3b4', // darker than sketch's #d8d3c4, which was 1.25:1 on a pure-white `raised`\n },\n accent: {\n primary: '#1450d7', // palette.blue\n secondary: '#006b2e', // palette.green\n tertiary: '#bd0010', // palette.red\n quiet: '#6a6252',\n },\n intent: {\n info: '#1450d7',\n success: '#006b2e',\n warning: '#974503',\n danger: '#bd0010',\n },\n accentHue: {\n primary: 'blue',\n secondary: 'green',\n tertiary: 'red',\n quiet: 'neutral',\n },\n intentHue: {\n info: 'blue',\n success: 'green',\n warning: 'orange',\n danger: 'red',\n },\n palette: {\n red: '#bd0010', // 16deg — was #dc2626, 4.03:1 — failed WCAG AA\n orange: '#974503', // 58deg — was #9a4708, 5.34:1\n yellow: '#705a00', // 92deg — new — no light yellow was ever authored\n green: '#006b2e', // 142deg — was #15803d, 4.18:1 — failed\n teal: '#006859', // 163deg — new\n cyan: '#006675', // 212deg — new\n blue: '#1450d7', // 233deg — was #2563eb, 4.31:1 — failed\n violet: '#7d00f4', // 295deg — new\n magenta: '#a300ad', // 328deg — new\n pink: '#b4006c', // 354deg — new\n },\n paletteBright: {\n red: '#8f002a',\n orange: '#6c3700',\n yellow: '#534200',\n green: '#024f00',\n teal: '#004d34',\n cyan: '#004b56',\n blue: '#004e6d',\n violet: '#6000bd',\n magenta: '#7f0080',\n pink: '#8a0052',\n },\n shadow: '#23262e',\n },\n};\n\n/** The level applied when nothing else has been chosen or persisted. */\nexport const DEFAULT_LEVEL: ThemeLevel = 'midnight';\n\n/**\n * Which level each OS colour-scheme preference maps to. `prefers-color-scheme`\n * is binary, so the ladder has to nominate a representative at each end; making\n * that an explicit constant means it can be changed in one place rather than\n * being an accident of ordering.\n */\nexport const SYSTEM_LEVEL: Readonly<Record<Polarity, ThemeLevel>> = {\n dark: 'midnight',\n light: 'sketch',\n};\n\n/**\n * The weight running text is drawn at, by polarity — not by level.\n *\n * Light strokes on a dark ground spread optically and read heavier than they\n * are; dark strokes on a light ground do not, so one weight cannot look the\n * same at both ends. The type was tuned on `midnight`, and on `sketch` body copy\n * read thin, most visibly on paper (#372). Same argument as `syntaxEmphasis`:\n * light grounds compress the usable luma band, so weight does work colour cannot.\n *\n * Keyed on Polarity because a Level carries colour and only colour (ADR 0004,\n * asserted in `media.test.ts`), and this is exactly the non-colour,\n * light-vs-dark property the polarity variants exist for. Emitted per level as\n * `--ds-ink-weight` and applied in `styles.css`'s base layer, so any weight\n * utility still wins. Inter and Space Grotesk are variable and draw 450 as\n * asked; IBM Plex Mono ships static cuts and resolves 450 to its 500.\n */\nexport const INK_WEIGHT: Readonly<Record<Polarity, number>> = {\n dark: 400,\n light: 450,\n};\n\n/** Type guard for values arriving from `localStorage`, URLs, or props. */\nexport function isThemeLevel(value: unknown): value is ThemeLevel {\n return typeof value === 'string' && (THEME_LEVELS as readonly string[]).includes(value);\n}\n\n/** The next level on the ladder, wrapping from the lightest back to the darkest. */\nexport function nextLevel(level: ThemeLevel): ThemeLevel {\n const index = THEME_LEVELS.indexOf(level);\n return THEME_LEVELS[(index + 1) % THEME_LEVELS.length]!;\n}\n\n/**\n * Exhaustiveness guard for `switch` statements over a level.\n *\n * Prefer a `Record<ThemeLevel, T>` where the branches are values — it is\n * checked without any call site remembering to add a default. Where control\n * flow genuinely has to branch, end the switch with `assertNever(level)` so\n * adding a fifth level fails the build instead of falling through.\n */\nexport function assertNever(value: never, message = 'Unhandled theme level'): never {\n throw new Error(`${message}: ${String(value)}`);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,115 @@
1
+ // src/theme/media.ts
2
+ var MEDIA = ["web", "video", "graphic"];
3
+ var CSS_MEDIUM = "web";
4
+ var MEDIA_DEFINITIONS = {
5
+ web: {
6
+ label: "Web",
7
+ description: "CSS pixels, reflowing, user-zoomable. Wall clock.",
8
+ unit: "rem",
9
+ spacing: [0, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 6, 8],
10
+ type: {
11
+ caption: { size: 0.75, lineHeight: 1.4 },
12
+ body: { size: 1, lineHeight: 1.6 },
13
+ lead: { size: 1.125, lineHeight: 1.55 },
14
+ title: { size: 1.5, lineHeight: 1.2 },
15
+ display: { size: 2.25, lineHeight: 1.05 },
16
+ hero: { size: 3, lineHeight: 0.95 }
17
+ },
18
+ weight: { regular: 400, bold: 700, black: 900 },
19
+ borderWidth: { hairline: 1, edge: 2, heavy: 4 },
20
+ shadowOffset: { sm: 2, md: 4, lg: 6 },
21
+ radius: { none: 0, soft: 0 },
22
+ motion: { instant: 0, quick: 150, considered: 300, easing: "cubic-bezier(0.4, 0, 0.2, 1)" },
23
+ layer: { base: 0, raised: 10, overlay: 100, top: 1e3 },
24
+ focusRing: { width: 2, offset: 2 },
25
+ contrastFloor: { role: 4.5, hue: 5.5, hueBright: 4.5 }
26
+ },
27
+ video: {
28
+ label: "Video",
29
+ description: "A fixed 1920\xD71080 frame. No reflow, no zoom. Frame clock.",
30
+ unit: "px",
31
+ // A 1080p frame is read at distance and often at half scale in a PR player,
32
+ // so the grid is coarser and the smallest step is larger than the web's.
33
+ spacing: [0, 8, 16, 24, 32, 48, 64, 96, 128, 192, 256],
34
+ // The evaluation's note, in numbers: "the type scale a 1080p frame needs
35
+ // and a 16px web page does not". Nothing here is the web's scale scaled.
36
+ type: {
37
+ caption: { size: 24, lineHeight: 1.35 },
38
+ body: { size: 32, lineHeight: 1.5 },
39
+ lead: { size: 40, lineHeight: 1.45 },
40
+ title: { size: 56, lineHeight: 1.15 },
41
+ display: { size: 88, lineHeight: 1.05 },
42
+ hero: { size: 128, lineHeight: 0.95 }
43
+ },
44
+ weight: { regular: 500, bold: 700, black: 900 },
45
+ // Compression eats a 2px edge. 4 is the thinnest line that survives a
46
+ // re-encode intact, which is why this is not the web's number.
47
+ borderWidth: { hairline: 2, edge: 4, heavy: 8 },
48
+ shadowOffset: { sm: 6, md: 12, lg: 18 },
49
+ radius: { none: 0, soft: 0 },
50
+ // Frames, not milliseconds. At 30fps `quick` is 5 frames. Durations must be
51
+ // whole frames or a render lands mid-transition and the still is a smear —
52
+ // which is the failure `remotion-evaluation.md` measured for the web's
53
+ // wall-clock transitions.
54
+ motion: { instant: 0, quick: 5, considered: 12, easing: "easeInOutCubic" },
55
+ layer: { base: 0, raised: 10, overlay: 100, top: 1e3 },
56
+ focusRing: { width: 4, offset: 4 },
57
+ // A projected or compressed frame loses local contrast, so every floor
58
+ // rises. This is the per-Medium indexing ADR 0004 calls for.
59
+ contrastFloor: { role: 5.5, hue: 7, hueBright: 5.5 }
60
+ },
61
+ graphic: {
62
+ label: "Graphic",
63
+ description: "A fixed viewBox. No reflow. No time base at all.",
64
+ unit: "viewBox",
65
+ spacing: [0, 2, 4, 8, 12, 16, 24, 32, 48, 64, 96],
66
+ type: {
67
+ caption: { size: 8, lineHeight: 1.3 },
68
+ body: { size: 12, lineHeight: 1.4 },
69
+ lead: { size: 14, lineHeight: 1.4 },
70
+ title: { size: 20, lineHeight: 1.2 },
71
+ display: { size: 32, lineHeight: 1.05 },
72
+ hero: { size: 48, lineHeight: 1 }
73
+ },
74
+ weight: { regular: 400, bold: 700, black: 900 },
75
+ // A stroke in viewBox units scales with the drawing, so 1 is a real
76
+ // hairline here rather than a sub-pixel risk.
77
+ borderWidth: { hairline: 0.5, edge: 1, heavy: 2 },
78
+ shadowOffset: { sm: 1, md: 2, lg: 3 },
79
+ radius: { none: 0, soft: 0 },
80
+ // No time base. Zero rather than absent, so `Record<Medium, T>` stays
81
+ // total and a caller cannot forget to handle this Medium.
82
+ motion: { instant: 0, quick: 0, considered: 0, easing: "none" },
83
+ layer: { base: 0, raised: 1, overlay: 2, top: 3 },
84
+ focusRing: { width: 1, offset: 1 },
85
+ // A diagram is often reproduced in print or at thumbnail size, and unlike
86
+ // a video it cannot rely on motion to carry meaning.
87
+ contrastFloor: { role: 5.5, hue: 7, hueBright: 5.5 }
88
+ }
89
+ };
90
+ var LOOP_KEYFRAMES = {
91
+ spin: { to: { transform: "rotate(360deg)" } },
92
+ pulse: { "0%, 100%": { opacity: "1" }, "50%": { opacity: "0.45" } },
93
+ // The indeterminate progress sweep. The indicator is a third of the track,
94
+ // so `-100%` starts it fully off the leading edge and `400%` carries it
95
+ // fully past the trailing one.
96
+ track: { "0%": { transform: "translateX(-100%)" }, "100%": { transform: "translateX(400%)" } }
97
+ };
98
+ var LOOPS = {
99
+ spin: { keyframes: "spin", periods: 3, easing: "linear" },
100
+ "spin-slow": { keyframes: "spin", periods: 9, easing: "linear" },
101
+ pulse: { keyframes: "pulse", periods: 4, easing: "medium" },
102
+ track: { keyframes: "track", periods: 5, easing: "medium" }
103
+ };
104
+ function isMedium(value) {
105
+ return typeof value === "string" && MEDIA.includes(value);
106
+ }
107
+ export {
108
+ CSS_MEDIUM,
109
+ LOOPS,
110
+ LOOP_KEYFRAMES,
111
+ MEDIA,
112
+ MEDIA_DEFINITIONS,
113
+ isMedium
114
+ };
115
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/theme/media.ts"],"sourcesContent":["/**\n * The Medium axis — the single place a spatial or temporal value is written.\n *\n * Companion to `levels.ts`, and deliberately its mirror image. A Level varies\n * *colour* and is selected at runtime by `data-theme`; a Medium varies\n * *geometry and time* and is selected at build time by which artifact is being\n * emitted. No token varies on both, and a token that varies on neither is\n * invariant and lives in `FIXED_COLOURS`. See\n * `docs/adr/0004-two-axes-level-and-medium.md`.\n *\n * ## What a Medium is\n *\n * A unit system and a time base. Not a format — that is a Target, and the two\n * do not correspond: four Targets (VS Code, Zed, Shiki, a terminal) share one\n * host geometry and declare no scale at all, while `graphic` is emitted to\n * several formats. **Developer themes are not a Medium**; a theme may not set\n * an editor's line height or a terminal's cell size.\n *\n * ## Why this file exists at all\n *\n * Everything the token layer declared before this varied by Level, so #49 —\n * spacing, type scale, motion, z-index — had nowhere to land. It kept having to\n * guess whether its values were fields on `LevelDefinition` or a flat sibling\n * record, and both answers are wrong: a Level-keyed scale authors every spacing\n * value twice for two Levels that must agree, and a single flat record forces a\n * 1080p frame and a 16px page to share a type scale, which they cannot.\n */\n\n/**\n * Every Medium, in the order they are emitted.\n *\n * Adding one is a compile error at every `Record<Medium, T>` until each answers\n * it — the same guarantee `THEME_LEVELS` provides on the other axis, and the\n * reason the enumeration is reversible at a known cost rather than a guess.\n */\nexport const MEDIA = ['web', 'video', 'graphic'] as const;\n\nexport type Medium = (typeof MEDIA)[number];\n\n/** The Medium `theme.css` and the `@theme` block carry. Exactly one, per ADR 0004. */\nexport const CSS_MEDIUM: Medium = 'web';\n\n/**\n * A type step. `size` and `lineHeight` travel together because pinning a font\n * size without its leading is what let a badge inherit an article's unitless\n * 1.5 — recorded in `Button.tsx` and the reason `text-[0.75rem]` appears there\n * rather than `text-xs`.\n *\n * Units are the Medium's own: CSS `rem` on `web`, frame-relative `px` on\n * `video`, `viewBox` units on `graphic`. A number here means \"in this Medium's\n * unit\", which is why the field is a number and not a string.\n */\nexport interface TypeStep {\n readonly size: number;\n readonly lineHeight: number;\n}\n\n/** The type scale's steps, smallest first. Named, not indexed, so a step can be removed. */\nexport type TypeStepName = 'caption' | 'body' | 'lead' | 'title' | 'display' | 'hero';\n\n/** Motion. `graphic` has no time base, so its durations are zero rather than absent. */\nexport interface Motion {\n /** Milliseconds on `web` (wall clock); frames on `video` (frame clock). */\n readonly instant: number;\n readonly quick: number;\n readonly considered: number;\n /** A CSS easing function on `web`; the same curve as a name elsewhere. */\n readonly easing: string;\n}\n\n/**\n * Every geometric and temporal value a Medium must answer.\n *\n * Adding a field here is a compile error in all three Media until each one\n * answers it — the property a bare record cannot provide, and the same\n * discipline `LevelDefinition` keeps for colour.\n */\nexport interface MediumDefinition {\n readonly label: string;\n readonly description: string;\n\n /**\n * The unit a number in this definition is measured in. Documentation rather\n * than arithmetic: nothing multiplies by it, but a reader of an emitted\n * artifact needs to know whether `4` is a CSS pixel or a frame pixel.\n */\n readonly unit: 'rem' | 'px' | 'viewBox';\n\n /** Spacing scale, in this Medium's unit. The brutalist grid is a 4-step. */\n readonly spacing: readonly number[];\n\n readonly type: Readonly<Record<TypeStepName, TypeStep>>;\n\n /** Font weights this Medium uses. Brutalism leans on 700/900, not 400/500. */\n readonly weight: Readonly<Record<'regular' | 'bold' | 'black', number>>;\n\n /**\n * Border width. The load-bearing weight is 2 on `web` — edges are drawn, not\n * implied — and a video frame needs more to survive compression.\n */\n readonly borderWidth: Readonly<Record<'hairline' | 'edge' | 'heavy', number>>;\n\n /**\n * Hard offset shadows. Three steps, and no blur field: a blurred shadow is a\n * different design language, so its absence is the decision.\n */\n readonly shadowOffset: Readonly<Record<'sm' | 'md' | 'lg', number>>;\n\n /**\n * Radius. Zero is a value on this scale, not the absence of one — which is\n * what makes `src/styles.css`'s `border-radius: 0px !important` a reset that\n * reaches the whole consumer document (#54) rather than a token.\n */\n readonly radius: Readonly<Record<'none' | 'soft', number>>;\n\n readonly motion: Motion;\n\n /** Stacking layers. A `graphic` has no stacking context to speak of. */\n readonly layer: Readonly<Record<'base' | 'raised' | 'overlay' | 'top', number>>;\n\n /** Focus-ring geometry. The colour is a Level's business, the width is this one's. */\n readonly focusRing: Readonly<Record<'width' | 'offset', number>>;\n\n /**\n * The contrast floor a Gate enforces *for this Medium*.\n *\n * The asymmetry ADR 0004 says is most likely to be got wrong later: colour\n * values do not vary by Medium, but the floors they must clear do. A\n * projected frame, a compression-damaged video and an unantialiased terminal\n * cell are not the browser's legibility problem.\n *\n * `web` restates the numbers already in `MINIMUM_RATIO`, so nothing moves on\n * merge; the other two are stricter because they have to be.\n */\n readonly contrastFloor: Readonly<Record<'role' | 'hue' | 'hueBright', number>>;\n}\n\n/**\n * The three Media.\n *\n * `web`'s values are the ones already shipping — the 4px grid, the 2px edge,\n * the 2/4/6 offset shadows, zero radius — read out of the components rather\n * than chosen here, so declaring them changes no rendering.\n */\nexport const MEDIA_DEFINITIONS: Readonly<Record<Medium, MediumDefinition>> = {\n web: {\n label: 'Web',\n description: 'CSS pixels, reflowing, user-zoomable. Wall clock.',\n unit: 'rem',\n spacing: [0, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 6, 8],\n type: {\n caption: { size: 0.75, lineHeight: 1.4 },\n body: { size: 1, lineHeight: 1.6 },\n lead: { size: 1.125, lineHeight: 1.55 },\n title: { size: 1.5, lineHeight: 1.2 },\n display: { size: 2.25, lineHeight: 1.05 },\n hero: { size: 3, lineHeight: 0.95 },\n },\n weight: { regular: 400, bold: 700, black: 900 },\n borderWidth: { hairline: 1, edge: 2, heavy: 4 },\n shadowOffset: { sm: 2, md: 4, lg: 6 },\n radius: { none: 0, soft: 0 },\n motion: { instant: 0, quick: 150, considered: 300, easing: 'cubic-bezier(0.4, 0, 0.2, 1)' },\n layer: { base: 0, raised: 10, overlay: 100, top: 1000 },\n focusRing: { width: 2, offset: 2 },\n contrastFloor: { role: 4.5, hue: 5.5, hueBright: 4.5 },\n },\n\n video: {\n label: 'Video',\n description: 'A fixed 1920×1080 frame. No reflow, no zoom. Frame clock.',\n unit: 'px',\n // A 1080p frame is read at distance and often at half scale in a PR player,\n // so the grid is coarser and the smallest step is larger than the web's.\n spacing: [0, 8, 16, 24, 32, 48, 64, 96, 128, 192, 256],\n // The evaluation's note, in numbers: \"the type scale a 1080p frame needs\n // and a 16px web page does not\". Nothing here is the web's scale scaled.\n type: {\n caption: { size: 24, lineHeight: 1.35 },\n body: { size: 32, lineHeight: 1.5 },\n lead: { size: 40, lineHeight: 1.45 },\n title: { size: 56, lineHeight: 1.15 },\n display: { size: 88, lineHeight: 1.05 },\n hero: { size: 128, lineHeight: 0.95 },\n },\n weight: { regular: 500, bold: 700, black: 900 },\n // Compression eats a 2px edge. 4 is the thinnest line that survives a\n // re-encode intact, which is why this is not the web's number.\n borderWidth: { hairline: 2, edge: 4, heavy: 8 },\n shadowOffset: { sm: 6, md: 12, lg: 18 },\n radius: { none: 0, soft: 0 },\n // Frames, not milliseconds. At 30fps `quick` is 5 frames. Durations must be\n // whole frames or a render lands mid-transition and the still is a smear —\n // which is the failure `remotion-evaluation.md` measured for the web's\n // wall-clock transitions.\n motion: { instant: 0, quick: 5, considered: 12, easing: 'easeInOutCubic' },\n layer: { base: 0, raised: 10, overlay: 100, top: 1000 },\n focusRing: { width: 4, offset: 4 },\n // A projected or compressed frame loses local contrast, so every floor\n // rises. This is the per-Medium indexing ADR 0004 calls for.\n contrastFloor: { role: 5.5, hue: 7, hueBright: 5.5 },\n },\n\n graphic: {\n label: 'Graphic',\n description: 'A fixed viewBox. No reflow. No time base at all.',\n unit: 'viewBox',\n spacing: [0, 2, 4, 8, 12, 16, 24, 32, 48, 64, 96],\n type: {\n caption: { size: 8, lineHeight: 1.3 },\n body: { size: 12, lineHeight: 1.4 },\n lead: { size: 14, lineHeight: 1.4 },\n title: { size: 20, lineHeight: 1.2 },\n display: { size: 32, lineHeight: 1.05 },\n hero: { size: 48, lineHeight: 1 },\n },\n weight: { regular: 400, bold: 700, black: 900 },\n // A stroke in viewBox units scales with the drawing, so 1 is a real\n // hairline here rather than a sub-pixel risk.\n borderWidth: { hairline: 0.5, edge: 1, heavy: 2 },\n shadowOffset: { sm: 1, md: 2, lg: 3 },\n radius: { none: 0, soft: 0 },\n // No time base. Zero rather than absent, so `Record<Medium, T>` stays\n // total and a caller cannot forget to handle this Medium.\n motion: { instant: 0, quick: 0, considered: 0, easing: 'none' },\n layer: { base: 0, raised: 1, overlay: 2, top: 3 },\n focusRing: { width: 1, offset: 1 },\n // A diagram is often reproduced in print or at thumbnail size, and unlike\n // a video it cannot rely on motion to carry meaning.\n contrastFloor: { role: 5.5, hue: 7, hueBright: 5.5 },\n },\n};\n\n/**\n * The looping animations the feedback primitives wear — `Spinner`, `Skeleton`\n * and indeterminate `Progress` — as `--animate-ds-<name>` tokens (#302).\n *\n * Each one splits across the two variance classes this file already knows:\n *\n * - its **period** is time, so it is a multiple of the Medium's `considered`\n * duration rather than a literal. A spinner is not a 300ms interaction, but\n * its period should still move when the Medium's time base does; `900ms`\n * would throw that away. The generator writes it as\n * `calc(var(--ds-duration-considered) * N)`, so it follows `theme.css`'s one\n * Medium without a second number being authored.\n * - its **keyframe** is a shape, and is the same on every Level and in every\n * Medium — invariant, declared once, the way `FIXED_COLOURS` is for colour.\n *\n * They were declared in `styles.css` from 0.10.0 on the argument that a\n * keyframe is \"not a value on either axis\". The consequence was that a\n * consumer on the documented contract — `tailwindcss` then `theme.css`, no\n * `styles.css` — got `animate-ds-spin` in the compiled components and no theme\n * variable to back it, so Tailwind emitted nothing and all three stood still.\n * A utility a component writes is part of the token surface, whatever axis its\n * parts sit on, so it is generated into `theme.css` from here.\n *\n * `spin-slow` is the `prefers-reduced-motion` partner of `spin`, not a\n * decorative alternative: a stopped spinner says \"hung\", which is the opposite\n * of the thing being communicated, so the rotation survives the preference and\n * its speed does not. `pulse` is what `Skeleton` drops to instead — it changes\n * no geometry, only opacity — and under the preference even that stops. The\n * preference itself is applied at the call site (`motion-reduce:`), not here.\n *\n * `linear` on the rotation is deliberate and is not a missing token: an eased\n * loop accelerates and decelerates once per revolution, which reads as a\n * stutter rather than as work in progress. The two with a beginning and an end\n * take the Medium's easing.\n */\nexport interface Loop {\n /** Which entry of `LOOP_KEYFRAMES` it runs. Two loops may share one shape. */\n readonly keyframes: LoopKeyframesName;\n /** The period, as a multiple of the Medium's `motion.considered`. */\n readonly periods: number;\n /** `linear`, or the Medium's own `motion.easing`. */\n readonly easing: 'linear' | 'medium';\n}\n\nexport type LoopKeyframesName = 'spin' | 'pulse' | 'track';\n\n/** A keyframe as selector → declarations. Emitted as `@keyframes ds-<name>`. */\nexport type Keyframes = Readonly<Record<string, Readonly<Record<string, string>>>>;\n\nexport const LOOP_KEYFRAMES: Readonly<Record<LoopKeyframesName, Keyframes>> = {\n spin: { to: { transform: 'rotate(360deg)' } },\n pulse: { '0%, 100%': { opacity: '1' }, '50%': { opacity: '0.45' } },\n // The indeterminate progress sweep. The indicator is a third of the track,\n // so `-100%` starts it fully off the leading edge and `400%` carries it\n // fully past the trailing one.\n track: { '0%': { transform: 'translateX(-100%)' }, '100%': { transform: 'translateX(400%)' } },\n};\n\nexport const LOOPS: Readonly<Record<'spin' | 'spin-slow' | 'pulse' | 'track', Loop>> = {\n spin: { keyframes: 'spin', periods: 3, easing: 'linear' },\n 'spin-slow': { keyframes: 'spin', periods: 9, easing: 'linear' },\n pulse: { keyframes: 'pulse', periods: 4, easing: 'medium' },\n track: { keyframes: 'track', periods: 5, easing: 'medium' },\n};\n\n/** Type guard for a Medium arriving from an emitter flag or an env var. */\nexport function isMedium(value: unknown): value is Medium {\n return typeof value === 'string' && (MEDIA as readonly string[]).includes(value);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,105 @@
1
+ // src/theme/recommended.ts
2
+ import { DEFAULT_LEVEL, LEVELS } from "./levels.js";
3
+ var RECOMMENDED_COLOUR_NAMESPACES = [
4
+ "surface",
5
+ "content",
6
+ "edge",
7
+ "accent",
8
+ "intent"
9
+ ];
10
+ var RECOMMENDED_COLOUR_ROLES = {
11
+ surface: ["base", "raised", "sunken", "overlay"],
12
+ content: ["primary", "secondary", "muted", "inverse"],
13
+ edge: ["strong", "default", "subtle"],
14
+ accent: ["primary", "secondary", "tertiary", "quiet"],
15
+ intent: ["info", "success", "warning", "danger"]
16
+ };
17
+ var RECOMMENDED_COLOURS = {
18
+ midnight: {
19
+ surface: LEVELS.midnight.surface,
20
+ content: LEVELS.midnight.text,
21
+ edge: LEVELS.midnight.border,
22
+ accent: LEVELS.midnight.accent,
23
+ intent: LEVELS.midnight.intent
24
+ },
25
+ sketch: {
26
+ surface: LEVELS.sketch.surface,
27
+ content: LEVELS.sketch.text,
28
+ edge: LEVELS.sketch.border,
29
+ accent: LEVELS.sketch.accent,
30
+ intent: LEVELS.sketch.intent
31
+ }
32
+ };
33
+ function getRecommendedColours(level = DEFAULT_LEVEL) {
34
+ return RECOMMENDED_COLOURS[level] ?? RECOMMENDED_COLOURS[DEFAULT_LEVEL];
35
+ }
36
+ var RECOMMENDED_COLOUR_VARS = {
37
+ surface: {
38
+ base: "var(--ds-surface-base)",
39
+ raised: "var(--ds-surface-raised)",
40
+ sunken: "var(--ds-surface-sunken)",
41
+ overlay: "var(--ds-surface-overlay)"
42
+ },
43
+ content: {
44
+ primary: "var(--ds-text-primary)",
45
+ secondary: "var(--ds-text-secondary)",
46
+ muted: "var(--ds-text-muted)",
47
+ inverse: "var(--ds-text-inverse)"
48
+ },
49
+ edge: {
50
+ strong: "var(--ds-border-strong)",
51
+ default: "var(--ds-border-default)",
52
+ subtle: "var(--ds-border-subtle)"
53
+ },
54
+ accent: {
55
+ primary: "var(--ds-accent-primary)",
56
+ secondary: "var(--ds-accent-secondary)",
57
+ tertiary: "var(--ds-accent-tertiary)",
58
+ quiet: "var(--ds-accent-quiet)"
59
+ },
60
+ intent: {
61
+ info: "var(--ds-intent-info)",
62
+ success: "var(--ds-intent-success)",
63
+ warning: "var(--ds-intent-warning)",
64
+ danger: "var(--ds-intent-danger)"
65
+ }
66
+ };
67
+ var RECOMMENDED_COLOUR_PROPERTIES = [
68
+ "bg",
69
+ "text",
70
+ "border",
71
+ "ring",
72
+ "divide",
73
+ "placeholder",
74
+ "from",
75
+ "via",
76
+ "to"
77
+ ];
78
+ var RECOMMENDED_COLOUR_CLASSES = Object.freeze(
79
+ RECOMMENDED_COLOUR_PROPERTIES.flatMap(
80
+ (property) => RECOMMENDED_COLOUR_NAMESPACES.flatMap(
81
+ (namespace) => RECOMMENDED_COLOUR_ROLES[namespace].map(
82
+ (role) => `${property}-${namespace}-${role}`
83
+ )
84
+ )
85
+ )
86
+ );
87
+ var CLASS_SET = new Set(RECOMMENDED_COLOUR_CLASSES);
88
+ function isRecommendedColourClass(className) {
89
+ if (!className) return false;
90
+ const segments = className.split(":");
91
+ const base = segments.pop() ?? "";
92
+ if (segments.includes("dark") || segments.includes("light")) return false;
93
+ return CLASS_SET.has(base);
94
+ }
95
+ export {
96
+ RECOMMENDED_COLOURS,
97
+ RECOMMENDED_COLOUR_CLASSES,
98
+ RECOMMENDED_COLOUR_NAMESPACES,
99
+ RECOMMENDED_COLOUR_PROPERTIES,
100
+ RECOMMENDED_COLOUR_ROLES,
101
+ RECOMMENDED_COLOUR_VARS,
102
+ getRecommendedColours,
103
+ isRecommendedColourClass
104
+ };
105
+ //# sourceMappingURL=recommended.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/theme/recommended.ts"],"sourcesContent":["/**\n * Recommended colours and semantic tokens for consumers of @rtkelly-labs/design-system.\n *\n * ## Why this exists\n *\n * Rule 3 (AGENTS.md) requires that colours are addressed by role, never by hue:\n * `bg-surface-raised` or `text-intent-danger`, not `cyan`.\n *\n * Consumers (such as the blog, talk presentation platforms, canvas/SVG renderers,\n * and AI agent linters) need to depend on these roles as structured, typed data:\n *\n * 1. Runtime hex/rgba values per level for graphics, canvas, and charts (`RECOMMENDED_COLOURS`)\n * 2. CSS custom property expressions for inline styles and styled components (`RECOMMENDED_COLOUR_VARS`)\n * 3. The canonical Tailwind utility classnames for linters and validation (`RECOMMENDED_COLOUR_CLASSES`)\n */\n\nimport type { BorderTone, Emphasis, Intent, Surface, TextTone } from '../lib/theme';\nimport { DEFAULT_LEVEL, LEVELS, type ThemeLevel } from './levels';\n\n/**\n * The five semantic namespaces that make up the recommended colour surface.\n */\nexport const RECOMMENDED_COLOUR_NAMESPACES = [\n 'surface',\n 'content',\n 'edge',\n 'accent',\n 'intent',\n] as const;\n\nexport type RecommendedColourNamespace = (typeof RECOMMENDED_COLOUR_NAMESPACES)[number];\n\n/**\n * The roles defined in each recommended namespace.\n */\nexport const RECOMMENDED_COLOUR_ROLES = {\n surface: ['base', 'raised', 'sunken', 'overlay'],\n content: ['primary', 'secondary', 'muted', 'inverse'],\n edge: ['strong', 'default', 'subtle'],\n accent: ['primary', 'secondary', 'tertiary', 'quiet'],\n intent: ['info', 'success', 'warning', 'danger'],\n} as const;\n\n/**\n * Resolved colour literals (hex/rgba) for each recommended role on a single theme level.\n */\nexport interface RecommendedThemeColours {\n readonly surface: Readonly<Record<Surface, string>>;\n readonly content: Readonly<Record<TextTone, string>>;\n readonly edge: Readonly<Record<BorderTone, string>>;\n readonly accent: Readonly<Record<Emphasis, string>>;\n readonly intent: Readonly<Record<Intent, string>>;\n}\n\n/**\n * The recommended colour values for each level of the theme ladder.\n *\n * Use this in non-CSS environments (canvas, SVG generation, email templates, charts)\n * where CSS custom properties cannot resolve.\n */\nexport const RECOMMENDED_COLOURS: Readonly<Record<ThemeLevel, RecommendedThemeColours>> = {\n midnight: {\n surface: LEVELS.midnight.surface,\n content: LEVELS.midnight.text,\n edge: LEVELS.midnight.border,\n accent: LEVELS.midnight.accent,\n intent: LEVELS.midnight.intent,\n },\n sketch: {\n surface: LEVELS.sketch.surface,\n content: LEVELS.sketch.text,\n edge: LEVELS.sketch.border,\n accent: LEVELS.sketch.accent,\n intent: LEVELS.sketch.intent,\n },\n};\n\n/**\n * Return the recommended colours for a given theme level, defaulting to the\n * ladder's own default — read from `levels.ts`, never restated here, so the\n * two cannot drift.\n */\nexport function getRecommendedColours(level: ThemeLevel = DEFAULT_LEVEL): RecommendedThemeColours {\n return RECOMMENDED_COLOURS[level] ?? RECOMMENDED_COLOURS[DEFAULT_LEVEL];\n}\n\n/**\n * CSS custom property expressions for every recommended role, aligned with the\n * public Tailwind utility namespaces (`surface`, `content`, `edge`, `accent`, `intent`).\n */\nexport const RECOMMENDED_COLOUR_VARS = {\n surface: {\n base: 'var(--ds-surface-base)',\n raised: 'var(--ds-surface-raised)',\n sunken: 'var(--ds-surface-sunken)',\n overlay: 'var(--ds-surface-overlay)',\n },\n content: {\n primary: 'var(--ds-text-primary)',\n secondary: 'var(--ds-text-secondary)',\n muted: 'var(--ds-text-muted)',\n inverse: 'var(--ds-text-inverse)',\n },\n edge: {\n strong: 'var(--ds-border-strong)',\n default: 'var(--ds-border-default)',\n subtle: 'var(--ds-border-subtle)',\n },\n accent: {\n primary: 'var(--ds-accent-primary)',\n secondary: 'var(--ds-accent-secondary)',\n tertiary: 'var(--ds-accent-tertiary)',\n quiet: 'var(--ds-accent-quiet)',\n },\n intent: {\n info: 'var(--ds-intent-info)',\n success: 'var(--ds-intent-success)',\n warning: 'var(--ds-intent-warning)',\n danger: 'var(--ds-intent-danger)',\n },\n} as const;\n\n/**\n * The colour-bearing utility families, taken from the same list\n * `scripts/token-rules.mjs` treats as colours. A hand-picked subset would be\n * the thing the next new component trips over: `border-intent-danger`,\n * `ring-accent-primary` and `divide-edge-strong` are already in the package.\n */\nexport const RECOMMENDED_COLOUR_PROPERTIES = [\n 'bg',\n 'text',\n 'border',\n 'ring',\n 'divide',\n 'placeholder',\n 'from',\n 'via',\n 'to',\n] as const;\n\nexport type RecommendedColourProperty = (typeof RECOMMENDED_COLOUR_PROPERTIES)[number];\n\n/**\n * Canonical Tailwind utility classes for recommended colours: every supported\n * colour property × namespace × role. Derived from {@link RECOMMENDED_COLOUR_ROLES}\n * rather than enumerated, so a new role appears here the moment it appears\n * there — the hand-maintained list this replaced was already missing the\n * `ring`, `divide` and colour-edged `border` utilities the package itself uses.\n */\nexport type RecommendedColourClass = {\n [N in RecommendedColourNamespace]: `${RecommendedColourProperty}-${N}-${(typeof RECOMMENDED_COLOUR_ROLES)[N][number]}`;\n}[RecommendedColourNamespace];\n\nexport const RECOMMENDED_COLOUR_CLASSES: readonly RecommendedColourClass[] = Object.freeze(\n RECOMMENDED_COLOUR_PROPERTIES.flatMap((property) =>\n RECOMMENDED_COLOUR_NAMESPACES.flatMap((namespace) =>\n RECOMMENDED_COLOUR_ROLES[namespace].map(\n (role) => `${property}-${namespace}-${role}` as RecommendedColourClass,\n ),\n ),\n ),\n);\n\nconst CLASS_SET: ReadonlySet<string> = new Set<string>(RECOMMENDED_COLOUR_CLASSES);\n\n/**\n * Test whether a Tailwind class addresses a recommended role utility.\n * Non-colour variants (`hover:`, `md:`) are stripped; polarity variants\n * (`dark:`, `light:`) are rejected outright — role tokens already switch per\n * level, and `dark:` now means \"midnight or dim\", not \"not sketch\", so a\n * polarity-prefixed colour utility is the bug this validator exists to catch.\n */\nexport function isRecommendedColourClass(className: string): boolean {\n if (!className) return false;\n const segments = className.split(':');\n const base = segments.pop() ?? '';\n if (segments.includes('dark') || segments.includes('light')) return false;\n return CLASS_SET.has(base);\n}\n"],"mappings":";AAiBA,SAAS,eAAe,cAA+B;AAKhD,IAAM,gCAAgC;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,IAAM,2BAA2B;AAAA,EACtC,SAAS,CAAC,QAAQ,UAAU,UAAU,SAAS;AAAA,EAC/C,SAAS,CAAC,WAAW,aAAa,SAAS,SAAS;AAAA,EACpD,MAAM,CAAC,UAAU,WAAW,QAAQ;AAAA,EACpC,QAAQ,CAAC,WAAW,aAAa,YAAY,OAAO;AAAA,EACpD,QAAQ,CAAC,QAAQ,WAAW,WAAW,QAAQ;AACjD;AAmBO,IAAM,sBAA6E;AAAA,EACxF,UAAU;AAAA,IACR,SAAS,OAAO,SAAS;AAAA,IACzB,SAAS,OAAO,SAAS;AAAA,IACzB,MAAM,OAAO,SAAS;AAAA,IACtB,QAAQ,OAAO,SAAS;AAAA,IACxB,QAAQ,OAAO,SAAS;AAAA,EAC1B;AAAA,EACA,QAAQ;AAAA,IACN,SAAS,OAAO,OAAO;AAAA,IACvB,SAAS,OAAO,OAAO;AAAA,IACvB,MAAM,OAAO,OAAO;AAAA,IACpB,QAAQ,OAAO,OAAO;AAAA,IACtB,QAAQ,OAAO,OAAO;AAAA,EACxB;AACF;AAOO,SAAS,sBAAsB,QAAoB,eAAwC;AAChG,SAAO,oBAAoB,KAAK,KAAK,oBAAoB,aAAa;AACxE;AAMO,IAAM,0BAA0B;AAAA,EACrC,SAAS;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,EACX;AAAA,EACA,SAAS;AAAA,IACP,SAAS;AAAA,IACT,WAAW;AAAA,IACX,OAAO;AAAA,IACP,SAAS;AAAA,EACX;AAAA,EACA,MAAM;AAAA,IACJ,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EACA,QAAQ;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,EACT;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AACF;AAQO,IAAM,gCAAgC;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAeO,IAAM,6BAAgE,OAAO;AAAA,EAClF,8BAA8B;AAAA,IAAQ,CAAC,aACrC,8BAA8B;AAAA,MAAQ,CAAC,cACrC,yBAAyB,SAAS,EAAE;AAAA,QAClC,CAAC,SAAS,GAAG,QAAQ,IAAI,SAAS,IAAI,IAAI;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAM,YAAiC,IAAI,IAAY,0BAA0B;AAS1E,SAAS,yBAAyB,WAA4B;AACnE,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,WAAW,UAAU,MAAM,GAAG;AACpC,QAAM,OAAO,SAAS,IAAI,KAAK;AAC/B,MAAI,SAAS,SAAS,MAAM,KAAK,SAAS,SAAS,OAAO,EAAG,QAAO;AACpE,SAAO,UAAU,IAAI,IAAI;AAC3B;","names":[]}