@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,138 @@
1
+ "use client";
2
+
3
+ // src/components/experiments/ExperimentsView.tsx
4
+ import { Beaker, Boxes, Palette, Type, Sparkles, Projector, Terminal } from "lucide-react";
5
+ import { PageTitle } from "../PageTitle.js";
6
+ import { Badge } from "../Badge.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var DEFAULT_EXPERIMENTS = [
9
+ {
10
+ id: "component-library",
11
+ name: "Component Library",
12
+ description: "Every core primitive component rendered across the full theme ladder, side by side",
13
+ icon: /* @__PURE__ */ jsx(Boxes, { size: 36, color: "var(--ds-accent-primary)" }),
14
+ status: "active",
15
+ componentCount: 8
16
+ },
17
+ {
18
+ id: "design-sandbox",
19
+ name: "Design System Sandbox",
20
+ description: "Interactive component variations, theme toggles, and live presentation playground",
21
+ icon: /* @__PURE__ */ jsx(Palette, { size: 36, color: "var(--ds-accent-tertiary)" }),
22
+ status: "active",
23
+ componentCount: 12
24
+ },
25
+ {
26
+ id: "typography-proposals",
27
+ name: "Editorial Typography Proposals",
28
+ description: "Three-role font pairings (Space Grotesk + Inter + IBM Plex Mono + VT323)",
29
+ icon: /* @__PURE__ */ jsx(Type, { size: 36, color: "var(--ds-accent-secondary)" }),
30
+ status: "active",
31
+ componentCount: 4
32
+ },
33
+ {
34
+ id: "presentation-decks",
35
+ name: "Brutalist Slide Decks",
36
+ description: "Keyboard-navigated presentation deck engine with fullscreen support",
37
+ icon: /* @__PURE__ */ jsx(Projector, { size: 36, color: "var(--ds-intent-success)" }),
38
+ status: "active",
39
+ componentCount: 6
40
+ },
41
+ {
42
+ id: "graphics-generators",
43
+ name: "Graphics Generators",
44
+ description: "Seed-driven SVG background and hero graphics generators for talks",
45
+ icon: /* @__PURE__ */ jsx(Sparkles, { size: 36, color: "var(--ds-accent-primary)" }),
46
+ status: "active",
47
+ componentCount: 5
48
+ }
49
+ ];
50
+ var STATUS_ACCENT = {
51
+ active: "success",
52
+ experimental: "info",
53
+ archived: "quiet"
54
+ };
55
+ var ExperimentsView = ({
56
+ experiments = DEFAULT_EXPERIMENTS,
57
+ onSelectExperiment
58
+ }) => {
59
+ return /* @__PURE__ */ jsxs(
60
+ "div",
61
+ {
62
+ "data-slot": "experiments-view",
63
+ style: {
64
+ maxWidth: "1000px",
65
+ margin: "0 auto",
66
+ padding: "2rem 1.5rem",
67
+ color: "var(--ds-text-primary)"
68
+ },
69
+ children: [
70
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "1rem", marginBottom: "1.5rem" }, children: [
71
+ /* @__PURE__ */ jsx(Beaker, { size: 40, color: "var(--ds-accent-primary)" }),
72
+ /* @__PURE__ */ jsx(PageTitle, { subtitle: "Interactive prototypes, design systems, and creative explorations", bracketed: true, children: "EXPERIMENTS & LABS" })
73
+ ] }),
74
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: "1.5rem", marginTop: "2rem" }, children: experiments.map((exp) => /* @__PURE__ */ jsxs(
75
+ "div",
76
+ {
77
+ onClick: () => onSelectExperiment && onSelectExperiment(exp.id),
78
+ className: "bg-surface-raised text-content-primary border-2 border-edge-strong shadow-hard-md p-6 transition-[transform,box-shadow] duration-150 ease-out hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-[6px_6px_0_0_var(--ds-accent-primary)] motion-reduce:transition-none motion-reduce:hover:translate-x-0 motion-reduce:hover:translate-y-0",
79
+ style: {
80
+ display: "flex",
81
+ gap: "1.5rem",
82
+ alignItems: "center",
83
+ cursor: onSelectExperiment ? "pointer" : "default"
84
+ },
85
+ children: [
86
+ /* @__PURE__ */ jsx("div", { children: exp.icon }),
87
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1 }, children: [
88
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "1rem", marginBottom: "0.4rem" }, children: [
89
+ /* @__PURE__ */ jsx(
90
+ "h3",
91
+ {
92
+ style: {
93
+ fontFamily: "var(--ds-font-display)",
94
+ fontSize: "1.4rem",
95
+ fontWeight: 800,
96
+ textTransform: "uppercase",
97
+ margin: 0,
98
+ color: "var(--ds-text-primary)"
99
+ },
100
+ children: exp.name
101
+ }
102
+ ),
103
+ /* @__PURE__ */ jsx(Badge, { accent: STATUS_ACCENT[exp.status], children: exp.status.toUpperCase() })
104
+ ] }),
105
+ /* @__PURE__ */ jsx(
106
+ "p",
107
+ {
108
+ style: {
109
+ fontFamily: "var(--ds-font-mono)",
110
+ fontSize: "0.875rem",
111
+ color: "var(--ds-text-primary)",
112
+ opacity: 0.8,
113
+ margin: 0
114
+ },
115
+ children: exp.description
116
+ }
117
+ ),
118
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: "0.75rem", fontSize: "0.8rem", fontFamily: "var(--ds-font-mono)", color: "var(--ds-accent-secondary)", display: "flex", alignItems: "center", gap: "0.4rem" }, children: [
119
+ /* @__PURE__ */ jsx(Terminal, { size: 14 }),
120
+ /* @__PURE__ */ jsxs("span", { children: [
121
+ exp.componentCount,
122
+ " primitives contained"
123
+ ] })
124
+ ] })
125
+ ] })
126
+ ]
127
+ },
128
+ exp.id
129
+ )) })
130
+ ]
131
+ }
132
+ );
133
+ };
134
+ export {
135
+ DEFAULT_EXPERIMENTS,
136
+ ExperimentsView
137
+ };
138
+ //# sourceMappingURL=ExperimentsView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/experiments/ExperimentsView.tsx"],"sourcesContent":["'use client';\n\nimport type React from 'react';\nimport { Beaker, Boxes, Palette, Type, Sparkles, Projector, Terminal } from 'lucide-react';\nimport { PageTitle } from '../PageTitle';\nimport { Badge } from '../Badge';\nimport type { AccentToken } from '../../lib/theme';\n\nexport interface ExperimentItem {\n /** Stable identifier, handed back by `onSelectExperiment`. */\n id: string;\n /** Card heading, uppercased by the type scale. */\n name: string;\n /** One line under the heading saying what the experiment explores. */\n description: string;\n /** Leading glyph. A node rather than a component type, so it can carry its own size. */\n icon?: React.ReactNode;\n /**\n * Lifecycle state, shown as a badge and coloured by {@link STATUS_ACCENT} —\n * `success` for `active`, `info` for `experimental`, `quiet` for `archived`.\n */\n status: 'active' | 'archived' | 'experimental';\n /** How many primitives the experiment exercises, shown in the footer line. */\n componentCount: number;\n}\n\n/**\n * The catalogue `ExperimentsView` falls back to — ryankelly.dev's own.\n *\n * Exported so a consumer can spread it, slice it or use it as the shape of\n * their own list. Until #95 it was exported and *not* accepted as a prop, so\n * the name promised a fallback the component had no way to take.\n */\nexport const DEFAULT_EXPERIMENTS: ExperimentItem[] = [\n {\n id: 'component-library',\n name: 'Component Library',\n description: 'Every core primitive component rendered across the full theme ladder, side by side',\n icon: <Boxes size={36} color=\"var(--ds-accent-primary)\" />,\n status: 'active',\n componentCount: 8,\n },\n {\n id: 'design-sandbox',\n name: 'Design System Sandbox',\n description: 'Interactive component variations, theme toggles, and live presentation playground',\n icon: <Palette size={36} color=\"var(--ds-accent-tertiary)\" />,\n status: 'active',\n componentCount: 12,\n },\n {\n id: 'typography-proposals',\n name: 'Editorial Typography Proposals',\n description: 'Three-role font pairings (Space Grotesk + Inter + IBM Plex Mono + VT323)',\n icon: <Type size={36} color=\"var(--ds-accent-secondary)\" />,\n status: 'active',\n componentCount: 4,\n },\n {\n id: 'presentation-decks',\n name: 'Brutalist Slide Decks',\n description: 'Keyboard-navigated presentation deck engine with fullscreen support',\n icon: <Projector size={36} color=\"var(--ds-intent-success)\" />,\n status: 'active',\n componentCount: 6,\n },\n {\n id: 'graphics-generators',\n name: 'Graphics Generators',\n description: 'Seed-driven SVG background and hero graphics generators for talks',\n icon: <Sparkles size={36} color=\"var(--ds-accent-primary)\" />,\n status: 'active',\n componentCount: 5,\n },\n];\n\n/**\n * How a lifecycle state is coloured.\n *\n * A `Record` over the union rather than a ternary, which is the ladder rule\n * from AGENTS.md applied to a smaller union: a fourth status becomes a compile\n * error here instead of silently falling through to whichever branch the\n * ternary ended on. The expression this replaced —\n * `status === 'active' ? 'cyan' : 'yellow'` — rendered `archived` and\n * `experimental` identically, so two different states looked the same while\n * their labels differed.\n *\n * `active` and `experimental` take **intent** roles because they are states a\n * reader acts on: one is live, one is in progress. `archived` takes `quiet`,\n * an *emphasis* role, and that is the deliberate exception — \"archived\" is not\n * a signal, it is the absence of one, so the right treatment is to recede\n * rather than to communicate.\n */\nconst STATUS_ACCENT: Record<ExperimentItem['status'], AccentToken> = {\n active: 'success',\n experimental: 'info',\n archived: 'quiet',\n};\n\nexport interface ExperimentsViewProps {\n /**\n * The catalogue to render, in display order. Defaults to\n * {@link DEFAULT_EXPERIMENTS}, which is *this site's* list rather than a\n * neutral one — pass your own to use this anywhere else.\n */\n experiments?: readonly ExperimentItem[];\n /**\n * Called with the experiment's `id` when a card is chosen. Omit it and the\n * grid is a read-only display: the cards lose their pointer cursor and do\n * nothing when clicked.\n */\n onSelectExperiment?: (id: string) => void;\n}\n\n/**\n * The experiments index: a grid of cards, one per experiment, each with a\n * status badge and a component count.\n *\n * Pass `experiments` to supply the catalogue; it defaults to\n * {@link DEFAULT_EXPERIMENTS}, which is this site's own list and is exported so\n * a consumer can extend it rather than start from nothing.\n * `onSelectExperiment` turns the cards into navigation — without it the grid is\n * a read-only display.\n *\n * `status` is the only place the component uses colour to mean something, and\n * it reads from {@link STATUS_ACCENT} — intent roles for the two states a\n * reader acts on, `quiet` for `archived`. See that constant for why the third\n * one is the exception.\n */\nexport const ExperimentsView: React.FC<ExperimentsViewProps> = ({\n experiments = DEFAULT_EXPERIMENTS,\n onSelectExperiment,\n}) => {\n return (\n <div data-slot=\"experiments-view\"\n style={{\n maxWidth: '1000px',\n margin: '0 auto',\n padding: '2rem 1.5rem',\n color: 'var(--ds-text-primary)',\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem' }}>\n <Beaker size={40} color=\"var(--ds-accent-primary)\" />\n <PageTitle subtitle=\"Interactive prototypes, design systems, and creative explorations\" bracketed>\n EXPERIMENTS & LABS\n </PageTitle>\n </div>\n\n <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', marginTop: '2rem' }}>\n {experiments.map((exp) => (\n <div\n key={exp.id}\n onClick={() => onSelectExperiment && onSelectExperiment(exp.id)}\n className=\"bg-surface-raised text-content-primary border-2 border-edge-strong shadow-hard-md p-6 transition-[transform,box-shadow] duration-150 ease-out hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-[6px_6px_0_0_var(--ds-accent-primary)] motion-reduce:transition-none motion-reduce:hover:translate-x-0 motion-reduce:hover:translate-y-0\"\n style={{\n display: 'flex',\n gap: '1.5rem',\n alignItems: 'center',\n cursor: onSelectExperiment ? 'pointer' : 'default',\n }}\n >\n <div>{exp.icon}</div>\n <div style={{ flex: 1 }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '0.4rem' }}>\n <h3\n style={{\n fontFamily: 'var(--ds-font-display)',\n fontSize: '1.4rem',\n fontWeight: 800,\n textTransform: 'uppercase',\n margin: 0,\n color: 'var(--ds-text-primary)',\n }}\n >\n {exp.name}\n </h3>\n <Badge accent={STATUS_ACCENT[exp.status]}>{exp.status.toUpperCase()}</Badge>\n </div>\n <p\n style={{\n fontFamily: 'var(--ds-font-mono)',\n fontSize: '0.875rem',\n color: 'var(--ds-text-primary)',\n opacity: 0.8,\n margin: 0,\n }}\n >\n {exp.description}\n </p>\n <div style={{ marginTop: '0.75rem', fontSize: '0.8rem', fontFamily: 'var(--ds-font-mono)', color: 'var(--ds-accent-secondary)', display: 'flex', alignItems: 'center', gap: '0.4rem' }}>\n <Terminal size={14} />\n <span>{exp.componentCount} primitives contained</span>\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n};\n"],"mappings":"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","names":[]}
@@ -0,0 +1,200 @@
1
+ // src/components/fieldFrame.tsx
2
+ import { forwardRef, useId } from "react";
3
+ import { Field as BaseField } from "@base-ui/react/field";
4
+ import { Fieldset as BaseFieldset } from "@base-ui/react/fieldset";
5
+ import { recipe } from "../lib/recipe.js";
6
+ import { accentVar } from "../lib/theme.js";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ var frame = recipe({
9
+ slots: {
10
+ root: "flex w-full flex-col gap-1.5 font-mono",
11
+ label: "text-xs font-bold uppercase tracking-wider text-content-secondary",
12
+ message: "font-mono text-xs",
13
+ /**
14
+ * The grouping element — a `<fieldset>`, or the `radiogroup` a `RadioGroup`
15
+ * renders in its place. The reset is the browser's fieldset chrome: the
16
+ * groove border, the padding and the `min-inline-size: min-content` that
17
+ * lets a fieldset overflow its column.
18
+ */
19
+ group: "m-0 flex min-w-0 flex-col gap-3 border-0 p-0"
20
+ },
21
+ variants: {
22
+ invalid: {
23
+ true: {
24
+ message: "font-bold text-intent-danger"
25
+ },
26
+ false: {
27
+ message: "text-content-muted"
28
+ }
29
+ },
30
+ /**
31
+ * Where the label sits, which is the whole difference between a text field
32
+ * and a boolean one.
33
+ *
34
+ * `stack` writes the label above the control. `inline` puts the control
35
+ * *inside* the label, which is Base UI's documented pattern for a checkbox
36
+ * or a switch: the label then owns the hit area, and `htmlFor` still points
37
+ * at the hidden input, so a click on the words toggles the control without
38
+ * anyone wiring an `onClick`.
39
+ */
40
+ layout: {
41
+ stack: {},
42
+ inline: {
43
+ label: "flex w-fit cursor-pointer items-center gap-3 text-sm text-content-primary data-[disabled]:cursor-not-allowed data-[disabled]:text-content-muted"
44
+ }
45
+ }
46
+ },
47
+ defaultVariants: {
48
+ invalid: false,
49
+ layout: "stack"
50
+ }
51
+ });
52
+ function accentStyle(accent, style) {
53
+ return { ...style, "--field-accent": accentVar(accent) };
54
+ }
55
+ var FieldFrame = forwardRef(function FieldFrame2({
56
+ label,
57
+ error,
58
+ helperText,
59
+ disabled,
60
+ layout = "stack",
61
+ className,
62
+ labelClassName: labelClassName2,
63
+ nativeLabel = true,
64
+ labelId,
65
+ children,
66
+ ...props
67
+ }, ref) {
68
+ const styles = frame({ invalid: Boolean(error), layout });
69
+ const labelElement = nativeLabel ? void 0 : /* @__PURE__ */ jsx("div", {});
70
+ return /* @__PURE__ */ jsxs(
71
+ BaseField.Root,
72
+ {
73
+ ref,
74
+ "data-slot": "field",
75
+ className: styles.root({ class: className }),
76
+ invalid: Boolean(error),
77
+ disabled,
78
+ ...props,
79
+ children: [
80
+ layout === "inline" ? (
81
+ // The control goes *inside* the label. A boolean control is a span with
82
+ // `role="checkbox"`, not a labelable element, so a sibling label would
83
+ // associate by `aria-labelledby` alone and the words would not be a hit
84
+ // target. Base UI's own Field examples nest it for the same reason.
85
+ label ? /* @__PURE__ */ jsxs(BaseField.Label, { "data-slot": "field-label", className: styles.label({ class: labelClassName2 }), children: [
86
+ children,
87
+ label
88
+ ] }) : children
89
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
90
+ label && /* @__PURE__ */ jsx(
91
+ BaseField.Label,
92
+ {
93
+ id: labelId,
94
+ nativeLabel,
95
+ render: labelElement,
96
+ "data-slot": "field-label",
97
+ className: styles.label({ class: labelClassName2 }),
98
+ children: label
99
+ }
100
+ ),
101
+ children
102
+ ] }),
103
+ /* @__PURE__ */ jsx(FieldMessage, { error, helperText })
104
+ ]
105
+ }
106
+ );
107
+ });
108
+ function FieldMessage({ error, helperText, id, slot = "field" }) {
109
+ const styles = frame({ invalid: Boolean(error) });
110
+ if (error) {
111
+ return /* @__PURE__ */ jsxs(
112
+ BaseField.Error,
113
+ {
114
+ match: true,
115
+ id,
116
+ "data-slot": `${slot}-error`,
117
+ className: styles.message(),
118
+ render: /* @__PURE__ */ jsx("span", {}),
119
+ children: [
120
+ "> ",
121
+ error
122
+ ]
123
+ }
124
+ );
125
+ }
126
+ if (helperText) {
127
+ return /* @__PURE__ */ jsxs(BaseField.Description, { id, "data-slot": `${slot}-description`, className: styles.message(), render: /* @__PURE__ */ jsx("span", {}), children: [
128
+ "> ",
129
+ helperText
130
+ ] });
131
+ }
132
+ return null;
133
+ }
134
+ function labelClassName(className) {
135
+ return frame().label({ class: className });
136
+ }
137
+ var GroupFrame = forwardRef(function GroupFrame2({ legend, error, helperText, disabled, className, render, children, ...props }, ref) {
138
+ const styles = frame({ invalid: Boolean(error) });
139
+ const messageId = useId();
140
+ const hasMessage = Boolean(error || helperText);
141
+ return /* @__PURE__ */ jsxs(
142
+ BaseField.Root,
143
+ {
144
+ "data-slot": "fieldset-field",
145
+ className: styles.root(),
146
+ invalid: Boolean(error),
147
+ disabled,
148
+ children: [
149
+ /* @__PURE__ */ jsxs(
150
+ BaseFieldset.Root,
151
+ {
152
+ ref,
153
+ render,
154
+ disabled,
155
+ "data-slot": "fieldset",
156
+ "data-invalid": error ? "" : void 0,
157
+ "aria-describedby": hasMessage ? messageId : void 0,
158
+ className: styles.group({ class: className }),
159
+ ...props,
160
+ children: [
161
+ legend,
162
+ children
163
+ ]
164
+ }
165
+ ),
166
+ /* @__PURE__ */ jsx(FieldMessage, { error, helperText, id: messageId, slot: "fieldset" })
167
+ ]
168
+ }
169
+ );
170
+ });
171
+ function FieldItem({ label, helperText, disabled, children }) {
172
+ const styles = frame({ layout: "inline" });
173
+ return /* @__PURE__ */ jsxs(BaseField.Item, { "data-slot": "field-item", className: "flex flex-col gap-1", disabled, children: [
174
+ label ? /* @__PURE__ */ jsxs(BaseField.Label, { "data-slot": "field-label", className: styles.label(), children: [
175
+ children,
176
+ label
177
+ ] }) : children,
178
+ helperText && /* @__PURE__ */ jsxs(
179
+ BaseField.Description,
180
+ {
181
+ "data-slot": "field-description",
182
+ className: styles.message({ class: "pl-9" }),
183
+ render: /* @__PURE__ */ jsx("span", {}),
184
+ children: [
185
+ "> ",
186
+ helperText
187
+ ]
188
+ }
189
+ )
190
+ ] });
191
+ }
192
+ export {
193
+ FieldFrame,
194
+ FieldItem,
195
+ FieldMessage,
196
+ GroupFrame,
197
+ accentStyle,
198
+ labelClassName
199
+ };
200
+ //# sourceMappingURL=fieldFrame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/fieldFrame.tsx"],"sourcesContent":["import { forwardRef, useId } from 'react';\nimport type { CSSProperties, HTMLAttributes, ReactElement, ReactNode } from 'react';\nimport { Field as BaseField } from '@base-ui/react/field';\nimport { Fieldset as BaseFieldset } from '@base-ui/react/fieldset';\nimport { recipe } from '../lib/recipe';\nimport { accentVar } from '../lib/theme';\nimport type { AccentToken } from '../lib/theme';\n\n/**\n * The one field implementation.\n *\n * It was inside `Input.tsx`, which was fine while the only fields were the\n * three text controls. It is a module of its own now because `Checkbox` and\n * `Switch` need exactly the same semantics in a different arrangement, and the\n * alternative — a second `Field.Root` composed by hand next door — is the\n * \"second field implementation\" #238 exists to prevent. One implementation is\n * also what makes a boolean and a text input agree about what `error` means.\n *\n * It carries three arrangements of that one contract: `FieldFrame` for a\n * single control, `GroupFrame` for a set of controls that answer one question\n * together (#239), and `FieldItem` for one option inside such a set. The group\n * is the reason the message moved into `FieldMessage`: a radio group's error\n * and a text input's error are the same element, wired the same way, and a\n * second copy is the drift this module exists to stop.\n *\n * Nothing exports this from `src/index.ts`, deliberately, for the reason\n * `dialogSurface.ts` states: it holds a `recipe`, whose type comes from\n * `tailwind-variants`, and publishing it would put that library back into the\n * `.d.ts`.\n */\n\nconst frame = recipe({\n slots: {\n root: 'flex w-full flex-col gap-1.5 font-mono',\n label: 'text-xs font-bold uppercase tracking-wider text-content-secondary',\n message: 'font-mono text-xs',\n /**\n * The grouping element — a `<fieldset>`, or the `radiogroup` a `RadioGroup`\n * renders in its place. The reset is the browser's fieldset chrome: the\n * groove border, the padding and the `min-inline-size: min-content` that\n * lets a fieldset overflow its column.\n */\n group: 'm-0 flex min-w-0 flex-col gap-3 border-0 p-0',\n },\n variants: {\n invalid: {\n true: {\n message: 'font-bold text-intent-danger',\n },\n false: {\n message: 'text-content-muted',\n },\n },\n /**\n * Where the label sits, which is the whole difference between a text field\n * and a boolean one.\n *\n * `stack` writes the label above the control. `inline` puts the control\n * *inside* the label, which is Base UI's documented pattern for a checkbox\n * or a switch: the label then owns the hit area, and `htmlFor` still points\n * at the hidden input, so a click on the words toggles the control without\n * anyone wiring an `onClick`.\n */\n layout: {\n stack: {},\n inline: {\n label:\n 'flex w-fit cursor-pointer items-center gap-3 text-sm text-content-primary '\n + 'data-[disabled]:cursor-not-allowed data-[disabled]:text-content-muted',\n },\n },\n },\n defaultVariants: {\n invalid: false,\n layout: 'stack',\n },\n});\n\n/** Shared by every control built on this frame. */\nexport interface FieldProps {\n /**\n * The visible label, wired to the control by `id` — generated when none is\n * supplied, so the association holds without the caller arranging it. Omit\n * it only where a visible label exists elsewhere, and then give the control\n * an `aria-label`: a field with no name is a field a screen reader cannot\n * announce.\n */\n label?: string;\n /**\n * The validation message. Its presence is also the invalid state: it sets\n * `aria-invalid`, switches the border to `intent.danger`, and joins the\n * control's `aria-describedby`, so it is read when the field takes focus.\n * It is not a live region: on submit, `ErrorSummary` is what announces the\n * errors. Pass the message, never a boolean — \"invalid\" without a reason leaves the\n * reader to guess what to change.\n */\n error?: string;\n /**\n * Standing guidance shown under the control — a format, a constraint, what\n * the value is for. Always visible, unlike `error`, and replaced by it while\n * one is set, because two competing instructions under one field is worse\n * than either.\n */\n helperText?: string;\n /**\n * Semantic accent for the focus border. Accepts an `Emphasis`\n * (`primary`…`quiet`) or an `Intent` (`info`/`success`/`warning`/`danger`);\n * the legacy hue names still resolve to the same values.\n */\n accent?: AccentToken;\n /**\n * Merged onto the control itself, not the wrapper — so it sizes the input\n * (`w-full`) rather than the label-and-error group around it.\n */\n className?: string;\n}\n\n/**\n * The props a boolean control adds to the shared field contract.\n *\n * Declared once and implemented by both `Checkbox` and `Switch`, because #238\n * is one capability in two presentations: a pair that disagreed about\n * `readOnly` or `onCheckedChange` would be the same defect as having no\n * control at all.\n *\n * `onCheckedChange` takes the new value and nothing else. Base UI passes event\n * details as a second argument; forwarding that would name a Base UI type in\n * the published `.d.ts`, which the confinement rule forbids.\n */\nexport interface BooleanFieldProps\n extends Omit<HTMLAttributes<HTMLSpanElement>, 'className' | 'defaultChecked' | 'onChange'>,\n FieldProps {\n /** Ticked or on. Supplying it makes the control controlled; pair it with `onCheckedChange`. */\n checked?: boolean;\n /** The starting value for an uncontrolled control. Ignored when `checked` is set. */\n defaultChecked?: boolean;\n /** Called with the new value whenever the user toggles the control. */\n onCheckedChange?: (checked: boolean) => void;\n /**\n * The name the value is submitted under. Base UI renders the hidden input\n * that carries it, so a control inside a `<form>` appears in `FormData`\n * without anything here rendering an input of its own.\n */\n name?: string;\n /** The value submitted when checked. Defaults to the native `\"on\"`. */\n value?: string;\n /** Ignores user interaction, and greys the label with it. */\n disabled?: boolean;\n /** Announces and renders the current value, but refuses to change it. */\n readOnly?: boolean;\n /** Marks the control as required for form submission. */\n required?: boolean;\n /** Placed on the control, never on the wrapper — see `FieldFrame`. */\n id?: string;\n}\n\n/**\n * The accent is known at runtime, so it travels as a custom property.\n *\n * The caller's `style` is merged in rather than left to a later prop spread:\n * spread after `style={accentStyle(…)}`, any `style={{ width: 320 }}` replaced\n * the whole object and took `--field-accent` with it, leaving a checked\n * control with no fill. The accent is set last so a caller cannot unset it by\n * accident.\n */\nexport function accentStyle(accent: AccentToken, style?: CSSProperties): CSSProperties {\n return { ...style, '--field-accent': accentVar(accent) } as CSSProperties;\n}\n\nexport interface FieldFrameProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'className' | 'children'> {\n label?: string;\n error?: string;\n helperText?: string;\n /** Passed to `Field.Root`, so the label and message grey out with the control. */\n disabled?: boolean;\n /** `stack` for a text control, `inline` for a boolean one. */\n layout?: 'stack' | 'inline';\n /** Merged onto the wrapper. The control's own `className` is its business. */\n className?: string;\n /** Merged onto the label, for the row a boolean control lays out. */\n labelClassName?: string;\n /**\n * Whether the label is a native `<label>`. `false` renders it as a `div`\n * that names the control by `aria-labelledby` and focuses it on click — for\n * a control that is a button, where a native label's click would activate\n * it. `Select`'s listbox trigger is that case (#164).\n */\n nativeLabel?: boolean;\n /** An explicit id for the label, for a second element that must be named by it. */\n labelId?: string;\n children: ReactNode;\n}\n\n/**\n * The label, message and ARIA wiring every control shares.\n *\n * This was a hand-rolled `useField()` returning `id`, `aria-invalid` and\n * `aria-describedby`. Base UI's `Field` is the public equivalent, and it owns\n * two things the hook did not: `aria-describedby` composes when a field carries\n * *both* a description and an error, and the validity state is exposed to\n * descendants — `CheckboxRootState extends FieldRootState`, so `Checkbox` and\n * `Switch` inherit `disabled`, `touched`, `dirty`, `valid`, `filled` and\n * `focused` rather than re-deriving them.\n *\n * `error` here is a prop rather than native constraint validation, so\n * `Field.Root` is told it is `invalid` and `Field.Error` is matched\n * unconditionally. That keeps the control externally driven, which is what the\n * existing API promises.\n *\n * Both parts render a `span` rather than Base UI's default `p`, because a\n * message sits inside a flex column beside the control and a block paragraph\n * would change the layout.\n *\n * A caller's `id` goes on the *control*, never on `Field.Root` — the root's\n * `id` names the wrapper, and passing it there silently leaves the control on\n * a generated id while the label still points at it. The symptom is an `id`\n * prop that appears to do nothing.\n */\nexport const FieldFrame = forwardRef<HTMLDivElement, FieldFrameProps>(function FieldFrame(\n {\n label,\n error,\n helperText,\n disabled,\n layout = 'stack',\n className,\n labelClassName,\n nativeLabel = true,\n labelId,\n children,\n ...props\n },\n ref,\n) {\n const styles = frame({ invalid: Boolean(error), layout });\n // Base UI warns when a non-native label renders a `<label>`, so the element\n // follows the flag.\n const labelElement = nativeLabel ? undefined : <div />;\n\n return (\n <BaseField.Root\n ref={ref}\n data-slot=\"field\"\n className={styles.root({ class: className })}\n invalid={Boolean(error)}\n disabled={disabled}\n {...props}\n >\n {layout === 'inline' ? (\n // The control goes *inside* the label. A boolean control is a span with\n // `role=\"checkbox\"`, not a labelable element, so a sibling label would\n // associate by `aria-labelledby` alone and the words would not be a hit\n // target. Base UI's own Field examples nest it for the same reason.\n label ? (\n <BaseField.Label data-slot=\"field-label\" className={styles.label({ class: labelClassName })}>\n {children}\n {label}\n </BaseField.Label>\n ) : (\n children\n )\n ) : (\n <>\n {label && (\n <BaseField.Label\n id={labelId}\n nativeLabel={nativeLabel}\n render={labelElement}\n data-slot=\"field-label\"\n className={styles.label({ class: labelClassName })}\n >\n {label}\n </BaseField.Label>\n )}\n {children}\n </>\n )}\n <FieldMessage error={error} helperText={helperText} />\n </BaseField.Root>\n );\n});\n\nexport interface FieldMessageProps {\n error?: string;\n helperText?: string;\n /** Names the rendered message, for a group that must point at it by hand. */\n id?: string;\n /** `field` for a single control's message, `fieldset` for a group's. */\n slot?: 'field' | 'fieldset';\n}\n\n/**\n * The message under a field: the error when there is one, the standing\n * guidance otherwise, and nothing when neither is set.\n *\n * Its own component because two frames render it. Both parts register their\n * `id` with the enclosing `Field.Root`, which is what puts them in the\n * control's `aria-describedby` — for a group, in the `radiogroup`'s, and in\n * every option's through `Field.Item`'s inherited message ids.\n *\n * ## An error is described, not announced (#299)\n *\n * The error is not a live region: no `role=\"alert\"`, no `role=\"status\"`, no\n * `aria-live`. Base UI's `Field.Error` adds none of its own either. The error\n * reaches a screen reader through the control's `aria-describedby`, together\n * with `aria-invalid`, so it is read when the reader arrives at the field.\n *\n * It was `role=\"alert\"`, and that collided with `ErrorSummary`. On a failed\n * submit every invalid field fired an assertive alert at the moment the\n * summary took focus, so a four-error form read four alerts and then the same\n * four messages again in the summary. This is GOV.UK's pattern: the summary\n * takes focus and says what is wrong, each link moves focus to its field, and\n * the field's own error is read as its description on arrival.\n *\n * Inline or on-blur validation, with no summary, is the one case where a live\n * error would help. No consumer does it yet. A form that needs it should own\n * one live region for the whole form, rather than every field carrying its own.\n */\nexport function FieldMessage({ error, helperText, id, slot = 'field' }: FieldMessageProps) {\n const styles = frame({ invalid: Boolean(error) });\n\n if (error) {\n return (\n <BaseField.Error\n match\n id={id}\n data-slot={`${slot}-error`}\n className={styles.message()}\n render={<span />}\n >\n &gt; {error}\n </BaseField.Error>\n );\n }\n if (helperText) {\n return (\n <BaseField.Description id={id} data-slot={`${slot}-description`} className={styles.message()} render={<span />}>\n &gt; {helperText}\n </BaseField.Description>\n );\n }\n return null;\n}\n\n/**\n * The label typography, for the one label this module does not render: the\n * public `Legend`, which lives beside `Fieldset` so its type is published under\n * its own name. A group's name and a field's name are the same kind of text,\n * and reading the class from here is what keeps them the same.\n */\nexport function labelClassName(className?: string): string {\n return frame().label({ class: className });\n}\n\nexport interface GroupFrameProps\n extends Omit<HTMLAttributes<HTMLElement>, 'className' | 'children'> {\n /** The group's name — a rendered `Legend`, placed first inside the group. */\n legend?: ReactNode;\n error?: string;\n helperText?: string;\n /** Disables the whole group: the fieldset, and every `Field` inside it. */\n disabled?: boolean;\n /** Merged onto the grouping element, not the wrapper around it and its message. */\n className?: string;\n /**\n * What the group renders as. Omitted, it is a `<fieldset>`. `RadioGroup`\n * passes Base UI's `RadioGroup` here, so the `radiogroup` role, the roving\n * focus and the fieldset's legend wiring land on one element rather than two\n * nested ones each claiming to be the group.\n */\n render?: ReactElement;\n children: ReactNode;\n}\n\n/**\n * The label, message and ARIA wiring for a *set* of controls.\n *\n * `FieldFrame` models one control with one label, one description and one\n * error. A group needs all three for the set, and the failure when it does\n * not have them is specific: each option gets a label, the group gets none,\n * and the group's error is announced against whichever option carried it.\n *\n * So the set is a `Field.Root` of its own, wrapping a Base UI `Fieldset.Root`:\n *\n * - The legend names the group. `Fieldset.Legend` registers its id and the\n * fieldset (or the `radiogroup` rendered in its place) takes it as\n * `aria-labelledby` — one accessible name for the set.\n * - The message is `FieldMessage`, registered with the group's `Field.Root`.\n * A `radiogroup` is that field's control and composes it into its own\n * `aria-describedby`; a plain `<fieldset>` is not a Base UI control, so the\n * id is also written onto it here by hand. Either way the error describes\n * the group rather than one option.\n * - `aria-invalid` is left to the control. It is permitted on `radiogroup`,\n * which Base UI sets from the field's validity, and not on `group` — the\n * role a `<fieldset>` has — so a plain fieldset carries `data-invalid` for\n * styling and announces its error through the description alone.\n * - `disabled` goes to both the field and the fieldset. The fieldset's context\n * is what every `Field.Root` inside it reads, so a `Checkbox` in a disabled\n * `Fieldset` greys and refuses input without being told.\n *\n * The message sits *after* the grouping element, inside the wrapper, because a\n * `radiogroup` owns radios, and a message span among them is a child the role\n * does not allow.\n */\nexport const GroupFrame = forwardRef<HTMLElement, GroupFrameProps>(function GroupFrame(\n { legend, error, helperText, disabled, className, render, children, ...props },\n ref,\n) {\n const styles = frame({ invalid: Boolean(error) });\n const messageId = useId();\n const hasMessage = Boolean(error || helperText);\n\n return (\n <BaseField.Root\n data-slot=\"fieldset-field\"\n className={styles.root()}\n invalid={Boolean(error)}\n disabled={disabled}\n >\n <BaseFieldset.Root\n ref={ref}\n render={render}\n disabled={disabled}\n data-slot=\"fieldset\"\n data-invalid={error ? '' : undefined}\n aria-describedby={hasMessage ? messageId : undefined}\n className={styles.group({ class: className })}\n {...props}\n >\n {legend}\n {children}\n </BaseFieldset.Root>\n <FieldMessage error={error} helperText={helperText} id={messageId} slot=\"fieldset\" />\n </BaseField.Root>\n );\n});\n\nexport interface FieldItemProps {\n label?: string;\n /** Guidance for this one option, described on it alone. */\n helperText?: string;\n /** Disables this option without disabling the rest of the group. */\n disabled?: boolean;\n children: ReactNode;\n}\n\n/**\n * One option inside a group: its own label and description, the group's\n * error.\n *\n * A `Field.Item`, not a nested `Field.Root`. A second root would give the\n * option a field of its own, and the group's control — the `radiogroup` —\n * would stop seeing the option's hidden input, which is the half that makes\n * the selected value appear in `FormData`. `Field.Item` scopes only the label\n * and description, and inherits the group's message ids, so the group error\n * still reaches each option's `aria-describedby` while the option's own\n * guidance reaches that option only.\n *\n * The label is the `inline` one `FieldFrame` gives a boolean — the control\n * inside it, so the words are the hit target.\n */\nexport function FieldItem({ label, helperText, disabled, children }: FieldItemProps) {\n const styles = frame({ layout: 'inline' });\n\n return (\n <BaseField.Item data-slot=\"field-item\" className=\"flex flex-col gap-1\" disabled={disabled}>\n {label ? (\n <BaseField.Label data-slot=\"field-label\" className={styles.label()}>\n {children}\n {label}\n </BaseField.Label>\n ) : (\n children\n )}\n {helperText && (\n <BaseField.Description\n data-slot=\"field-description\"\n // Indented past the control, so the guidance sits under its words.\n className={styles.message({ class: 'pl-9' })}\n render={<span />}\n >\n &gt; {helperText}\n </BaseField.Description>\n )}\n </BaseField.Item>\n );\n}\n"],"mappings":"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","names":["FieldFrame","labelClassName","GroupFrame"]}
@@ -0,0 +1,35 @@
1
+ // src/components/floatingSurface.ts
2
+ import { recipe } from "../lib/recipe.js";
3
+ var floatingSurface = recipe({
4
+ slots: {
5
+ positioner: "z-top will-change-auto!",
6
+ popup: "max-w-(--available-width) border-2 border-edge-strong bg-surface-raised font-mono text-content-primary shadow-hard-md transition-opacity duration-quick ease-brutalist data-[starting-style]:opacity-0 data-[ending-style]:opacity-0 motion-reduce:transition-none"
7
+ }
8
+ });
9
+ var floatingParts = recipe({
10
+ slots: {
11
+ header: "flex items-center justify-between gap-3 border-b-2 border-edge-strong bg-surface-base px-4 py-2",
12
+ title: "font-display text-sm font-bold uppercase tracking-wider text-content-primary",
13
+ close: "border-2 border-edge-strong bg-surface-raised px-1.5 font-mono text-base font-bold leading-none text-content-primary transition-colors hover:bg-surface-base hover:text-accent-tertiary motion-reduce:transition-none",
14
+ body: "px-4 py-3 font-sans text-sm leading-relaxed text-content-primary",
15
+ // A highlighted item is marked by the accent *fill* and the inverse ink —
16
+ // `text.inverse on accent.primary`, a pair `check:contrast` audits on
17
+ // every Level. Not a surface pair: `docs/theming.md` § Selection State is
18
+ // why a raised-against-base highlight is ruled out.
19
+ item: "flex w-full cursor-default select-none items-center gap-2 px-3 py-1.5 text-left text-sm text-content-primary data-[highlighted]:bg-accent-primary data-[highlighted]:text-content-inverse",
20
+ // A disabled item is muted ink, and stays muted when the arrow keys land
21
+ // on it: the highlight is the page ground (`text.muted on surface.base`)
22
+ // rather than the accent fill, so a focused disabled item never looks
23
+ // actionable. The focus ring still marks where the keyboard is.
24
+ disabled: "cursor-not-allowed text-content-muted data-[highlighted]:bg-surface-base data-[highlighted]:text-content-muted",
25
+ danger: "text-intent-danger data-[highlighted]:bg-intent-danger",
26
+ indicator: "flex w-4 shrink-0 justify-center font-bold",
27
+ groupLabel: "px-3 pt-2 pb-1 text-xs font-bold uppercase tracking-wider text-content-secondary",
28
+ separator: "my-1 h-0.5 bg-edge-strong"
29
+ }
30
+ });
31
+ export {
32
+ floatingParts,
33
+ floatingSurface
34
+ };
35
+ //# sourceMappingURL=floatingSurface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/floatingSurface.ts"],"sourcesContent":["import { recipe } from '../lib/recipe';\n\n// The chrome `Tooltip`, `Popover` and `Menu` share (#166).\n//\n// A module of its own for the reason `dialogSurface.ts` is one: `src/index.ts`\n// re-exports each component file wholesale, and a `recipe` in the published\n// surface would put `tailwind-variants`' types into the `.d.ts`. Nothing\n// exports this file.\n//\n// The three are one module because they are one kind of thing — an element\n// anchored to another, positioned by the same Floating UI engine Base UI\n// wraps, dismissed by the same stack #162 established. Three recipes would\n// drift into three answers to the same three questions: which layer, which\n// motion, which edge.\n//\n// ## The layer is `z-top`, the dialogs' layer, on purpose\n//\n// `--ds-layer-overlay` sits below `--ds-layer-top`, and the obvious reading is\n// that a floating element belongs on it. It does not: a menu opened from a\n// control inside a `Modal` would then paint *underneath* the modal it was\n// opened from. Every surface here is portalled to the end of `<body>`, so one\n// layer plus DOM order is already the stack — whichever opened last paints\n// last, which is the same order Escape unwinds them in.\n//\n// ## Motion\n//\n// Enter and exit are `data-starting-style` / `data-ending-style`, the dialogs'\n// and the toast's attributes, over `--duration-quick` and `--ease-brutalist`.\n// Opacity only: a floating element that also scaled or slid would be a\n// second movement arriving at the pointer, and the hard shadow already says\n// which way is up. `motion-reduce:transition-none` drops the fade entirely.\n//\n// ## Collision\n//\n// Nothing here positions. `--available-width` (here) and `--available-height`\n// (the popover's and the menu's caps) are the Positioner's own measurements of\n// the room between the anchor and the viewport edge, so at a phone's width a\n// popup shrinks to fit rather than running off-screen once the engine has\n// flipped and shifted it as far as it can.\n//\n// ## No compositor layer: `will-change` is forced back to `auto`\n//\n// Floating UI writes `will-change: transform` onto the positioner's inline\n// style, but only when `devicePixelRatio >= 1.5` — so on a phone and never on\n// a desktop at 1x. That promotes the popup to its own compositor layer, which\n// Chromium rasterises on its own schedule and may then draw resampled at a\n// sub-pixel offset rather than painted on the pixel grid with the page. Which\n// of the two you get depends on timing, not on the markup: #293 was the\n// mobile tooltip failing its baseline with the whole popup smeared\n// horizontally by a fraction of a pixel, box and text alike, then matching on\n// retry. The desktop rows of the same components, with no layer, never have.\n//\n// The hint buys smoother repositioning while a scroll moves the anchor, which\n// a label-sized popup does not need, and costs soft text on exactly the\n// high-density screens it is added for. `!` because the value is inline, and\n// an inline declaration beats every non-important class.\n//\n// Each component extends this recipe in its own file — `recipe({ extend:\n// floatingSurface, … })` — with the box that is its own: a tooltip's label\n// size, a popover's width, a menu's list padding. The shared half stays here,\n// so the three cannot disagree about layer, edge, fill or motion.\nexport const floatingSurface = recipe({\n slots: {\n positioner: 'z-top will-change-auto!',\n popup:\n 'max-w-(--available-width) border-2 border-edge-strong bg-surface-raised font-mono text-content-primary '\n + 'shadow-hard-md transition-opacity duration-quick ease-brutalist '\n + 'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0 motion-reduce:transition-none',\n },\n});\n\n// The parts only `Popover` and `Menu` render. Kept beside the surface so the\n// header a popover wears is visibly the dialogs' header at a smaller scale.\nexport const floatingParts = recipe({\n slots: {\n header:\n 'flex items-center justify-between gap-3 border-b-2 border-edge-strong bg-surface-base px-4 py-2',\n title: 'font-display text-sm font-bold uppercase tracking-wider text-content-primary',\n close:\n 'border-2 border-edge-strong bg-surface-raised px-1.5 font-mono text-base font-bold leading-none '\n + 'text-content-primary transition-colors hover:bg-surface-base hover:text-accent-tertiary '\n + 'motion-reduce:transition-none',\n body: 'px-4 py-3 font-sans text-sm leading-relaxed text-content-primary',\n // A highlighted item is marked by the accent *fill* and the inverse ink —\n // `text.inverse on accent.primary`, a pair `check:contrast` audits on\n // every Level. Not a surface pair: `docs/theming.md` § Selection State is\n // why a raised-against-base highlight is ruled out.\n item:\n 'flex w-full cursor-default select-none items-center gap-2 px-3 py-1.5 text-left text-sm '\n + 'text-content-primary data-[highlighted]:bg-accent-primary data-[highlighted]:text-content-inverse',\n // A disabled item is muted ink, and stays muted when the arrow keys land\n // on it: the highlight is the page ground (`text.muted on surface.base`)\n // rather than the accent fill, so a focused disabled item never looks\n // actionable. The focus ring still marks where the keyboard is.\n disabled:\n 'cursor-not-allowed text-content-muted data-[highlighted]:bg-surface-base '\n + 'data-[highlighted]:text-content-muted',\n danger: 'text-intent-danger data-[highlighted]:bg-intent-danger',\n indicator: 'flex w-4 shrink-0 justify-center font-bold',\n groupLabel:\n 'px-3 pt-2 pb-1 text-xs font-bold uppercase tracking-wider text-content-secondary',\n separator: 'my-1 h-0.5 bg-edge-strong',\n },\n});\n"],"mappings":";AAAA,SAAS,cAAc;AA6DhB,IAAM,kBAAkB,OAAO;AAAA,EACpC,OAAO;AAAA,IACL,YAAY;AAAA,IACZ,OACE;AAAA,EAGJ;AACF,CAAC;AAIM,IAAM,gBAAgB,OAAO;AAAA,EAClC,OAAO;AAAA,IACL,QACE;AAAA,IACF,OAAO;AAAA,IACP,OACE;AAAA,IAGF,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,IAKN,MACE;AAAA;AAAA;AAAA;AAAA;AAAA,IAMF,UACE;AAAA,IAEF,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,YACE;AAAA,IACF,WAAW;AAAA,EACb;AACF,CAAC;","names":[]}
@@ -0,0 +1,47 @@
1
+ // src/components/marketing/CTASection.tsx
2
+ import { forwardRef, useId } from "react";
3
+ import { recipe } from "../../lib/recipe.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ var ctaSection = recipe({
6
+ slots: {
7
+ root: "my-12 border-4 border-edge-strong bg-surface-raised px-6 py-12 md:px-12",
8
+ heading: "font-display text-[2rem] font-black uppercase leading-tight text-content-primary",
9
+ body: "mt-4 max-w-2xl font-mono text-sm leading-relaxed text-content-secondary",
10
+ actions: "mt-8 flex flex-wrap gap-4"
11
+ },
12
+ variants: {
13
+ accent: {
14
+ primary: { root: "shadow-hard-accent-primary" },
15
+ secondary: { root: "shadow-hard-accent-secondary" },
16
+ tertiary: { root: "shadow-hard-accent-tertiary" }
17
+ },
18
+ align: {
19
+ center: { root: "text-center", body: "mx-auto", actions: "justify-center" },
20
+ start: { root: "text-left", actions: "justify-start" }
21
+ }
22
+ },
23
+ defaultVariants: { accent: "primary", align: "center" }
24
+ });
25
+ var CTASection = forwardRef(function CTASection2({ title, children, actions, accent = "primary", align = "center", className, ...props }, ref) {
26
+ const styles = ctaSection({ accent, align });
27
+ const titleId = useId();
28
+ return /* @__PURE__ */ jsxs(
29
+ "section",
30
+ {
31
+ ref,
32
+ "data-slot": "cta-section",
33
+ "aria-labelledby": titleId,
34
+ ...props,
35
+ className: styles.root({ class: className }),
36
+ children: [
37
+ /* @__PURE__ */ jsx("h2", { id: titleId, className: styles.heading(), children: title }),
38
+ children ? /* @__PURE__ */ jsx("div", { "data-slot": "cta-section-body", className: styles.body(), children }) : null,
39
+ actions ? /* @__PURE__ */ jsx("div", { "data-slot": "cta-section-actions", className: styles.actions(), children: actions }) : null
40
+ ]
41
+ }
42
+ );
43
+ });
44
+ export {
45
+ CTASection
46
+ };
47
+ //# sourceMappingURL=CTASection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/marketing/CTASection.tsx"],"sourcesContent":["import { forwardRef, useId } from 'react';\nimport type { HTMLAttributes, ReactNode } from 'react';\nimport { recipe } from '../../lib/recipe';\nimport type { SectionAlign } from './SectionGrid';\n\n/** The roles a call-to-action band's offset shadow may take. */\nexport type CTASectionAccent = 'primary' | 'secondary' | 'tertiary';\n\nexport interface CTASectionProps\n extends Omit<HTMLAttributes<HTMLElement>, 'title' | 'children' | 'className'> {\n /** The ask, rendered as the band's `<h2>` — and the name of the section. */\n title: ReactNode;\n /** A sentence under the ask: what happens next, what it costs. */\n children?: ReactNode;\n /** The buttons — normally one primary action and at most one alternative. */\n actions?: ReactNode;\n /** The role of the band's offset shadow. `primary` by default. */\n accent?: CTASectionAccent;\n /** Where the text and actions sit — `center` (the default) or `start`. */\n align?: SectionAlign;\n /** Merged into the `<section>`'s classes. */\n className?: string;\n}\n\nconst ctaSection = recipe({\n slots: {\n root: 'my-12 border-4 border-edge-strong bg-surface-raised px-6 py-12 md:px-12',\n heading: 'font-display text-[2rem] font-black uppercase leading-tight text-content-primary',\n body: 'mt-4 max-w-2xl font-mono text-sm leading-relaxed text-content-secondary',\n actions: 'mt-8 flex flex-wrap gap-4',\n },\n variants: {\n accent: {\n primary: { root: 'shadow-hard-accent-primary' },\n secondary: { root: 'shadow-hard-accent-secondary' },\n tertiary: { root: 'shadow-hard-accent-tertiary' },\n },\n align: {\n center: { root: 'text-center', body: 'mx-auto', actions: 'justify-center' },\n start: { root: 'text-left', actions: 'justify-start' },\n },\n },\n defaultVariants: { accent: 'primary', align: 'center' },\n});\n\n/**\n * A closing call to action: a bordered band with the ask, a line under it and\n * the buttons, set apart from the sections around it by a hard offset shadow.\n *\n * It holds no copy and no buttons of its own — `Start free`, `Read the docs`\n * and `Book a call` are the page's words — so the same band ends a product\n * page, sits between two sections of a project site, or closes a post.\n *\n * @example\n * ```tsx\n * <CTASection\n * title=\"Ready when you are\"\n * actions={<Button href=\"/start\" variant=\"primary\" bracketed>START</Button>}\n * >\n * No card required.\n * </CTASection>\n * ```\n */\nexport const CTASection = forwardRef<HTMLElement, CTASectionProps>(function CTASection(\n { title, children, actions, accent = 'primary', align = 'center', className, ...props },\n ref,\n) {\n const styles = ctaSection({ accent, align });\n const titleId = useId();\n\n return (\n <section\n ref={ref}\n data-slot=\"cta-section\"\n aria-labelledby={titleId}\n {...props}\n className={styles.root({ class: className })}\n >\n <h2 id={titleId} className={styles.heading()}>\n {title}\n </h2>\n {children ? (\n <div data-slot=\"cta-section-body\" className={styles.body()}>\n {children}\n </div>\n ) : null}\n {actions ? (\n <div data-slot=\"cta-section-actions\" className={styles.actions()}>\n {actions}\n </div>\n ) : null}\n </section>\n );\n});\n"],"mappings":";AAAA,SAAS,YAAY,aAAa;AAElC,SAAS,cAAc;AAqEnB,SAOE,KAPF;AA/CJ,IAAM,aAAa,OAAO;AAAA,EACxB,OAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,MAAM;AAAA,IACN,SAAS;AAAA,EACX;AAAA,EACA,UAAU;AAAA,IACR,QAAQ;AAAA,MACN,SAAS,EAAE,MAAM,6BAA6B;AAAA,MAC9C,WAAW,EAAE,MAAM,+BAA+B;AAAA,MAClD,UAAU,EAAE,MAAM,8BAA8B;AAAA,IAClD;AAAA,IACA,OAAO;AAAA,MACL,QAAQ,EAAE,MAAM,eAAe,MAAM,WAAW,SAAS,iBAAiB;AAAA,MAC1E,OAAO,EAAE,MAAM,aAAa,SAAS,gBAAgB;AAAA,IACvD;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,QAAQ,WAAW,OAAO,SAAS;AACxD,CAAC;AAoBM,IAAM,aAAa,WAAyC,SAASA,YAC1E,EAAE,OAAO,UAAU,SAAS,SAAS,WAAW,QAAQ,UAAU,WAAW,GAAG,MAAM,GACtF,KACA;AACA,QAAM,SAAS,WAAW,EAAE,QAAQ,MAAM,CAAC;AAC3C,QAAM,UAAU,MAAM;AAEtB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,aAAU;AAAA,MACV,mBAAiB;AAAA,MAChB,GAAG;AAAA,MACJ,WAAW,OAAO,KAAK,EAAE,OAAO,UAAU,CAAC;AAAA,MAE3C;AAAA,4BAAC,QAAG,IAAI,SAAS,WAAW,OAAO,QAAQ,GACxC,iBACH;AAAA,QACC,WACC,oBAAC,SAAI,aAAU,oBAAmB,WAAW,OAAO,KAAK,GACtD,UACH,IACE;AAAA,QACH,UACC,oBAAC,SAAI,aAAU,uBAAsB,WAAW,OAAO,QAAQ,GAC5D,mBACH,IACE;AAAA;AAAA;AAAA,EACN;AAEJ,CAAC;","names":["CTASection"]}
@@ -0,0 +1,49 @@
1
+ // src/components/marketing/FeatureGrid.tsx
2
+ import { forwardRef } from "react";
3
+ import { recipe } from "../../lib/recipe.js";
4
+ import { accentTextClass } from "../../lib/accentClasses.js";
5
+ import { Card } from "../Card.js";
6
+ import { SectionGrid } from "./SectionGrid.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var FeatureGrid = forwardRef(function FeatureGrid2(props, ref) {
9
+ return /* @__PURE__ */ jsx(SectionGrid, { ref, slot: "feature-grid", ...props });
10
+ });
11
+ var feature = recipe({
12
+ slots: {
13
+ body: "mt-2",
14
+ icon: "block",
15
+ description: "mt-3 font-mono text-[0.85rem] leading-[1.6] text-content-primary opacity-90"
16
+ }
17
+ });
18
+ var Feature = forwardRef(function Feature2({ title, icon, accent, children, className, ...props }, ref) {
19
+ const styles = feature();
20
+ return /* @__PURE__ */ jsx(
21
+ Card,
22
+ {
23
+ ref,
24
+ variant: "panel",
25
+ accent,
26
+ title,
27
+ "data-slot": "feature",
28
+ ...props,
29
+ className,
30
+ children: /* @__PURE__ */ jsxs("div", { "data-slot": "feature-body", className: styles.body(), children: [
31
+ icon ? /* @__PURE__ */ jsx(
32
+ "span",
33
+ {
34
+ "data-slot": "feature-icon",
35
+ "aria-hidden": "true",
36
+ className: styles.icon({ class: accent ? accentTextClass(accent) : void 0 }),
37
+ children: icon
38
+ }
39
+ ) : null,
40
+ children ? /* @__PURE__ */ jsx("div", { "data-slot": "feature-description", className: styles.description(), children }) : null
41
+ ] })
42
+ }
43
+ );
44
+ });
45
+ export {
46
+ Feature,
47
+ FeatureGrid
48
+ };
49
+ //# sourceMappingURL=FeatureGrid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/marketing/FeatureGrid.tsx"],"sourcesContent":["import { forwardRef } from 'react';\nimport type { HTMLAttributes, ReactNode } from 'react';\nimport { recipe } from '../../lib/recipe';\nimport { accentTextClass } from '../../lib/accentClasses';\nimport type { AccentToken } from '../../lib/theme';\nimport { Card } from '../Card';\nimport { SectionGrid } from './SectionGrid';\nimport type { SectionAlign, SectionColumns } from './SectionGrid';\n\nexport type { SectionAlign, SectionColumns };\n\nexport interface FeatureGridProps\n extends Omit<HTMLAttributes<HTMLElement>, 'title' | 'className'> {\n /**\n * The section's heading, rendered as its `<h2>`. When present the section is\n * named by it, so it becomes a landmark a screen reader can list.\n */\n title?: ReactNode;\n /** A line under the heading — what the features add up to. */\n description?: ReactNode;\n /**\n * How many columns at desktop widths: `2`, `3` (the default) or `4`. Below\n * `md` the grid is always one column, so a phone never gets three cramped\n * cards side by side.\n */\n columns?: SectionColumns;\n /** Where the heading block sits — `center` (the default) or `start`. */\n align?: SectionAlign;\n /** The cells — normally `Feature`s. */\n children?: ReactNode;\n /** Merged into the `<section>`'s classes. Spacing between sections is the page's to set. */\n className?: string;\n}\n\n/**\n * A marketing section of features: an optional heading and line, over a grid\n * of {@link Feature} cells.\n *\n * A layout, not content. It holds no copy, no icons and no product name — the\n * page passes all of them — so the same section is a product's feature list, a\n * project's principles, or a service's \"how it works\".\n *\n * The grid is one column below `md` and `columns` wide above it. Vertical\n * margin is `my-12`, which collapses with a neighbouring section's, so a page\n * of sections stacked in a column spaces itself.\n *\n * @example\n * ```tsx\n * <FeatureGrid title=\"Built for control\" columns={3}>\n * <Feature title=\"Sync\" icon={<Cpu size={28} />} accent=\"primary\">\n * Changes land everywhere at once.\n * </Feature>\n * </FeatureGrid>\n * ```\n */\nexport const FeatureGrid = forwardRef<HTMLElement, FeatureGridProps>(function FeatureGrid(props, ref) {\n return <SectionGrid ref={ref} slot=\"feature-grid\" {...props} />;\n});\n\nexport interface FeatureProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {\n /** The feature's name, set as the cell's `<h3>` in the accent. */\n title: string;\n /**\n * A decorative icon above the text — a `lucide-react` icon at about 28px.\n * Coloured by `accent`, and hidden from assistive technology: the title\n * already names the feature.\n */\n icon?: ReactNode;\n /** The semantic accent for the stripe, the title and the icon. None by default. */\n accent?: AccentToken;\n /** The description — a sentence or two. */\n children?: ReactNode;\n /** Merged into the cell's classes. */\n className?: string;\n}\n\nconst feature = recipe({\n slots: {\n body: 'mt-2',\n icon: 'block',\n description: 'mt-3 font-mono text-[0.85rem] leading-[1.6] text-content-primary opacity-90',\n },\n});\n\n/**\n * One cell of a {@link FeatureGrid}: a titled `Card` panel with an optional\n * icon and a short description.\n *\n * The accent is the whole colour budget — the panel's stripe, its title and\n * the icon all take it — so choose it by role, and let neighbouring cells\n * differ to tell them apart.\n *\n * @example\n * ```tsx\n * <Feature title=\"Backups\" icon={<Shield size={28} />} accent=\"secondary\">\n * Written on a schedule, restored in one step.\n * </Feature>\n * ```\n */\nexport const Feature = forwardRef<HTMLDivElement, FeatureProps>(function Feature(\n { title, icon, accent, children, className, ...props },\n ref,\n) {\n const styles = feature();\n return (\n <Card\n ref={ref}\n variant=\"panel\"\n accent={accent}\n title={title}\n data-slot=\"feature\"\n {...props}\n className={className}\n >\n <div data-slot=\"feature-body\" className={styles.body()}>\n {icon ? (\n <span\n data-slot=\"feature-icon\"\n aria-hidden=\"true\"\n className={styles.icon({ class: accent ? accentTextClass(accent) : undefined })}\n >\n {icon}\n </span>\n ) : null}\n {children ? (\n <div data-slot=\"feature-description\" className={styles.description()}>\n {children}\n </div>\n ) : null}\n </div>\n </Card>\n );\n});\n"],"mappings":";AAAA,SAAS,kBAAkB;AAE3B,SAAS,cAAc;AACvB,SAAS,uBAAuB;AAEhC,SAAS,YAAY;AACrB,SAAS,mBAAmB;AAkDnB,cA0DH,YA1DG;AADF,IAAM,cAAc,WAA0C,SAASA,aAAY,OAAO,KAAK;AACpG,SAAO,oBAAC,eAAY,KAAU,MAAK,gBAAgB,GAAG,OAAO;AAC/D,CAAC;AAmBD,IAAM,UAAU,OAAO;AAAA,EACrB,OAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,aAAa;AAAA,EACf;AACF,CAAC;AAiBM,IAAM,UAAU,WAAyC,SAASC,SACvE,EAAE,OAAO,MAAM,QAAQ,UAAU,WAAW,GAAG,MAAM,GACrD,KACA;AACA,QAAM,SAAS,QAAQ;AACvB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA,aAAU;AAAA,MACT,GAAG;AAAA,MACJ;AAAA,MAEA,+BAAC,SAAI,aAAU,gBAAe,WAAW,OAAO,KAAK,GAClD;AAAA,eACC;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YACV,eAAY;AAAA,YACZ,WAAW,OAAO,KAAK,EAAE,OAAO,SAAS,gBAAgB,MAAM,IAAI,OAAU,CAAC;AAAA,YAE7E;AAAA;AAAA,QACH,IACE;AAAA,QACH,WACC,oBAAC,SAAI,aAAU,uBAAsB,WAAW,OAAO,YAAY,GAChE,UACH,IACE;AAAA,SACN;AAAA;AAAA,EACF;AAEJ,CAAC;","names":["FeatureGrid","Feature"]}
@@ -0,0 +1,44 @@
1
+ // src/components/marketing/Hero.tsx
2
+ import { forwardRef, useId } from "react";
3
+ import { recipe } from "../../lib/recipe.js";
4
+ import { PageTitle } from "../PageTitle.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ var hero = recipe({
7
+ slots: {
8
+ root: "mb-16",
9
+ eyebrow: "mb-6 inline-block",
10
+ actions: "mt-10 flex flex-wrap gap-5",
11
+ media: "mt-14"
12
+ },
13
+ variants: {
14
+ align: {
15
+ center: { root: "text-center", actions: "justify-center" },
16
+ start: { root: "text-left", actions: "justify-start" }
17
+ }
18
+ },
19
+ defaultVariants: { align: "center" }
20
+ });
21
+ var Hero = forwardRef(function Hero2({ title, subtitle, eyebrow, actions, children, bracketed = true, align = "center", className, ...props }, ref) {
22
+ const styles = hero({ align });
23
+ const titleId = useId();
24
+ return /* @__PURE__ */ jsxs(
25
+ "section",
26
+ {
27
+ ref,
28
+ "data-slot": "hero",
29
+ "aria-labelledby": titleId,
30
+ ...props,
31
+ className: styles.root({ class: className }),
32
+ children: [
33
+ eyebrow ? /* @__PURE__ */ jsx("span", { "data-slot": "hero-eyebrow", className: styles.eyebrow(), children: eyebrow }) : null,
34
+ /* @__PURE__ */ jsx(PageTitle, { id: titleId, subtitle, bracketed, children: title }),
35
+ actions ? /* @__PURE__ */ jsx("div", { "data-slot": "hero-actions", className: styles.actions(), children: actions }) : null,
36
+ children ? /* @__PURE__ */ jsx("div", { "data-slot": "hero-media", className: styles.media(), children }) : null
37
+ ]
38
+ }
39
+ );
40
+ });
41
+ export {
42
+ Hero
43
+ };
44
+ //# sourceMappingURL=Hero.js.map