@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
package/README.md CHANGED
@@ -1,3 +1,318 @@
1
- # @rtkelly-labs/design-system
1
+ # 🎨 @rtkelly-labs/design-system
2
2
 
3
- Placeholder. Real versions are published from https://github.com/rtkelly-labs/design-system with provenance.
3
+ The foundational visual design system for **ryankelly.dev** and every personal surface that has
4
+ to look like it. Built around a **brutalist / neon-terminal** aesthetic: hard edges, zero
5
+ border-radius, offset shadows, and two themes — `midnight`, neon on blue-black, and `sketch`,
6
+ warm paper and pen ink.
7
+
8
+ ## This is a multi-surface system, and that is the foundational decision
9
+
10
+ **A colour originates exactly once**, as a literal in
11
+ [`src/theme/levels.ts`](./src/theme/levels.ts). Every consumable form is *generated* from it
12
+ and verified against it in CI. Nothing else may hold a colour literal — not a website, not a
13
+ video composition, not a graphic generator, not an editor theme.
14
+
15
+ | Surface | Form | State |
16
+ |---|---|---|
17
+ | **Websites** | Tailwind v4 tokens via generated `theme.css` | ships today |
18
+ | **Design tokens** | `tokens/palette.<level>.tokens.json`, DTCG 2025.10, OKLCH | ships today |
19
+ | **Developer themes** | VS Code · Zed · Shiki · Neovim · JetBrains, and a terminal's 16 ANSI slots | designed, blocked on a hue vocabulary |
20
+ | **Graphics** | generated diagrams, charts, ASCII-art panels | forked in the consumer — to be reclaimed |
21
+ | **Video** | Remotion frames at 1080p, where a 16px web type scale is wrong | groundwork only |
22
+
23
+ Two decisions carry this, and they are worth reading before changing anything about colour:
24
+
25
+ - [**ADR 0002 — One source of colour decisions, many emitted surfaces**](./docs/adr/0002-one-source-many-emitted-surfaces.md).
26
+ The root decision. Why per-surface palettes, manual ports and a runtime theming service were
27
+ all rejected, and why the source is TypeScript rather than JSON.
28
+ - [**ADR 0003 — Two Levels, each independently authored**](./docs/adr/0003-two-levels-independently-authored.md).
29
+ `midnight` and `sketch`, neither derived from the other. Neither is named for its polarity,
30
+ which is what keeps `Polarity` a real declared property.
31
+ - [**ADR 0001 — Hues are declared below Roles**](./docs/adr/0001-hues-declared-below-roles.md).
32
+ Components address a colour by its **job** (`accent.primary`), never its appearance (`cyan`).
33
+ But a terminal has sixteen positions named by colour and no concept of a keyword — so a hue
34
+ vocabulary is declared *underneath* the role vocabulary. Half the surfaces above cannot exist
35
+ without it.
36
+
37
+ [`docs/theme-taxonomy.md`](./docs/theme-taxonomy.md) is the full matrix: what every Level must
38
+ declare, which target needs which group, what is generated, and which gate covers each.
39
+
40
+ Published to the public **npm registry** as `@rtkelly-labs/design-system` (via npm trusted publishing — see `.github/workflows/publish-package.yml`).
41
+
42
+ **📖 Browse it: [design-system.ryankelly.dev](https://design-system.ryankelly.dev)** — the
43
+ full Storybook, with the blog's own Storybook composed into the sidebar. Unreleased work
44
+ lands at [preview.design-system.ryankelly.dev](https://preview.design-system.ryankelly.dev)
45
+ (the `preview` branch). Domains are declared in `rtkelly13/shared-utilities` at
46
+ [`infra/vercel/`](https://github.com/rtkelly13/shared-utilities/tree/main/infra/vercel);
47
+ see [AGENTS.md](./AGENTS.md#-hosted-storybook).
48
+
49
+ ---
50
+
51
+ ## Documentation figures
52
+
53
+ `FigureFrame` supplies a caption and border. The visual components do not create
54
+ their own frame, so related views can share one. For example:
55
+
56
+ ```tsx
57
+ import { FigureFrame, Timeline, UptimeStrip } from '@rtkelly-labs/design-system';
58
+
59
+ <FigureFrame title="SERVICE INCIDENT">
60
+ <Timeline events={[{ date: '14:02', label: 'Latency rose' }, { date: '14:11', label: 'Rolled back' }]} />
61
+ <UptimeStrip days="ok degraded down ok" />
62
+ </FigureFrame>
63
+ ```
64
+
65
+ Use `ActivityGrid` for dated counts such as commits or blog posts, and
66
+ `GanttChart` for work with start and end dates. `FlowDiagram`, `TreeDiagram`,
67
+ `ChangeSummary`, and `BeforeAfter` cover relationships and comparisons.
68
+ `SwatchCompare`, `TokenTable`, and `MetricStrip` are measured comparisons, and
69
+ `TermGrid` is a glossary. `Decision` is a standalone documentation block, not a
70
+ figure renderer. `Steps` and `Terminal` remain documentation blocks. See
71
+ [ADR 0006](./docs/adr/0006-document-visual-taxonomy.md) for the naming rules.
72
+
73
+ These components are adapted from [mdxcn](https://github.com/keshav-exe/mdxcn)
74
+ as source code, not imported from an mdxcn npm package. Every adapted component
75
+ links to its upstream source in the file header. See the
76
+ [license and attribution](./docs/attributions/mdxcn.md).
77
+
78
+ ---
79
+
80
+ ## 📸 Visual Regression Layer
81
+
82
+ Playwright screenshots of Storybook stories, asserted on every PR. The determinism contract and
83
+ operating instructions are in [`docs/visual-regression.md`](./docs/visual-regression.md).
84
+
85
+ - **What is asserted**: three tables in `tests/visual.spec.ts`: `CASES` (desktop), `MOBILE_CASES`
86
+ (narrow viewport) and `INTERACTIONS` (hover, focus and open states). They run against a static
87
+ Storybook build served on `http://localhost:6006`. `pnpm check:visual-coverage` requires every component to
88
+ have an asserted story or a stated reason.
89
+ - **Two projects**: `chromium` (desktop) and `mobile`. Baselines live in `tests/__snapshots__/`,
90
+ named by file rather than story id, so retitling a story does not orphan one.
91
+ - **Tolerance**: `maxDiffPixels: 0` at Playwright's default per-pixel `threshold` of `0.2`. A
92
+ flaky case is a defect to fix, not a threshold to loosen.
93
+ - **Linux only**: baselines are rendered on the CI runner, and font rasterisation differs by OS. On
94
+ macOS or Windows the suite skips every case (`test.skip(process.platform !== 'linux')`), so an
95
+ all-skipped run there has asserted nothing.
96
+
97
+ ```bash
98
+ # Regenerate baselines: comment on the PR (owner/member/collaborator only)
99
+ /update-snapshots # writes only missing baselines (new cases); never overwrites
100
+ /update-snapshots changed # also overwrites baselines that now differ: an intended visual change
101
+ /update-snapshots all # regenerates every baseline
102
+
103
+ # On a Linux machine matching CI:
104
+ pnpm test:visual # assert
105
+ pnpm test:visual:missing # write baselines only for new cases
106
+ ```
107
+
108
+ The bot's snapshot commit does not trigger CI. Push a fresh commit afterwards so the new baselines
109
+ are asserted.
110
+
111
+ ### Capturing this package deterministically
112
+
113
+ If you screenshot, print, or render outside a live browser session, three switches decide whether
114
+ you get the same output twice:
115
+
116
+ ```tsx
117
+ <ThemeProvider defaultLevel="midnight" scoped> {/* 1. no storage, no matchMedia, no documentElement */}
118
+ ```
119
+
120
+ ```ts
121
+ await document.fonts.ready; // 2. or you capture the fallback face
122
+ // 3. suppress transitions:
123
+ // *, *::before, *::after { transition: none !important; animation: none !important }
124
+ ```
125
+
126
+ `scoped` is the one that does the work — `persist={false} followSystem={false}` look like the
127
+ determinism controls and are redundant under it. Full reasoning in
128
+ [`docs/deterministic-rendering.md`](./docs/deterministic-rendering.md).
129
+
130
+ ---
131
+
132
+ ## 📦 Installation & Prerelease Testing
133
+
134
+ ### Stable Release
135
+
136
+ ```bash
137
+ pnpm add @rtkelly-labs/design-system
138
+ ```
139
+
140
+ No registry configuration needed — it's a public npm package.
141
+
142
+ **ESM only, Node 22 or later.** The package is `"type": "module"` with one build. Bundlers
143
+ (Vite, Next, webpack, esbuild) and `import` need nothing. `require('@rtkelly-labs/design-system')`
144
+ works on Node 22.12+, which loads ES modules from `require()`; a toolchain that needs the package
145
+ to be CommonJS (Jest without ESM support, for one) does not. Server components can import it
146
+ directly — see [`docs/api-surface.md`](./docs/api-surface.md) for which components render on the
147
+ server.
148
+
149
+ ### Dev Prerelease Testing
150
+
151
+ Comment `/publish-dev` on a Pull Request and CI publishes that branch as
152
+ `<version>-dev.<pr>.<short-sha>` under the `dev` dist-tag:
153
+
154
+ ```bash
155
+ pnpm add @rtkelly-labs/design-system@dev # latest dev build
156
+ pnpm view @rtkelly-labs/design-system dist-tags # exact versions per PR
157
+ ```
158
+
159
+ ---
160
+
161
+ ## ⚡ Usage & Setup
162
+
163
+ ### 1. Import Stylesheet & Theme Provider
164
+
165
+ In your app entrypoint (`_app.tsx`, `main.tsx`, or `layout.tsx`):
166
+
167
+ ```tsx
168
+ import '@rtkelly-labs/design-system/styles.css';
169
+ import { ThemeProvider, Button, Card, PageTitle, Badge, Divider } from '@rtkelly-labs/design-system';
170
+
171
+ export function App() {
172
+ return (
173
+ <ThemeProvider defaultLevel="midnight">
174
+ <main style={{ padding: '2rem' }}>
175
+ <PageTitle subtitle="Foundation Design System Surface">
176
+ [ MY APPLICATION ]
177
+ </PageTitle>
178
+
179
+ <Card>
180
+ <Badge accent="primary">v1.2.0 ACTIVE</Badge>
181
+ <p style={{ margin: '1rem 0' }}>
182
+ Brutalist UI surface shared across all projects.
183
+ </p>
184
+ <Button bracketed variant="tertiary">
185
+ EXECUTE ACTION
186
+ </Button>
187
+ </Card>
188
+
189
+ {/* The mark follows the level's polarity: a terminal rule on the dark
190
+ rungs, a hand-ruled pencil dash on the light ones. */}
191
+ <Divider />
192
+ </main>
193
+ </ThemeProvider>
194
+ );
195
+ }
196
+ ```
197
+
198
+ ---
199
+
200
+ ### 2. Tailwind CSS v4 token contract
201
+
202
+ Import the theme contract in your CSS entrypoint. It ships the `@theme` tokens,
203
+ one `[data-theme]` block per level, the per-level and polarity variants, and an
204
+ `@source` directive — that last one is load-bearing, because Tailwind v4 skips
205
+ `node_modules` during content detection and would otherwise generate none of the
206
+ utilities the compiled components use:
207
+
208
+ ```css
209
+ @import "tailwindcss";
210
+ @import "@rtkelly-labs/design-system/theme.css";
211
+ ```
212
+
213
+ `theme.css` is **generated** from `src/theme/levels.ts`, which is the single place
214
+ any level name or colour is written.
215
+
216
+ `styles.css` = `theme.css` + `prose.css` + web-font imports + opinionated global
217
+ resets (zero border-radius everywhere, base typography). Import that, or compose
218
+ the pieces yourself — but not both.
219
+
220
+ **Peer requirements.** `tailwindcss` v4 for either stylesheet, and
221
+ `@tailwindcss/typography` if you import `prose.css`, which loads it as a
222
+ `@plugin`. Both are optional peers: a Tailwind plugin resolves from *your*
223
+ `node_modules` at your build time, so neither can be bundled.
224
+
225
+ ### 3. Semantic tokens
226
+
227
+ Address **roles**, not hues. Each rung of the ladder declares its own mapping of
228
+ these roles, so a component that hard-codes `cyan` cannot be rethemed without
229
+ editing the component. Every pair is contrast-audited on every level by
230
+ `pnpm check:contrast`, which gates CI.
231
+
232
+ | Role group | Tokens | Use for |
233
+ |---|---|---|
234
+ | `--ds-accent-*` | `primary`, `secondary`, `tertiary`, `quiet` | Visual hierarchy |
235
+ | `--ds-intent-*` | `info`, `success`, `warning`, `danger` | Communicated meaning |
236
+ | `--ds-surface-*` | `base`, `raised`, `sunken`, `overlay` | Background elevation |
237
+ | `--ds-text-*` | `primary`, `secondary`, `muted`, `inverse` | Text prominence |
238
+ | `--ds-border-*` | `strong`, `default`, `subtle` | Rule weight |
239
+ | `--ds-font-*` | `display`, `body`, `mono`, `pixel` | Typography roles |
240
+
241
+ ```tsx
242
+ import { Badge, Tag, accentVar, semanticTokens } from '@rtkelly-labs/design-system';
243
+
244
+ <Badge accent="primary">HIERARCHY</Badge>
245
+ <Tag text="deprecated" accent="danger" /> {/* meaning, not colour */}
246
+ <div style={{ borderColor: accentVar('secondary') }} />
247
+ <div style={{ background: semanticTokens.surface.raised }} />
248
+ ```
249
+
250
+ Tailwind aliases are generated too: `text-accent-primary`, `bg-surface-raised`,
251
+ `border-edge-subtle`, `text-intent-danger`.
252
+
253
+ The legacy palette names (`'cyan' | 'pink' | 'yellow' | 'green'`) still resolve to
254
+ identical values, so existing call sites keep working — but they are deprecated.
255
+
256
+ ---
257
+
258
+ ## 📚 Documentation Portal
259
+
260
+ A complete chrome kit for MDX documentation sites.
261
+
262
+ ```tsx
263
+ import {
264
+ DocsLayout, DocsHeader, DocsSidebar, TableOfContents,
265
+ Breadcrumbs, DocPager, Prose, CodeBlock, CodeTabs, CodeTab, AnchorHeading,
266
+ DocsLinkProvider, mdxComponents,
267
+ } from '@rtkelly-labs/design-system';
268
+ import '@rtkelly-labs/design-system/prose.css';
269
+ ```
270
+
271
+ **Every section is addressable.** Headings render through `AnchorHeading`, which gives
272
+ each one a `#slug` id and a hover affordance that copies the *absolute* URL including
273
+ the hash — the thing you actually paste into a ticket.
274
+
275
+ **The header is durable.** `DocsHeader` is sticky and measures its own height into
276
+ `--docs-header-height`. That single value feeds `scroll-padding-top`, the
277
+ `scroll-margin-top` on every heading, and the scroll-spy reading line — so anchors
278
+ never land underneath the bar, at any viewport width.
279
+
280
+ **Bring your own router.** Wrap the app in `DocsLinkProvider` and all chrome navigation
281
+ goes through it instead of hard-navigating:
282
+
283
+ ```tsx
284
+ import { Link } from 'react-router-dom';
285
+
286
+ <DocsLinkProvider component={({ href, ...props }) => <Link to={href} {...props} />}>
287
+ <App />
288
+ </DocsLinkProvider>
289
+ ```
290
+
291
+ **Code tabs switch together.** `CodeTabs` is the language / package-manager
292
+ switcher: a real `tablist` with arrow-key traversal. Blocks sharing a `group`
293
+ switch as one and the choice persists across pages, so a reader picks `pnpm`
294
+ once. Fenced blocks inside a `CodeTab` attach to the strip automatically.
295
+
296
+ **One MDX mapping.** `mdxComponents` maps `h1`–`h6` → anchored headings, `pre` →
297
+ `CodeBlock`, `a` → router-aware links, and exposes `NoteBlock` / `TLDR` / `Card` /
298
+ `Tag` to MDX authors. Pass it to any MDX provider so every site renders Markdown
299
+ identically.
300
+
301
+ `prose.css` styles the bare tags a Markdown pipeline emits (`ul`, `ol`, `table`,
302
+ `blockquote`, `hr`, inline `code`, task lists), plus GitHub alert syntax
303
+ (`> [!NOTE]`) as produced by `remark-github-blockquote-alert`, and code titles from
304
+ `remark-code-title`. It is plain CSS on purpose: the docs chrome is layout-critical,
305
+ and Tailwind v4 skips `node_modules` in automatic content detection, so a
306
+ misconfigured `@source` would half-break a sidebar rather than fail loudly.
307
+
308
+ ---
309
+
310
+ ## 🛠 Repository & Publishing
311
+
312
+ - **Repository**: `https://github.com/rtkelly-labs/design-system`
313
+ - **npm**: `@rtkelly-labs/design-system` on `https://registry.npmjs.org`
314
+ - **CI Pipelines**:
315
+ - `ci.yml`: Typecheck, package build, Storybook build, and Linux visual regression testing.
316
+ - `publish-package.yml`: npm publishing (stable on main, dev prereleases via dispatch).
317
+ - `publish-dev-command.yml`: `/publish-dev` PR comment handler.
318
+ - `update-snapshots.yml`: On-demand visual snapshot regeneration.
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+ // src/components/AlertDialog.tsx
4
+ import { forwardRef } from "react";
5
+ import { AlertDialog as BaseAlertDialog } from "@base-ui/react/alert-dialog";
6
+ import { cn } from "../lib/recipe.js";
7
+ import { usePortalThemeAttribute } from "./portalTheme.js";
8
+ import { dialogSurface } from "./dialogSurface.js";
9
+ import { Button } from "./Button.js";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ var AlertDialog = forwardRef(function AlertDialog2({
12
+ isOpen,
13
+ onClose,
14
+ onConfirm,
15
+ title,
16
+ children,
17
+ confirmLabel = "CONFIRM",
18
+ cancelLabel = "CANCEL",
19
+ footer,
20
+ className,
21
+ ...props
22
+ }, ref) {
23
+ const slots = dialogSurface();
24
+ const portalTheme = usePortalThemeAttribute();
25
+ return /* @__PURE__ */ jsx(
26
+ BaseAlertDialog.Root,
27
+ {
28
+ open: isOpen,
29
+ onOpenChange: (open) => {
30
+ if (!open) onClose();
31
+ },
32
+ children: /* @__PURE__ */ jsxs(BaseAlertDialog.Portal, { ...portalTheme, children: [
33
+ /* @__PURE__ */ jsx(
34
+ BaseAlertDialog.Backdrop,
35
+ {
36
+ "data-slot": "alert-dialog-backdrop",
37
+ className: slots.backdrop()
38
+ }
39
+ ),
40
+ /* @__PURE__ */ jsx(
41
+ BaseAlertDialog.Viewport,
42
+ {
43
+ "data-slot": "alert-dialog-viewport",
44
+ className: slots.viewport(),
45
+ children: /* @__PURE__ */ jsxs(
46
+ BaseAlertDialog.Popup,
47
+ {
48
+ ref,
49
+ "data-slot": "alert-dialog",
50
+ className: cn(slots.popup(), "max-w-md", className),
51
+ ...props,
52
+ children: [
53
+ /* @__PURE__ */ jsx("div", { "data-slot": "alert-dialog-header", className: slots.header(), children: /* @__PURE__ */ jsx(
54
+ BaseAlertDialog.Title,
55
+ {
56
+ "data-slot": "alert-dialog-title",
57
+ className: slots.title(),
58
+ render: /* @__PURE__ */ jsxs("h3", { children: [
59
+ "[ ",
60
+ title,
61
+ " ]"
62
+ ] })
63
+ }
64
+ ) }),
65
+ /* @__PURE__ */ jsx(
66
+ BaseAlertDialog.Description,
67
+ {
68
+ "data-slot": "alert-dialog-body",
69
+ className: slots.body(),
70
+ render: /* @__PURE__ */ jsx("div", {}),
71
+ children
72
+ }
73
+ ),
74
+ /* @__PURE__ */ jsx("div", { "data-slot": "alert-dialog-footer", className: slots.footer(), children: footer || /* @__PURE__ */ jsxs(Fragment, { children: [
75
+ /* @__PURE__ */ jsx(
76
+ BaseAlertDialog.Close,
77
+ {
78
+ render: /* @__PURE__ */ jsx(Button, { variant: "default", size: "sm", children: cancelLabel })
79
+ }
80
+ ),
81
+ /* @__PURE__ */ jsx(Button, { onClick: onConfirm, variant: "tertiary", bracketed: true, size: "sm", children: confirmLabel })
82
+ ] }) })
83
+ ]
84
+ }
85
+ )
86
+ }
87
+ )
88
+ ] })
89
+ }
90
+ );
91
+ });
92
+ export {
93
+ AlertDialog
94
+ };
95
+ //# sourceMappingURL=AlertDialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/AlertDialog.tsx"],"sourcesContent":["'use client';\n\nimport { forwardRef } from 'react';\nimport type { HTMLAttributes, ReactNode } from 'react';\nimport { AlertDialog as BaseAlertDialog } from '@base-ui/react/alert-dialog';\nimport { cn } from '../lib/recipe';\nimport { usePortalThemeAttribute } from './portalTheme';\nimport { dialogSurface } from './dialogSurface';\nimport { Button } from './Button';\n\nexport interface AlertDialogProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children' | 'className'> {\n isOpen: boolean;\n /** Dismissal — Escape, or the cancel control. Never a backdrop click. */\n onClose: () => void;\n /** The destructive action. Called instead of, not as well as, `onClose`. */\n onConfirm: () => void;\n title: string;\n /**\n * What the action does, and what it costs. Short: this is wired as the\n * dialog's accessible description, so a screen reader reads it immediately\n * after the title.\n */\n children: ReactNode;\n confirmLabel?: string;\n cancelLabel?: string;\n /**\n * Replaces the cancel/confirm pair outright, for the rare confirmation with\n * a third option. Supplying it means owning dismissal — nothing else in the\n * dialog closes it.\n */\n footer?: ReactNode;\n className?: string;\n}\n\n/**\n * A destructive-action confirmation, on Base UI's `alert-dialog`.\n *\n * ## Why this is not `Modal` with different buttons\n *\n * The difference is the dismissal contract, and it is the whole point.\n * `AlertDialog` cannot be dismissed by clicking outside it: Base UI's\n * `AlertDialog.Root` omits `disablePointerDismissal` from its props\n * altogether, because the answer is never \"yes\". A confirmation you can\n * dismiss with a stray click is a confirmation that has not confirmed\n * anything, and shipping `Modal` without this is how a delete dialog ends up\n * backdrop-dismissible by default.\n *\n * Escape still closes it. That is deliberate and not a contradiction: Escape\n * is deliberate, unambiguous and always means *cancel*, and removing it would\n * trap a keyboard user in the dialog.\n *\n * It renders `role=\"alertdialog\"` rather than `role=\"dialog\"`, which is what\n * tells a screen reader to announce the whole surface rather than just its\n * name — the reason the description below is wired rather than decorative.\n *\n * ## The cancel control is the close control\n *\n * There is no `×`. A confirmation asks a question with two answers and both\n * are buttons; adding a third dismissal affordance in the corner makes the\n * safe answer ambiguous. Cancel is `AlertDialog.Close`, so it is also what a\n * touch screen reader finds when it looks for the way out.\n */\nexport const AlertDialog = forwardRef<HTMLDivElement, AlertDialogProps>(function AlertDialog(\n {\n isOpen,\n onClose,\n onConfirm,\n title,\n children,\n confirmLabel = 'CONFIRM',\n cancelLabel = 'CANCEL',\n footer,\n className,\n ...props\n },\n ref,\n) {\n const slots = dialogSurface();\n\n const portalTheme = usePortalThemeAttribute();\n return (\n <BaseAlertDialog.Root\n open={isOpen}\n onOpenChange={(open) => {\n if (!open) onClose();\n }}\n >\n <BaseAlertDialog.Portal {...portalTheme}>\n <BaseAlertDialog.Backdrop\n data-slot=\"alert-dialog-backdrop\"\n className={slots.backdrop()}\n />\n <BaseAlertDialog.Viewport\n data-slot=\"alert-dialog-viewport\"\n className={slots.viewport()}\n >\n <BaseAlertDialog.Popup\n ref={ref}\n data-slot=\"alert-dialog\"\n // Narrower than `Modal`'s `max-w-lg`: a confirmation is one\n // sentence and two buttons, and a wide one reads as a workflow the\n // reader is expected to study rather than a question to answer.\n className={cn(slots.popup(), 'max-w-md', className)}\n {...props}\n >\n <div data-slot=\"alert-dialog-header\" className={slots.header()}>\n <BaseAlertDialog.Title\n data-slot=\"alert-dialog-title\"\n className={slots.title()}\n render={<h3>[ {title} ]</h3>}\n />\n </div>\n\n <BaseAlertDialog.Description\n data-slot=\"alert-dialog-body\"\n className={slots.body()}\n // A `div`, not Base UI's default `p`: the body takes arbitrary\n // nodes, and a block element inside a paragraph is invalid HTML\n // that the parser silently repairs by closing the paragraph\n // early — which would leave the description pointing at nothing.\n render={<div />}\n >\n {children}\n </BaseAlertDialog.Description>\n\n <div data-slot=\"alert-dialog-footer\" className={slots.footer()}>\n {footer || (\n <>\n {/*\n * The label goes on the render element rather than on\n * `Close`: `Button`'s props are a union whose link arm\n * requires `children` and `href`, so an empty `<Button />`\n * does not typecheck even though Base UI would fill it in.\n */}\n <BaseAlertDialog.Close\n render={\n <Button variant=\"default\" size=\"sm\">\n {cancelLabel}\n </Button>\n }\n />\n <Button onClick={onConfirm} variant=\"tertiary\" bracketed size=\"sm\">\n {confirmLabel}\n </Button>\n </>\n )}\n </div>\n </BaseAlertDialog.Popup>\n </BaseAlertDialog.Viewport>\n </BaseAlertDialog.Portal>\n </BaseAlertDialog.Root>\n );\n});\n"],"mappings":";;;AAEA,SAAS,kBAAkB;AAE3B,SAAS,eAAe,uBAAuB;AAC/C,SAAS,UAAU;AACnB,SAAS,+BAA+B;AACxC,SAAS,qBAAqB;AAC9B,SAAS,cAAc;AAiFf,SAuCQ,UAvCR,KAqBgB,YArBhB;AA1BD,IAAM,cAAc,WAA6C,SAASA,aAC/E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,QAAQ,cAAc;AAE5B,QAAM,cAAc,wBAAwB;AAC5C,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,MAAM;AAAA,MACN,cAAc,CAAC,SAAS;AACtB,YAAI,CAAC,KAAM,SAAQ;AAAA,MACrB;AAAA,MAEA,+BAAC,gBAAgB,QAAhB,EAAwB,GAAG,aAC1B;AAAA;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,aAAU;AAAA,YACV,WAAW,MAAM,SAAS;AAAA;AAAA,QAC5B;AAAA,QACA;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,aAAU;AAAA,YACV,WAAW,MAAM,SAAS;AAAA,YAE1B;AAAA,cAAC,gBAAgB;AAAA,cAAhB;AAAA,gBACC;AAAA,gBACA,aAAU;AAAA,gBAIV,WAAW,GAAG,MAAM,MAAM,GAAG,YAAY,SAAS;AAAA,gBACjD,GAAG;AAAA,gBAEJ;AAAA,sCAAC,SAAI,aAAU,uBAAsB,WAAW,MAAM,OAAO,GAC3D;AAAA,oBAAC,gBAAgB;AAAA,oBAAhB;AAAA,sBACC,aAAU;AAAA,sBACV,WAAW,MAAM,MAAM;AAAA,sBACvB,QAAQ,qBAAC,QAAG;AAAA;AAAA,wBAAG;AAAA,wBAAM;AAAA,yBAAE;AAAA;AAAA,kBACzB,GACF;AAAA,kBAEA;AAAA,oBAAC,gBAAgB;AAAA,oBAAhB;AAAA,sBACC,aAAU;AAAA,sBACV,WAAW,MAAM,KAAK;AAAA,sBAKtB,QAAQ,oBAAC,SAAI;AAAA,sBAEZ;AAAA;AAAA,kBACH;AAAA,kBAEA,oBAAC,SAAI,aAAU,uBAAsB,WAAW,MAAM,OAAO,GAC1D,oBACC,iCAOE;AAAA;AAAA,sBAAC,gBAAgB;AAAA,sBAAhB;AAAA,wBACC,QACE,oBAAC,UAAO,SAAQ,WAAU,MAAK,MAC5B,uBACH;AAAA;AAAA,oBAEJ;AAAA,oBACA,oBAAC,UAAO,SAAS,WAAW,SAAQ,YAAW,WAAS,MAAC,MAAK,MAC3D,wBACH;AAAA,qBACF,GAEJ;AAAA;AAAA;AAAA,YACF;AAAA;AAAA,QACF;AAAA,SACF;AAAA;AAAA,EACF;AAEJ,CAAC;","names":["AlertDialog"]}