@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,75 @@
1
+ // src/components/ReportDocument.tsx
2
+ import { forwardRef } from "react";
3
+ import { cn, recipe } from "../lib/recipe.js";
4
+ import { childrenToText, slugify } from "../lib/slug.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ var document_ = recipe({
7
+ slots: {
8
+ // `print:` keeps the document usable on paper, which is where a report
9
+ // often ends up. Utilities rather than an `@media print` block, because a
10
+ // stylesheet is not where styling lives here.
11
+ root: "mx-auto w-full max-w-4xl bg-surface-base px-6 py-12 font-sans text-content-primary sm:px-8 print:max-w-none print:px-0 print:py-0",
12
+ header: "mb-10 border-b-4 border-edge-strong pb-6",
13
+ title: "font-display text-3xl font-bold uppercase leading-tight tracking-tight sm:text-4xl",
14
+ subtitle: "mt-3 max-w-2xl font-sans text-base text-content-secondary",
15
+ meta: "mt-6 flex flex-wrap gap-x-8 gap-y-3",
16
+ metaItem: "flex flex-col gap-0.5",
17
+ metaLabel: "font-mono text-[0.6875rem] font-bold uppercase tracking-widest text-content-muted",
18
+ metaValue: "font-mono text-sm text-content-secondary",
19
+ body: "flex flex-col gap-8",
20
+ section: "scroll-mt-4 break-inside-avoid",
21
+ sectionTitle: "mb-4 border-b-2 border-edge-subtle pb-2 font-display text-xl font-bold uppercase tracking-tight",
22
+ sectionBody: "flex flex-col gap-4",
23
+ details: "group border-2 border-edge-strong bg-surface-raised",
24
+ summary: "flex cursor-pointer list-none items-center gap-3 px-4 py-3 font-mono text-sm font-bold text-content-primary marker:content-none hover:bg-surface-sunken",
25
+ summaryNote: "font-normal text-content-muted",
26
+ detailsBody: "border-t-2 border-edge-subtle p-4"
27
+ }
28
+ });
29
+ var ReportDocument = forwardRef(function ReportDocument2({ title, subtitle, meta, children, className, ...props }, ref) {
30
+ const styles = document_();
31
+ return /* @__PURE__ */ jsxs("article", { "data-slot": "report-document", ref, className: cn(styles.root(), className), ...props, children: [
32
+ /* @__PURE__ */ jsxs("header", { className: styles.header(), children: [
33
+ /* @__PURE__ */ jsx("h1", { className: styles.title(), children: title }),
34
+ subtitle ? /* @__PURE__ */ jsx("p", { className: styles.subtitle(), children: subtitle }) : null,
35
+ meta && meta.length > 0 ? /* @__PURE__ */ jsx("dl", { className: styles.meta(), children: meta.map((item) => /* @__PURE__ */ jsxs("div", { className: styles.metaItem(), children: [
36
+ /* @__PURE__ */ jsx("dt", { className: styles.metaLabel(), children: item.label }),
37
+ /* @__PURE__ */ jsx("dd", { className: styles.metaValue(), children: item.value })
38
+ ] }, item.label)) }) : null
39
+ ] }),
40
+ /* @__PURE__ */ jsx("div", { className: styles.body(), children })
41
+ ] });
42
+ });
43
+ var ReportSection = forwardRef(function ReportSection2({ title, id, children, className, ...props }, ref) {
44
+ const styles = document_();
45
+ const anchor = id ?? slugify(childrenToText(title));
46
+ return /* @__PURE__ */ jsxs("section", { "data-slot": "report-section", ref, id: anchor, className: cn(styles.section(), className), ...props, children: [
47
+ /* @__PURE__ */ jsx("h2", { className: styles.sectionTitle(), children: title }),
48
+ /* @__PURE__ */ jsx("div", { className: styles.sectionBody(), children })
49
+ ] });
50
+ });
51
+ var ReportDetails = forwardRef(
52
+ function ReportDetails2({ summary, note, open, children, className, ...props }, ref) {
53
+ const styles = document_();
54
+ return /* @__PURE__ */ jsxs("details", { "data-slot": "report-details", ref, open, className: cn(styles.details(), className), ...props, children: [
55
+ /* @__PURE__ */ jsxs("summary", { className: styles.summary(), children: [
56
+ /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "text-accent-primary", children: [
57
+ /* @__PURE__ */ jsx("span", { className: "group-open:hidden", children: "[+]" }),
58
+ /* @__PURE__ */ jsx("span", { className: "hidden group-open:inline", children: "[-]" })
59
+ ] }),
60
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: summary }),
61
+ note ? /* @__PURE__ */ jsx("span", { className: styles.summaryNote(), children: note }) : null
62
+ ] }),
63
+ /* @__PURE__ */ jsx("div", { className: styles.detailsBody(), children })
64
+ ] });
65
+ }
66
+ );
67
+ ReportDocument.displayName = "ReportDocument";
68
+ ReportSection.displayName = "ReportSection";
69
+ ReportDetails.displayName = "ReportDetails";
70
+ export {
71
+ ReportDetails,
72
+ ReportDocument,
73
+ ReportSection
74
+ };
75
+ //# sourceMappingURL=ReportDocument.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/ReportDocument.tsx"],"sourcesContent":["import { forwardRef } from 'react';\nimport type { HTMLAttributes, ReactNode } from 'react';\nimport { cn, recipe } from '../lib/recipe';\nimport { childrenToText, slugify } from '../lib/slug';\n\n/**\n * The page frame for a generated report: a titled header with a metadata strip,\n * and a body that spaces its own sections.\n *\n * It exists so an agent writing a report has one obvious starting point rather\n * than a layout decision. Everything inside is ordinary composition — `Card`,\n * `StatCard`, `DataTable`, `NoteBlock`, `Prose` — and the frame only owns the\n * measure, the header and the vertical rhythm.\n *\n * Every colour addresses a role, so the same markup renders on every rung of\n * the ladder and the generator's `--theme` flag is the only thing that decides.\n */\n\nconst document_ = recipe({\n slots: {\n // `print:` keeps the document usable on paper, which is where a report\n // often ends up. Utilities rather than an `@media print` block, because a\n // stylesheet is not where styling lives here.\n root: 'mx-auto w-full max-w-4xl bg-surface-base px-6 py-12 font-sans text-content-primary sm:px-8 print:max-w-none print:px-0 print:py-0',\n header: 'mb-10 border-b-4 border-edge-strong pb-6',\n title: 'font-display text-3xl font-bold uppercase leading-tight tracking-tight sm:text-4xl',\n subtitle: 'mt-3 max-w-2xl font-sans text-base text-content-secondary',\n meta: 'mt-6 flex flex-wrap gap-x-8 gap-y-3',\n metaItem: 'flex flex-col gap-0.5',\n metaLabel: 'font-mono text-[0.6875rem] font-bold uppercase tracking-widest text-content-muted',\n metaValue: 'font-mono text-sm text-content-secondary',\n body: 'flex flex-col gap-8',\n section: 'scroll-mt-4 break-inside-avoid',\n sectionTitle:\n 'mb-4 border-b-2 border-edge-subtle pb-2 font-display text-xl font-bold uppercase tracking-tight',\n sectionBody: 'flex flex-col gap-4',\n details: 'group border-2 border-edge-strong bg-surface-raised',\n summary:\n 'flex cursor-pointer list-none items-center gap-3 px-4 py-3 font-mono text-sm font-bold '\n + 'text-content-primary marker:content-none hover:bg-surface-sunken',\n summaryNote: 'font-normal text-content-muted',\n detailsBody: 'border-t-2 border-edge-subtle p-4',\n },\n});\n\n/** One `label`/`value` pair in the header strip — a run date, a commit, a scope. */\nexport interface ReportMetaItem {\n label: string;\n value: ReactNode;\n}\n\nexport interface ReportDocumentProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {\n /** The report's name, as an `<h1>`. What a reader sees first and links by. */\n title: ReactNode;\n /** One line under the title saying what the report answers. */\n subtitle?: ReactNode;\n /** Rendered as a definition strip under the title. Omit for none. */\n meta?: readonly ReportMetaItem[];\n /** The report body — sections, cards, tables. Spaced by the frame. */\n children: ReactNode;\n /** Merged onto the article. For the measure or the page margin. */\n className?: string;\n}\n\n/**\n * The page frame for a generated report: a titled header with a metadata\n * strip, and a body that spaces its own sections.\n *\n * It exists so that a tool writing a report has one obvious starting point\n * rather than a layout decision to make badly. The frame owns the measure, the\n * header and the vertical rhythm, and nothing else — everything inside it is\n * ordinary composition with `Card`, `StatCard`, `DataTable`, `NoteBlock` and\n * `Prose`.\n *\n * Every colour addresses a role, so the same markup renders on every rung of\n * the ladder. It also prints: the `print:` utilities drop the page margin and\n * the measure, because a report is a thing people put in front of other\n * people.\n *\n * Use `ReportSection` for its parts and `ReportDetails` for anything folded\n * away; both are documented on this page.\n */\nexport const ReportDocument = forwardRef<HTMLElement, ReportDocumentProps>(function ReportDocument(\n { title, subtitle, meta, children, className, ...props },\n ref,\n) {\n const styles = document_();\n\n return (\n <article data-slot=\"report-document\" ref={ref} className={cn(styles.root(), className)} {...props}>\n <header className={styles.header()}>\n <h1 className={styles.title()}>{title}</h1>\n {subtitle ? <p className={styles.subtitle()}>{subtitle}</p> : null}\n {meta && meta.length > 0 ? (\n <dl className={styles.meta()}>\n {meta.map((item) => (\n <div key={item.label} className={styles.metaItem()}>\n <dt className={styles.metaLabel()}>{item.label}</dt>\n <dd className={styles.metaValue()}>{item.value}</dd>\n </div>\n ))}\n </dl>\n ) : null}\n </header>\n <div className={styles.body()}>{children}</div>\n </article>\n );\n});\n\n/**\n * A titled section inside a report. Headings carry the display font from\n * `styles.css`, so this only owns the rule above and the spacing.\n */\nexport interface ReportSectionProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {\n /** The section heading, rendered as an `<h2>`. */\n title: ReactNode;\n /**\n * Anchor for the section, so a reader can link straight to it. Derived from\n * the title when omitted — a generated report is often the thing someone\n * pastes into a ticket, and \"see the Failures section\" is worse than a URL.\n */\n id?: string;\n /** What the section contains. Stacked with the frame's own rhythm. */\n children: ReactNode;\n /** Merged onto the section element. */\n className?: string;\n}\n\nexport const ReportSection = forwardRef<HTMLElement, ReportSectionProps>(function ReportSection(\n { title, id, children, className, ...props },\n ref,\n) {\n const styles = document_();\n const anchor = id ?? slugify(childrenToText(title));\n return (\n <section data-slot=\"report-section\" ref={ref} id={anchor} className={cn(styles.section(), className)} {...props}>\n <h2 className={styles.sectionTitle()}>{title}</h2>\n <div className={styles.sectionBody()}>{children}</div>\n </section>\n );\n});\n\n/**\n * A collapsible block, built on `<details>`.\n *\n * This is the one interactive affordance a static report can actually have.\n * Every other one — a sort control, a filter, a tab strip — needs client JS that\n * this pipeline does not emit, so it renders as a dead control; `<details>` is\n * disclosure implemented by the browser itself. That makes progressive\n * disclosure available to a generated report at all: a summary anyone can scan,\n * with the stack trace, the full diff or the 200-row table folded away until\n * someone wants it.\n *\n * `open` is worth setting on the block a reader came for. Printing a report\n * prints only what is open, which is usually right and occasionally surprising.\n */\nexport interface ReportDetailsProps extends HTMLAttributes<HTMLDetailsElement> {\n /** The always-visible row. Should read as a claim, not a label. */\n summary: ReactNode;\n /** Short right-aligned annotation on the summary row — a count, a duration. */\n note?: ReactNode;\n /** Start expanded. Printing a report prints only what is open. */\n open?: boolean;\n /** The folded-away detail — a trace, a diff, a long table. */\n children: ReactNode;\n /** Merged onto the `<details>` element. */\n className?: string;\n}\n\nexport const ReportDetails = forwardRef<HTMLDetailsElement, ReportDetailsProps>(\n function ReportDetails({ summary, note, open, children, className, ...props }, ref) {\n const styles = document_();\n return (\n <details data-slot=\"report-details\" ref={ref} open={open} className={cn(styles.details(), className)} {...props}>\n <summary className={styles.summary()}>\n {/* Text, not an icon: the marker has to survive greyscale printing and a\n reader who cannot see colour. */}\n <span aria-hidden=\"true\" className=\"text-accent-primary\">\n <span className=\"group-open:hidden\">[+]</span>\n <span className=\"hidden group-open:inline\">[-]</span>\n </span>\n <span className=\"flex-1\">{summary}</span>\n {note ? <span className={styles.summaryNote()}>{note}</span> : null}\n </summary>\n <div className={styles.detailsBody()}>{children}</div>\n </details>\n );\n },\n);\n\n/*\n * `forwardRef` requires these: `check:component-contract` gates displayName at\n * budget 0, and without them a ref-forwarding component shows as\n * `ForwardRef(...)` in React DevTools and in a failing test's output.\n */\nReportDocument.displayName = 'ReportDocument';\nReportSection.displayName = 'ReportSection';\nReportDetails.displayName = 'ReportDetails';\n"],"mappings":";AAAA,SAAS,kBAAkB;AAE3B,SAAS,IAAI,cAAc;AAC3B,SAAS,gBAAgB,eAAe;AAwFhC,cAKM,YALN;AAzER,IAAM,YAAY,OAAO;AAAA,EACvB,OAAO;AAAA;AAAA;AAAA;AAAA,IAIL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,WAAW;AAAA,IACX,WAAW;AAAA,IACX,MAAM;AAAA,IACN,SAAS;AAAA,IACT,cACE;AAAA,IACF,aAAa;AAAA,IACb,SAAS;AAAA,IACT,SACE;AAAA,IAEF,aAAa;AAAA,IACb,aAAa;AAAA,EACf;AACF,CAAC;AAuCM,IAAM,iBAAiB,WAA6C,SAASA,gBAClF,EAAE,OAAO,UAAU,MAAM,UAAU,WAAW,GAAG,MAAM,GACvD,KACA;AACA,QAAM,SAAS,UAAU;AAEzB,SACE,qBAAC,aAAQ,aAAU,mBAAkB,KAAU,WAAW,GAAG,OAAO,KAAK,GAAG,SAAS,GAAI,GAAG,OAC1F;AAAA,yBAAC,YAAO,WAAW,OAAO,OAAO,GAC/B;AAAA,0BAAC,QAAG,WAAW,OAAO,MAAM,GAAI,iBAAM;AAAA,MACrC,WAAW,oBAAC,OAAE,WAAW,OAAO,SAAS,GAAI,oBAAS,IAAO;AAAA,MAC7D,QAAQ,KAAK,SAAS,IACrB,oBAAC,QAAG,WAAW,OAAO,KAAK,GACxB,eAAK,IAAI,CAAC,SACT,qBAAC,SAAqB,WAAW,OAAO,SAAS,GAC/C;AAAA,4BAAC,QAAG,WAAW,OAAO,UAAU,GAAI,eAAK,OAAM;AAAA,QAC/C,oBAAC,QAAG,WAAW,OAAO,UAAU,GAAI,eAAK,OAAM;AAAA,WAFvC,KAAK,KAGf,CACD,GACH,IACE;AAAA,OACN;AAAA,IACA,oBAAC,SAAI,WAAW,OAAO,KAAK,GAAI,UAAS;AAAA,KAC3C;AAEJ,CAAC;AAqBM,IAAM,gBAAgB,WAA4C,SAASC,eAChF,EAAE,OAAO,IAAI,UAAU,WAAW,GAAG,MAAM,GAC3C,KACA;AACA,QAAM,SAAS,UAAU;AACzB,QAAM,SAAS,MAAM,QAAQ,eAAe,KAAK,CAAC;AAClD,SACE,qBAAC,aAAQ,aAAU,kBAAiB,KAAU,IAAI,QAAQ,WAAW,GAAG,OAAO,QAAQ,GAAG,SAAS,GAAI,GAAG,OACxG;AAAA,wBAAC,QAAG,WAAW,OAAO,aAAa,GAAI,iBAAM;AAAA,IAC7C,oBAAC,SAAI,WAAW,OAAO,YAAY,GAAI,UAAS;AAAA,KAClD;AAEJ,CAAC;AA6BM,IAAM,gBAAgB;AAAA,EAC3B,SAASC,eAAc,EAAE,SAAS,MAAM,MAAM,UAAU,WAAW,GAAG,MAAM,GAAG,KAAK;AAClF,UAAM,SAAS,UAAU;AACzB,WACE,qBAAC,aAAQ,aAAU,kBAAiB,KAAU,MAAY,WAAW,GAAG,OAAO,QAAQ,GAAG,SAAS,GAAI,GAAG,OACxG;AAAA,2BAAC,aAAQ,WAAW,OAAO,QAAQ,GAGjC;AAAA,6BAAC,UAAK,eAAY,QAAO,WAAU,uBACjC;AAAA,8BAAC,UAAK,WAAU,qBAAoB,iBAAG;AAAA,UACvC,oBAAC,UAAK,WAAU,4BAA2B,iBAAG;AAAA,WAChD;AAAA,QACA,oBAAC,UAAK,WAAU,UAAU,mBAAQ;AAAA,QACjC,OAAO,oBAAC,UAAK,WAAW,OAAO,YAAY,GAAI,gBAAK,IAAU;AAAA,SACjE;AAAA,MACA,oBAAC,SAAI,WAAW,OAAO,YAAY,GAAI,UAAS;AAAA,OAClD;AAAA,EAEJ;AACF;AAOA,eAAe,cAAc;AAC7B,cAAc,cAAc;AAC5B,cAAc,cAAc;","names":["ReportDocument","ReportSection","ReportDetails"]}
@@ -0,0 +1,18 @@
1
+ // src/components/SectionContainer.tsx
2
+ import { cn } from "../lib/recipe.js";
3
+ import { jsx } from "react/jsx-runtime";
4
+ function SectionContainer({ children, className = "" }) {
5
+ return (
6
+ /*
7
+ * `cn()`, not a template. Appending the caller's `className` to a template
8
+ * string does nothing when the two conflict: Tailwind resolves
9
+ * `max-w-3xl` against `max-w-7xl` by CSS source order, not by the order the
10
+ * classes appear in the attribute. `cn()` merges, so the caller wins.
11
+ */
12
+ /* @__PURE__ */ jsx("section", { "data-slot": "section-container", className: cn("mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0", className), children })
13
+ );
14
+ }
15
+ export {
16
+ SectionContainer
17
+ };
18
+ //# sourceMappingURL=SectionContainer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/SectionContainer.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { cn } from '../lib/recipe';\n\nexport interface SectionContainerProps {\n children: ReactNode;\n className?: string;\n}\n\nexport function SectionContainer({ children, className = '' }: SectionContainerProps) {\n return (\n /*\n * `cn()`, not a template. Appending the caller's `className` to a template\n * string does nothing when the two conflict: Tailwind resolves\n * `max-w-3xl` against `max-w-7xl` by CSS source order, not by the order the\n * classes appear in the attribute. `cn()` merges, so the caller wins.\n */\n <section data-slot=\"section-container\" className={cn('mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0', className)}>\n {children}\n </section>\n );\n}\n"],"mappings":";AACA,SAAS,UAAU;AAef;AARG,SAAS,iBAAiB,EAAE,UAAU,YAAY,GAAG,GAA0B;AACpF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOE,oBAAC,aAAQ,aAAU,qBAAoB,WAAW,GAAG,uDAAuD,SAAS,GAClH,UACH;AAAA;AAEJ;","names":[]}
@@ -0,0 +1,199 @@
1
+ "use client";
2
+
3
+ // src/components/Select.tsx
4
+ import { forwardRef, useId } from "react";
5
+ import { ChevronDown } from "lucide-react";
6
+ import { Field as BaseField } from "@base-ui/react/field";
7
+ import { Select as BaseSelect } from "@base-ui/react/select";
8
+ import { recipe } from "../lib/recipe.js";
9
+ import { FieldFrame, accentStyle } from "./fieldFrame.js";
10
+ import { fieldControl } from "./Input.js";
11
+ import { usePortalThemeAttribute } from "./portalTheme.js";
12
+ import { jsx, jsxs } from "react/jsx-runtime";
13
+ var list = recipe({
14
+ slots: {
15
+ trigger: "flex items-center justify-between gap-3 text-left data-[disabled]:cursor-not-allowed data-[disabled]:border-edge-subtle data-[disabled]:bg-surface-sunken data-[disabled]:text-content-muted",
16
+ value: "truncate data-[placeholder]:text-content-muted",
17
+ icon: "flex shrink-0 text-content-secondary",
18
+ // No compositor layer at high pixel density — `floatingSurface.ts` has why.
19
+ positioner: "z-top will-change-auto!",
20
+ popup: "min-w-[var(--anchor-width)] border-2 border-edge-strong bg-surface-raised font-mono text-sm text-content-primary shadow-hard-md",
21
+ list: "max-h-[min(var(--available-height),20rem)] overflow-y-auto py-1",
22
+ item: "group flex cursor-pointer items-center gap-2 px-3 py-2 select-none data-[highlighted]:bg-[var(--field-accent)] data-[highlighted]:text-content-inverse data-[selected]:font-bold data-[disabled]:cursor-not-allowed data-[disabled]:text-content-muted",
23
+ mark: "w-3 shrink-0 text-[var(--field-accent)] group-data-[highlighted]:text-content-inverse",
24
+ empty: "px-3 py-2 text-content-muted"
25
+ }
26
+ });
27
+ function skipDisabledRows(event) {
28
+ const { key } = event;
29
+ if (key !== "ArrowDown" && key !== "ArrowUp" && key !== "Home" && key !== "End") return;
30
+ const rows = Array.from(event.currentTarget.querySelectorAll('[role="option"]'));
31
+ const enabled = rows.filter((row) => row.getAttribute("aria-disabled") !== "true");
32
+ if (enabled.length === 0) return;
33
+ let target;
34
+ if (key === "Home") target = enabled[0];
35
+ else if (key === "End") target = enabled[enabled.length - 1];
36
+ else {
37
+ const step = key === "ArrowDown" ? 1 : -1;
38
+ const from = rows.indexOf(event.target);
39
+ for (let i = from + step; i >= 0 && i < rows.length; i += step) {
40
+ if (enabled.includes(rows[i])) {
41
+ target = rows[i];
42
+ break;
43
+ }
44
+ }
45
+ if (!target) target = enabled.includes(rows[from]) ? rows[from] : enabled[0];
46
+ }
47
+ event.preventDefault();
48
+ event.stopPropagation();
49
+ target?.focus();
50
+ }
51
+ var Select = forwardRef(function Select2({
52
+ label,
53
+ error,
54
+ helperText,
55
+ options,
56
+ /** Semantic accent for the focus border, the highlighted row and the chosen mark. */
57
+ accent = "primary",
58
+ /** Merged onto the control itself, not the label-and-error group. */
59
+ className = "",
60
+ style,
61
+ id,
62
+ value,
63
+ defaultValue,
64
+ onValueChange,
65
+ placeholder,
66
+ name,
67
+ form,
68
+ autoComplete,
69
+ disabled,
70
+ required,
71
+ native = false,
72
+ ...props
73
+ }, ref) {
74
+ const labelId = useId();
75
+ const portalTheme = usePortalThemeAttribute();
76
+ const control = fieldControl({ invalid: Boolean(error), interactive: !disabled });
77
+ const initial = defaultValue ?? (placeholder === void 0 ? options.find((o) => !o.disabled)?.value : void 0);
78
+ if (native) {
79
+ return /* @__PURE__ */ jsx(FieldFrame, { label, error, helperText, disabled, children: /* @__PURE__ */ jsx(
80
+ BaseField.Control,
81
+ {
82
+ ref,
83
+ id,
84
+ name,
85
+ disabled,
86
+ value,
87
+ defaultValue: value === void 0 ? initial ?? "" : void 0,
88
+ onValueChange: (next) => onValueChange?.(next),
89
+ render: (controlProps) => /* @__PURE__ */ jsxs(
90
+ "select",
91
+ {
92
+ ...controlProps,
93
+ ...props,
94
+ form,
95
+ autoComplete,
96
+ required,
97
+ "data-slot": "field-control",
98
+ style: accentStyle(accent, style),
99
+ className: control.control({ class: className }),
100
+ children: [
101
+ placeholder !== void 0 && /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: placeholder }),
102
+ options.map((opt) => (
103
+ // The platform paints this list. `color-scheme`, which each
104
+ // Level declares, is as far as the palette reaches into it.
105
+ /* @__PURE__ */ jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value)
106
+ ))
107
+ ]
108
+ }
109
+ )
110
+ }
111
+ ) });
112
+ }
113
+ const styles = list();
114
+ const listName = label ? { "aria-labelledby": labelId } : { "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"] };
115
+ return /* @__PURE__ */ jsx(
116
+ FieldFrame,
117
+ {
118
+ label,
119
+ error,
120
+ helperText,
121
+ disabled,
122
+ nativeLabel: false,
123
+ labelId,
124
+ children: /* @__PURE__ */ jsxs(
125
+ BaseSelect.Root,
126
+ {
127
+ items: options,
128
+ name,
129
+ form,
130
+ autoComplete,
131
+ disabled,
132
+ required,
133
+ value: value === void 0 ? void 0 : value === "" ? null : value,
134
+ defaultValue: value === void 0 ? initial ?? null : void 0,
135
+ onValueChange: (next) => {
136
+ if (next !== null) onValueChange?.(next);
137
+ },
138
+ children: [
139
+ /* @__PURE__ */ jsxs(
140
+ BaseSelect.Trigger,
141
+ {
142
+ ref,
143
+ id,
144
+ "data-slot": "select-trigger",
145
+ style: accentStyle(accent, style),
146
+ className: control.control({ class: [styles.trigger(), className] }),
147
+ ...props,
148
+ children: [
149
+ /* @__PURE__ */ jsx(BaseSelect.Value, { "data-slot": "select-value", placeholder, className: styles.value() }),
150
+ /* @__PURE__ */ jsx(BaseSelect.Icon, { "data-slot": "select-icon", className: styles.icon(), children: /* @__PURE__ */ jsx(ChevronDown, { size: 16, strokeWidth: 2.5 }) })
151
+ ]
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsx(BaseSelect.Portal, { children: /* @__PURE__ */ jsx(
155
+ BaseSelect.Positioner,
156
+ {
157
+ ...portalTheme,
158
+ alignItemWithTrigger: false,
159
+ sideOffset: 4,
160
+ className: styles.positioner(),
161
+ children: /* @__PURE__ */ jsxs(
162
+ BaseSelect.Popup,
163
+ {
164
+ "data-slot": "select-popup",
165
+ style: accentStyle(accent),
166
+ className: styles.popup(),
167
+ onKeyDownCapture: skipDisabledRows,
168
+ children: [
169
+ /* @__PURE__ */ jsx(BaseSelect.List, { "data-slot": "select-list", className: styles.list(), ...listName, children: options.map((opt) => /* @__PURE__ */ jsxs(
170
+ BaseSelect.Item,
171
+ {
172
+ value: opt.value,
173
+ label: opt.label,
174
+ disabled: opt.disabled,
175
+ "data-slot": "select-item",
176
+ className: styles.item(),
177
+ children: [
178
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: styles.mark(), children: /* @__PURE__ */ jsx(BaseSelect.ItemIndicator, { children: ">" }) }),
179
+ /* @__PURE__ */ jsx(BaseSelect.ItemText, { "data-slot": "select-item-text", children: opt.label })
180
+ ]
181
+ },
182
+ opt.value
183
+ )) }),
184
+ options.length === 0 && /* @__PURE__ */ jsx("div", { "data-slot": "select-empty", className: styles.empty(), children: "> No options" })
185
+ ]
186
+ }
187
+ )
188
+ }
189
+ ) })
190
+ ]
191
+ }
192
+ )
193
+ }
194
+ );
195
+ });
196
+ export {
197
+ Select
198
+ };
199
+ //# sourceMappingURL=Select.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/Select.tsx"],"sourcesContent":["'use client';\n\nimport { forwardRef, useId } from 'react';\nimport type { HTMLAttributes, KeyboardEvent, Ref } from 'react';\nimport { ChevronDown } from 'lucide-react';\nimport { Field as BaseField } from '@base-ui/react/field';\nimport { Select as BaseSelect } from '@base-ui/react/select';\nimport { recipe } from '../lib/recipe';\nimport { FieldFrame, accentStyle } from './fieldFrame';\nimport { fieldControl } from './Input';\nimport { usePortalThemeAttribute } from './portalTheme';\nimport type { FieldProps } from './fieldFrame';\n\nexport interface SelectOption {\n /** What the option says, in the list and in the closed control once chosen. */\n label: string;\n /** What `value` becomes, and what a form submits, when this option is chosen. */\n value: string;\n /**\n * Shown but not choosable. The keyboard skips it, as the pointer cannot\n * select it, so a list with a disabled row still has somewhere to land.\n */\n disabled?: boolean;\n}\n\nexport interface SelectProps\n extends Omit<HTMLAttributes<HTMLElement>, 'className' | 'defaultValue' | 'onChange' | 'children'>,\n FieldProps {\n /** The choices, in the order the list shows them and the arrow keys visit them. */\n options: SelectOption[];\n /** The chosen option's `value`. Supplying it makes the field controlled; pair it with `onValueChange`. */\n value?: string;\n /**\n * The option chosen at first, for an uncontrolled field. Omitted, the first\n * enabled option is chosen — what a native `<select>` does — unless a\n * `placeholder` is set, in which case nothing is.\n */\n defaultValue?: string;\n /** Called with the newly chosen option's `value`, on either presentation. */\n onValueChange?: (value: string) => void;\n /**\n * Shown in the closed control while nothing is chosen. Its presence is also\n * what makes \"nothing chosen\" the starting state, so a required field can\n * refuse to submit until someone picks.\n */\n placeholder?: string;\n /** The name the chosen value is submitted under in a native form. */\n name?: string;\n /** The `id` of a `<form>` elsewhere in the page that the value belongs to. */\n form?: string;\n /** An autofill hint, as on a native `<select>`. */\n autoComplete?: string;\n /** Ignores user interaction, and greys the label and control with it. */\n disabled?: boolean;\n /** Requires a choice before the form submits; announced as `aria-required`. */\n required?: boolean;\n /** Placed on the control — the trigger, or the native `<select>` — never on the wrapper. */\n id?: string;\n /**\n * Render the platform's own `<select>` instead of this system's list.\n *\n * A deliberate choice rather than a fallback (#164): on a phone, a long list\n * is genuinely better as the native picker, which scrolls with momentum and\n * sits where the thumb is. The cost is the one the listbox exists to remove —\n * the open list is painted by the operating system, which `color-scheme`\n * reaches as a light/dark hint and not as a palette.\n */\n native?: boolean;\n}\n\n// The open list. The closed trigger is `fieldControl`, so it matches the text\n// controls; everything that only exists once the list is open is here. The\n// highlighted row is an accent fill with inverse text and the chosen row\n// carries an accent mark: selection devices in the sense\n// `auditSelectionDevices` means, never a surface swap, and pairs\n// `check:contrast` measures under `select.*`. (Comments inside the object\n// literal would ship: esbuild keeps them there.)\nconst list = recipe({\n slots: {\n trigger:\n 'flex items-center justify-between gap-3 text-left data-[disabled]:cursor-not-allowed '\n + 'data-[disabled]:border-edge-subtle data-[disabled]:bg-surface-sunken data-[disabled]:text-content-muted',\n value: 'truncate data-[placeholder]:text-content-muted',\n icon: 'flex shrink-0 text-content-secondary',\n // No compositor layer at high pixel density — `floatingSurface.ts` has why.\n positioner: 'z-top will-change-auto!',\n popup:\n 'min-w-[var(--anchor-width)] border-2 border-edge-strong bg-surface-raised font-mono text-sm '\n + 'text-content-primary shadow-hard-md',\n list: 'max-h-[min(var(--available-height),20rem)] overflow-y-auto py-1',\n item:\n 'group flex cursor-pointer items-center gap-2 px-3 py-2 select-none '\n + 'data-[highlighted]:bg-[var(--field-accent)] data-[highlighted]:text-content-inverse '\n + 'data-[selected]:font-bold '\n + 'data-[disabled]:cursor-not-allowed data-[disabled]:text-content-muted',\n mark: 'w-3 shrink-0 text-[var(--field-accent)] group-data-[highlighted]:text-content-inverse',\n empty: 'px-3 py-2 text-content-muted',\n },\n});\n\n// Base UI's listbox lets the arrows, Home and End land on a disabled row (it\n// passes `disabledIndices: []` on purpose, after the APG), and a row that can\n// be focused but not chosen is a dead end under Enter. Native `<select>` skips\n// them, and so does this: the keys are taken in the capture phase, before Base\n// UI sees them, and focus moves to the next enabled row — which Base UI's own\n// item `onFocus` then adopts as the highlight. With no enabled row at all the\n// keys are left to Base UI, so the list is never left with nowhere to be.\nfunction skipDisabledRows(event: KeyboardEvent<HTMLElement>) {\n const { key } = event;\n if (key !== 'ArrowDown' && key !== 'ArrowUp' && key !== 'Home' && key !== 'End') return;\n const rows = Array.from(event.currentTarget.querySelectorAll<HTMLElement>('[role=\"option\"]'));\n const enabled = rows.filter((row) => row.getAttribute('aria-disabled') !== 'true');\n if (enabled.length === 0) return;\n\n let target: HTMLElement | undefined;\n if (key === 'Home') target = enabled[0];\n else if (key === 'End') target = enabled[enabled.length - 1];\n else {\n const step = key === 'ArrowDown' ? 1 : -1;\n const from = rows.indexOf(event.target as HTMLElement);\n for (let i = from + step; i >= 0 && i < rows.length; i += step) {\n if (enabled.includes(rows[i]!)) {\n target = rows[i];\n break;\n }\n }\n // Past the last enabled row: stay put, as a native `<select>` does,\n // rather than wrapping or stepping onto a disabled one.\n if (!target) target = enabled.includes(rows[from]!) ? rows[from] : enabled[0];\n }\n\n event.preventDefault();\n event.stopPropagation();\n target?.focus();\n}\n\n/**\n * One of a fixed set, chosen from a list this system paints.\n *\n * The closed control is the same field as `Input` — label, helper text, error,\n * accent, drawn by the same recipe. The open list is a Base UI listbox rather\n * than the platform's, so it is drawn from the Level's roles like every other\n * surface: `surface.raised` behind the rows, an accent fill on the highlighted\n * one, an accent mark beside the chosen one. On a native `<select>` that list\n * belonged to the operating system, and `midnight` and `sketch` reached it only\n * as far as `color-scheme` allowed.\n *\n * The keyboard is a listbox's: Enter, Space or the arrows open it; the arrows,\n * Home and End move through it, skipping disabled rows; typing jumps to the\n * option that starts with what was typed; Enter commits the highlighted row\n * and Escape closes without changing anything.\n *\n * The value still reaches a native form: Base UI keeps a hidden input under\n * `name`, so `FormData` carries the chosen `value` as it did from the\n * `<select>`.\n *\n * Set `native` for the platform picker instead — see the prop.\n *\n * ```tsx\n * <Select\n * label=\"Deployment region\"\n * name=\"region\"\n * options={[\n * { label: 'US East (N. Virginia)', value: 'us-east-1' },\n * { label: 'EU West (Frankfurt)', value: 'eu-west-1' },\n * ]}\n * />\n * ```\n */\nexport const Select = forwardRef<HTMLElement, SelectProps>(function Select(\n {\n label,\n error,\n helperText,\n options,\n /** Semantic accent for the focus border, the highlighted row and the chosen mark. */\n accent = 'primary',\n /** Merged onto the control itself, not the label-and-error group. */\n className = '',\n style,\n id,\n value,\n defaultValue,\n onValueChange,\n placeholder,\n name,\n form,\n autoComplete,\n disabled,\n required,\n native = false,\n ...props\n },\n ref,\n) {\n const labelId = useId();\n const portalTheme = usePortalThemeAttribute();\n const control = fieldControl({ invalid: Boolean(error), interactive: !disabled });\n // What a native `<select>` shows with no value: its first enabled option. A\n // placeholder is the explicit way to start from nothing instead.\n const initial = defaultValue ?? (placeholder === undefined ? options.find((o) => !o.disabled)?.value : undefined);\n\n if (native) {\n return (\n <FieldFrame label={label} error={error} helperText={helperText} disabled={disabled}>\n <BaseField.Control\n ref={ref as Ref<HTMLInputElement>}\n id={id}\n name={name}\n disabled={disabled}\n value={value}\n defaultValue={value === undefined ? (initial ?? '') : undefined}\n onValueChange={(next) => onValueChange?.(next)}\n render={(controlProps) => (\n <select\n {...controlProps}\n {...props}\n form={form}\n autoComplete={autoComplete}\n required={required}\n data-slot=\"field-control\"\n style={accentStyle(accent, style)}\n className={control.control({ class: className })}\n >\n {placeholder !== undefined && (\n <option value=\"\" disabled>\n {placeholder}\n </option>\n )}\n {options.map((opt) => (\n // The platform paints this list. `color-scheme`, which each\n // Level declares, is as far as the palette reaches into it.\n <option key={opt.value} value={opt.value} disabled={opt.disabled}>\n {opt.label}\n </option>\n ))}\n </select>\n )}\n />\n </FieldFrame>\n );\n }\n\n const styles = list();\n // The listbox is named by the same label as the trigger, or by the trigger's\n // own `aria-label` when there is no visible one — a list that opens with no\n // name is announced as \"listbox\" and nothing else.\n const listName = label\n ? { 'aria-labelledby': labelId }\n : { 'aria-label': props['aria-label'], 'aria-labelledby': props['aria-labelledby'] };\n\n return (\n <FieldFrame\n label={label}\n error={error}\n helperText={helperText}\n disabled={disabled}\n nativeLabel={false}\n labelId={labelId}\n >\n <BaseSelect.Root<string>\n items={options}\n name={name}\n form={form}\n autoComplete={autoComplete}\n disabled={disabled}\n required={required}\n // `''` is \"nothing chosen\" on the native element, so it is here too.\n value={value === undefined ? undefined : value === '' ? null : value}\n defaultValue={value === undefined ? (initial ?? null) : undefined}\n // The new value only. Base UI's second argument is its own event\n // details type, and forwarding it would publish that type.\n onValueChange={(next) => {\n if (next !== null) onValueChange?.(next);\n }}\n >\n <BaseSelect.Trigger\n ref={ref as Ref<HTMLButtonElement>}\n id={id}\n data-slot=\"select-trigger\"\n style={accentStyle(accent, style)}\n className={control.control({ class: [styles.trigger(), className] })}\n {...props}\n >\n <BaseSelect.Value data-slot=\"select-value\" placeholder={placeholder} className={styles.value()} />\n <BaseSelect.Icon data-slot=\"select-icon\" className={styles.icon()}>\n <ChevronDown size={16} strokeWidth={2.5} />\n </BaseSelect.Icon>\n </BaseSelect.Trigger>\n <BaseSelect.Portal>\n {/* Below the trigger, never over it: Base UI's default slides the\n list up so the chosen row sits on the trigger, which moves with\n the value and hides the field it belongs to. */}\n <BaseSelect.Positioner\n {...portalTheme}\n alignItemWithTrigger={false}\n sideOffset={4}\n className={styles.positioner()}\n >\n {/* The accent again, because the list is portalled out of the\n trigger's subtree and would not inherit it. */}\n <BaseSelect.Popup\n data-slot=\"select-popup\"\n style={accentStyle(accent)}\n className={styles.popup()}\n onKeyDownCapture={skipDisabledRows}\n >\n <BaseSelect.List data-slot=\"select-list\" className={styles.list()} {...listName}>\n {options.map((opt) => (\n <BaseSelect.Item\n key={opt.value}\n value={opt.value}\n label={opt.label}\n disabled={opt.disabled}\n data-slot=\"select-item\"\n className={styles.item()}\n >\n <span aria-hidden=\"true\" className={styles.mark()}>\n <BaseSelect.ItemIndicator>&gt;</BaseSelect.ItemIndicator>\n </span>\n <BaseSelect.ItemText data-slot=\"select-item-text\">{opt.label}</BaseSelect.ItemText>\n </BaseSelect.Item>\n ))}\n </BaseSelect.List>\n {options.length === 0 && (\n <div data-slot=\"select-empty\" className={styles.empty()}>\n &gt; No options\n </div>\n )}\n </BaseSelect.Popup>\n </BaseSelect.Positioner>\n </BaseSelect.Portal>\n </BaseSelect.Root>\n </FieldFrame>\n );\n});\n"],"mappings":"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","names":["Select"]}
@@ -0,0 +1,29 @@
1
+ // src/components/SiteFooter.tsx
2
+ import { forwardRef } from "react";
3
+ import { recipe } from "../lib/recipe.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ var siteFooter = recipe({
6
+ slots: {
7
+ root: "border-t-4 border-edge-strong bg-surface-raised font-mono text-content-primary print:hidden",
8
+ body: "mx-auto flex max-w-6xl flex-col gap-8 px-4 py-8 md:flex-row md:justify-between md:px-6",
9
+ lead: "max-w-sm text-sm leading-relaxed",
10
+ nav: "flex flex-wrap gap-x-12 gap-y-6",
11
+ meta: "border-t-2 border-edge-strong",
12
+ metaInner: "mx-auto max-w-6xl px-4 py-4 text-xs uppercase tracking-wide text-content-secondary md:px-6"
13
+ }
14
+ });
15
+ var SiteFooter = forwardRef(function SiteFooter2({ children, nav, meta, className, ...props }, ref) {
16
+ const styles = siteFooter();
17
+ const hasBody = Boolean(children) || Boolean(nav);
18
+ return /* @__PURE__ */ jsxs("footer", { ref, "data-slot": "site-footer", ...props, className: styles.root({ class: className }), children: [
19
+ hasBody ? /* @__PURE__ */ jsxs("div", { "data-slot": "site-footer-body", className: styles.body(), children: [
20
+ children ? /* @__PURE__ */ jsx("div", { "data-slot": "site-footer-lead", className: styles.lead(), children }) : null,
21
+ nav ? /* @__PURE__ */ jsx("div", { "data-slot": "site-footer-nav", className: styles.nav(), children: nav }) : null
22
+ ] }) : null,
23
+ meta ? /* @__PURE__ */ jsx("div", { "data-slot": "site-footer-meta", className: hasBody ? styles.meta() : void 0, children: /* @__PURE__ */ jsx("div", { className: styles.metaInner(), children: meta }) }) : null
24
+ ] });
25
+ });
26
+ export {
27
+ SiteFooter
28
+ };
29
+ //# sourceMappingURL=SiteFooter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/SiteFooter.tsx"],"sourcesContent":["import { forwardRef } from 'react';\nimport type { HTMLAttributes, ReactNode } from 'react';\nimport { recipe } from '../lib/recipe';\n\nexport interface SiteFooterProps\n extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'className'> {\n /**\n * The lead block — the site's name, a line about it, a newsletter prompt.\n * Takes the first column at desktop widths and the top of the stack below.\n */\n children?: ReactNode;\n /**\n * Link columns — normally one or more vertical `SiteNav`s, each with its own\n * `label`. A footer nav is a second navigation landmark on the page, so it\n * must be named apart from the header's: `Footer`, `Product`, `Company`.\n */\n nav?: ReactNode;\n /**\n * The bottom strip — copyright, licence, colophon, a build stamp. Set in the\n * secondary ink, below a rule.\n */\n meta?: ReactNode;\n /** Merged into the `<footer>`'s classes. */\n className?: string;\n}\n\nconst siteFooter = recipe({\n slots: {\n root: 'border-t-4 border-edge-strong bg-surface-raised font-mono text-content-primary print:hidden',\n body: 'mx-auto flex max-w-6xl flex-col gap-8 px-4 py-8 md:flex-row md:justify-between md:px-6',\n lead: 'max-w-sm text-sm leading-relaxed',\n nav: 'flex flex-wrap gap-x-12 gap-y-6',\n meta: 'border-t-2 border-edge-strong',\n metaInner: 'mx-auto max-w-6xl px-4 py-4 text-xs uppercase tracking-wide text-content-secondary md:px-6',\n },\n});\n\n/**\n * The site's content-info landmark: a lead block, link columns and a bottom\n * strip.\n *\n * Like {@link SiteHeader} it is a layout and holds no content of its own —\n * no copyright line, no links, no name. Every slot is optional, so a\n * portfolio's one-line footer and a marketing site's four columns are the same\n * component with more or fewer slots filled.\n *\n * Rendered as `<footer>`, which is the `contentinfo` landmark when it is not\n * inside an `<article>`, `<aside>`, `<main>`, `<nav>` or `<section>` — so put\n * it beside `<main>`, not in it.\n *\n * @example\n * ```tsx\n * <SiteFooter\n * nav={\n * <SiteNav label=\"Footer\" orientation=\"vertical\">\n * <SiteNavItem href=\"/colophon\">Colophon</SiteNavItem>\n * </SiteNav>\n * }\n * meta=\"© 2026 Site name\"\n * >\n * A line about the site.\n * </SiteFooter>\n * ```\n */\nexport const SiteFooter = forwardRef<HTMLElement, SiteFooterProps>(function SiteFooter(\n { children, nav, meta, className, ...props },\n ref,\n) {\n const styles = siteFooter();\n const hasBody = Boolean(children) || Boolean(nav);\n\n return (\n <footer ref={ref} data-slot=\"site-footer\" {...props} className={styles.root({ class: className })}>\n {hasBody ? (\n <div data-slot=\"site-footer-body\" className={styles.body()}>\n {children ? (\n <div data-slot=\"site-footer-lead\" className={styles.lead()}>\n {children}\n </div>\n ) : null}\n {nav ? (\n <div data-slot=\"site-footer-nav\" className={styles.nav()}>\n {nav}\n </div>\n ) : null}\n </div>\n ) : null}\n {meta ? (\n <div data-slot=\"site-footer-meta\" className={hasBody ? styles.meta() : undefined}>\n <div className={styles.metaInner()}>{meta}</div>\n </div>\n ) : null}\n </footer>\n );\n});\n"],"mappings":";AAAA,SAAS,kBAAkB;AAE3B,SAAS,cAAc;AAwEf,SAEI,KAFJ;AAhDR,IAAM,aAAa,OAAO;AAAA,EACxB,OAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,KAAK;AAAA,IACL,MAAM;AAAA,IACN,WAAW;AAAA,EACb;AACF,CAAC;AA6BM,IAAM,aAAa,WAAyC,SAASA,YAC1E,EAAE,UAAU,KAAK,MAAM,WAAW,GAAG,MAAM,GAC3C,KACA;AACA,QAAM,SAAS,WAAW;AAC1B,QAAM,UAAU,QAAQ,QAAQ,KAAK,QAAQ,GAAG;AAEhD,SACE,qBAAC,YAAO,KAAU,aAAU,eAAe,GAAG,OAAO,WAAW,OAAO,KAAK,EAAE,OAAO,UAAU,CAAC,GAC7F;AAAA,cACC,qBAAC,SAAI,aAAU,oBAAmB,WAAW,OAAO,KAAK,GACtD;AAAA,iBACC,oBAAC,SAAI,aAAU,oBAAmB,WAAW,OAAO,KAAK,GACtD,UACH,IACE;AAAA,MACH,MACC,oBAAC,SAAI,aAAU,mBAAkB,WAAW,OAAO,IAAI,GACpD,eACH,IACE;AAAA,OACN,IACE;AAAA,IACH,OACC,oBAAC,SAAI,aAAU,oBAAmB,WAAW,UAAU,OAAO,KAAK,IAAI,QACrE,8BAAC,SAAI,WAAW,OAAO,UAAU,GAAI,gBAAK,GAC5C,IACE;AAAA,KACN;AAEJ,CAAC;","names":["SiteFooter"]}
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ // src/components/SiteHeader.tsx
4
+ import { forwardRef } from "react";
5
+ import { recipe } from "../lib/recipe.js";
6
+ import { SiteLink } from "./LinkProvider.js";
7
+ import { SiteHeaderCollapseContext } from "./siteNavContext.js";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ var siteHeader = recipe({
10
+ slots: {
11
+ root: "relative border-b-4 border-edge-strong bg-surface-raised font-mono text-content-primary print:hidden",
12
+ skip: "sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-top focus:border-2 focus:border-edge-strong focus:bg-surface-base focus:px-4 focus:py-2 focus:font-bold focus:uppercase focus:text-content-primary",
13
+ bar: "mx-auto flex max-w-6xl items-center gap-4 px-4 py-3 md:px-6",
14
+ brand: "mr-auto inline-flex items-center gap-2 font-display text-lg font-bold uppercase tracking-wider text-content-primary no-underline hover:text-accent-primary",
15
+ nav: "hidden",
16
+ mobile: "",
17
+ actions: "flex items-center gap-2"
18
+ },
19
+ variants: {
20
+ collapseAt: {
21
+ sm: { nav: "sm:block", mobile: "sm:hidden" },
22
+ md: { nav: "md:block", mobile: "md:hidden" },
23
+ lg: { nav: "lg:block", mobile: "lg:hidden" }
24
+ },
25
+ sticky: {
26
+ true: { root: "sticky top-0 z-top" }
27
+ }
28
+ },
29
+ defaultVariants: { collapseAt: "md" }
30
+ });
31
+ var SiteHeader = forwardRef(function SiteHeader2({
32
+ brand,
33
+ brandHref = "/",
34
+ nav,
35
+ mobileNav,
36
+ actions,
37
+ collapseAt = "md",
38
+ skipTo = "main-content",
39
+ skipLabel = "Skip to content",
40
+ sticky = false,
41
+ className,
42
+ ...props
43
+ }, ref) {
44
+ const styles = siteHeader({ collapseAt, sticky });
45
+ const skipId = skipTo === false ? null : skipTo.replace(/^#/, "");
46
+ return /* @__PURE__ */ jsxs("header", { ref, "data-slot": "site-header", ...props, className: styles.root({ class: className }), children: [
47
+ skipId ? /* @__PURE__ */ jsx("a", { href: `#${skipId}`, "data-slot": "site-header-skip", className: styles.skip(), children: skipLabel }) : null,
48
+ /* @__PURE__ */ jsxs("div", { "data-slot": "site-header-bar", className: styles.bar(), children: [
49
+ /* @__PURE__ */ jsx(SiteLink, { href: brandHref, current: false, "data-slot": "site-header-brand", className: styles.brand(), children: brand }),
50
+ nav ? /* @__PURE__ */ jsx("div", { "data-slot": "site-header-nav", className: styles.nav(), children: nav }) : null,
51
+ actions ? /* @__PURE__ */ jsx("div", { "data-slot": "site-header-actions", className: styles.actions(), children: actions }) : null,
52
+ mobileNav ? /* @__PURE__ */ jsx("div", { "data-slot": "site-header-mobile-nav", className: styles.mobile(), children: /* @__PURE__ */ jsx(SiteHeaderCollapseContext.Provider, { value: collapseAt, children: mobileNav }) }) : null
53
+ ] })
54
+ ] });
55
+ });
56
+ export {
57
+ SiteHeader
58
+ };
59
+ //# sourceMappingURL=SiteHeader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/SiteHeader.tsx"],"sourcesContent":["'use client';\n\nimport { forwardRef } from 'react';\nimport type { HTMLAttributes, ReactNode } from 'react';\nimport { recipe } from '../lib/recipe';\nimport { SiteLink } from './LinkProvider';\nimport { SiteHeaderCollapseContext } from './siteNavContext';\n\n/**\n * The breakpoint at which a `SiteHeader` swaps its `mobileNav` for its `nav`.\n * Tailwind's own names, and only the three a header plausibly collapses at.\n */\nexport type SiteHeaderCollapse = 'sm' | 'md' | 'lg';\n\nexport interface SiteHeaderProps\n extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'className'> {\n /**\n * The site's name or mark, rendered as the link home. Text, a logo, or both —\n * a logo image needs its own `alt`, since it becomes the link's name.\n */\n brand: ReactNode;\n /** Where the brand links to. The site root by default. */\n brandHref?: string;\n /**\n * The desktop navigation — normally a `SiteNav`. Shown at `collapseAt` and\n * wider; not rendered into the accessibility tree below it.\n */\n nav?: ReactNode;\n /**\n * The narrow-viewport navigation — normally a `MobileNav` with the same items\n * as `nav`. Shown below `collapseAt` only.\n */\n mobileNav?: ReactNode;\n /**\n * Trailing controls shown at every width — a CTA `Button`, a theme control, a\n * repository link. Keep it short: on a phone it shares the bar with the brand\n * and the menu trigger.\n */\n actions?: ReactNode;\n /**\n * Where `nav` gives way to `mobileNav`. `md` (768px) by default; a site with\n * many top-level items wants `lg`, one with three short ones may keep its row\n * down to `sm`.\n */\n collapseAt?: SiteHeaderCollapse;\n /**\n * The `id` of the page's main content, which the skip link jumps to. A\n * leading `#` is accepted and ignored. Pass `false` to omit the skip link —\n * only when the page already renders one of its own.\n */\n skipTo?: string | false;\n /** The skip link's text. `Skip to content` by default. */\n skipLabel?: string;\n /** Stick the bar to the top of the viewport as the page scrolls. Off by default. */\n sticky?: boolean;\n /** Merged into the `<header>`'s classes. */\n className?: string;\n}\n\n// The class strings are written out in full, per breakpoint, because\n// Tailwind's scanner reads source text: a template literal such as\n// `${at}:flex` would generate no CSS at all.\nconst siteHeader = recipe({\n slots: {\n root: 'relative border-b-4 border-edge-strong bg-surface-raised font-mono text-content-primary print:hidden',\n skip: 'sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-top '\n + 'focus:border-2 focus:border-edge-strong focus:bg-surface-base focus:px-4 focus:py-2 '\n + 'focus:font-bold focus:uppercase focus:text-content-primary',\n bar: 'mx-auto flex max-w-6xl items-center gap-4 px-4 py-3 md:px-6',\n brand: 'mr-auto inline-flex items-center gap-2 font-display text-lg font-bold uppercase '\n + 'tracking-wider text-content-primary no-underline hover:text-accent-primary',\n nav: 'hidden',\n mobile: '',\n actions: 'flex items-center gap-2',\n },\n variants: {\n collapseAt: {\n sm: { nav: 'sm:block', mobile: 'sm:hidden' },\n md: { nav: 'md:block', mobile: 'md:hidden' },\n lg: { nav: 'lg:block', mobile: 'lg:hidden' },\n },\n sticky: {\n true: { root: 'sticky top-0 z-top' },\n },\n },\n defaultVariants: { collapseAt: 'md' },\n});\n\n/**\n * The site's banner: skip link, brand, navigation and trailing actions, with\n * the navigation swapping to a {@link MobileNav} below a breakpoint.\n *\n * It is a layout and a landmark, not a menu. The `<header>` is the banner; the\n * navigation is whatever `SiteNav` and `MobileNav` the caller passes, so the\n * header holds no items, no labels and no current-page logic of its own. The\n * same four parts — this, `SiteNav`, `MobileNav`, `SiteFooter` — build a\n * portfolio, a blog, a marketing site and a project site without a prop\n * between them changing; the stories are those four.\n *\n * ## The breakpoint is the point\n *\n * Below `collapseAt` the `nav` slot is `display: none` and the `mobileNav`\n * slot is shown; at `collapseAt` and wider, the reverse. `display: none` rather\n * than `aria-hidden`, so the hidden one leaves the accessibility tree and the\n * tab order together — a screen reader never meets two copies of one nav.\n *\n * ## The skip link\n *\n * The first focusable element on the page, visually hidden until it takes\n * focus. It jumps to `#main-content` unless `skipTo` names another `id`; the\n * page's `<main>` must carry that `id`, which is the one thing this component\n * asks of the page around it.\n *\n * @example\n * ```tsx\n * const items = (\n * <>\n * <SiteNavItem href=\"/writing\">Writing</SiteNavItem>\n * <SiteNavItem href=\"/projects\">Projects</SiteNavItem>\n * </>\n * );\n *\n * <SiteHeader\n * brand=\"Site name\"\n * nav={<SiteNav label=\"Primary\">{items}</SiteNav>}\n * mobileNav={<MobileNav label=\"Primary\">{items}</MobileNav>}\n * />\n * <main id=\"main-content\">…</main>\n * ```\n */\nexport const SiteHeader = forwardRef<HTMLElement, SiteHeaderProps>(function SiteHeader(\n {\n brand,\n brandHref = '/',\n nav,\n mobileNav,\n actions,\n collapseAt = 'md',\n skipTo = 'main-content',\n skipLabel = 'Skip to content',\n sticky = false,\n className,\n ...props\n },\n ref,\n) {\n const styles = siteHeader({ collapseAt, sticky });\n const skipId = skipTo === false ? null : skipTo.replace(/^#/, '');\n\n return (\n <header ref={ref} data-slot=\"site-header\" {...props} className={styles.root({ class: className })}>\n {skipId ? (\n <a href={`#${skipId}`} data-slot=\"site-header-skip\" className={styles.skip()}>\n {skipLabel}\n </a>\n ) : null}\n <div data-slot=\"site-header-bar\" className={styles.bar()}>\n <SiteLink href={brandHref} current={false} data-slot=\"site-header-brand\" className={styles.brand()}>\n {brand}\n </SiteLink>\n {nav ? (\n <div data-slot=\"site-header-nav\" className={styles.nav()}>\n {nav}\n </div>\n ) : null}\n {actions ? (\n <div data-slot=\"site-header-actions\" className={styles.actions()}>\n {actions}\n </div>\n ) : null}\n {mobileNav ? (\n <div data-slot=\"site-header-mobile-nav\" className={styles.mobile()}>\n <SiteHeaderCollapseContext.Provider value={collapseAt}>\n {mobileNav}\n </SiteHeaderCollapseContext.Provider>\n </div>\n ) : null}\n </div>\n </header>\n );\n});\n"],"mappings":";;;AAEA,SAAS,kBAAkB;AAE3B,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,iCAAiC;AAkJlC,cAIF,YAJE;AA1FR,IAAM,aAAa,OAAO;AAAA,EACxB,OAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IAGN,KAAK;AAAA,IACL,OAAO;AAAA,IAEP,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,SAAS;AAAA,EACX;AAAA,EACA,UAAU;AAAA,IACR,YAAY;AAAA,MACV,IAAI,EAAE,KAAK,YAAY,QAAQ,YAAY;AAAA,MAC3C,IAAI,EAAE,KAAK,YAAY,QAAQ,YAAY;AAAA,MAC3C,IAAI,EAAE,KAAK,YAAY,QAAQ,YAAY;AAAA,IAC7C;AAAA,IACA,QAAQ;AAAA,MACN,MAAM,EAAE,MAAM,qBAAqB;AAAA,IACrC;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,YAAY,KAAK;AACtC,CAAC;AA4CM,IAAM,aAAa,WAAyC,SAASA,YAC1E;AAAA,EACE;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,SAAS;AAAA,EACT;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,SAAS,WAAW,EAAE,YAAY,OAAO,CAAC;AAChD,QAAM,SAAS,WAAW,QAAQ,OAAO,OAAO,QAAQ,MAAM,EAAE;AAEhE,SACE,qBAAC,YAAO,KAAU,aAAU,eAAe,GAAG,OAAO,WAAW,OAAO,KAAK,EAAE,OAAO,UAAU,CAAC,GAC7F;AAAA,aACC,oBAAC,OAAE,MAAM,IAAI,MAAM,IAAI,aAAU,oBAAmB,WAAW,OAAO,KAAK,GACxE,qBACH,IACE;AAAA,IACJ,qBAAC,SAAI,aAAU,mBAAkB,WAAW,OAAO,IAAI,GACrD;AAAA,0BAAC,YAAS,MAAM,WAAW,SAAS,OAAO,aAAU,qBAAoB,WAAW,OAAO,MAAM,GAC9F,iBACH;AAAA,MACC,MACC,oBAAC,SAAI,aAAU,mBAAkB,WAAW,OAAO,IAAI,GACpD,eACH,IACE;AAAA,MACH,UACC,oBAAC,SAAI,aAAU,uBAAsB,WAAW,OAAO,QAAQ,GAC5D,mBACH,IACE;AAAA,MACH,YACC,oBAAC,SAAI,aAAU,0BAAyB,WAAW,OAAO,OAAO,GAC/D,8BAAC,0BAA0B,UAA1B,EAAmC,OAAO,YACxC,qBACH,GACF,IACE;AAAA,OACN;AAAA,KACF;AAEJ,CAAC;","names":["SiteHeader"]}
@@ -0,0 +1,141 @@
1
+ "use client";
2
+
3
+ // src/components/SiteNav.tsx
4
+ import { forwardRef, useMemo } from "react";
5
+ import { Menu as BaseMenu } from "@base-ui/react/menu";
6
+ import { ChevronDown } from "lucide-react";
7
+ import { cn, recipe } from "../lib/recipe.js";
8
+ import { SiteLink } from "./LinkProvider.js";
9
+ import { Menu } from "./Menu.js";
10
+ import { floatingParts } from "./floatingSurface.js";
11
+ import { SiteNavContext, useSiteNavContext } from "./siteNavContext.js";
12
+ import { jsx, jsxs } from "react/jsx-runtime";
13
+ var siteNav = recipe({
14
+ slots: {
15
+ root: "font-mono",
16
+ list: "m-0 flex list-none p-0",
17
+ link: "border-transparent font-bold uppercase text-content-secondary no-underline hover:text-accent-primary aria-[current=page]:border-accent-primary aria-[current=page]:text-accent-primary",
18
+ trigger: "inline-flex cursor-pointer items-center gap-1 border-transparent bg-transparent font-mono font-bold uppercase text-content-secondary hover:text-accent-primary data-[popup-open]:text-accent-primary"
19
+ },
20
+ variants: {
21
+ orientation: {
22
+ horizontal: {
23
+ list: "flex-wrap items-center gap-x-6 gap-y-2",
24
+ link: "inline-block border-b-2 py-1 text-sm tracking-wide",
25
+ trigger: "border-b-2 py-1 text-sm tracking-wide"
26
+ },
27
+ vertical: {
28
+ list: "flex-col gap-1",
29
+ link: "block min-h-11 border-l-4 px-3 py-3 text-sm aria-[current=page]:bg-surface-base",
30
+ trigger: "min-h-11 w-full border-l-4 px-3 py-3 text-left text-sm"
31
+ }
32
+ }
33
+ },
34
+ defaultVariants: { orientation: "horizontal" }
35
+ });
36
+ var SiteNav = forwardRef(function SiteNav2({ label, children, orientation = "horizontal", className, ...props }, ref) {
37
+ const parent = useSiteNavContext();
38
+ const styles = siteNav({ orientation });
39
+ const onNavigate = parent.onNavigate;
40
+ const context = useMemo(
41
+ () => ({ orientation, inMenu: false, onNavigate }),
42
+ [orientation, onNavigate]
43
+ );
44
+ return /* @__PURE__ */ jsx(
45
+ "nav",
46
+ {
47
+ ref,
48
+ "data-slot": "site-nav",
49
+ "data-orientation": orientation,
50
+ ...props,
51
+ "aria-label": label,
52
+ className: styles.root({ class: className }),
53
+ children: /* @__PURE__ */ jsx("ul", { "data-slot": "site-nav-list", className: styles.list(), children: /* @__PURE__ */ jsx(SiteNavContext.Provider, { value: context, children }) })
54
+ }
55
+ );
56
+ });
57
+ var SiteNavItem = forwardRef(
58
+ function SiteNavItem2(props, ref) {
59
+ const context = useSiteNavContext();
60
+ const styles = siteNav({ orientation: context.orientation });
61
+ if (typeof props.href !== "string") {
62
+ const { label, children: children2, defaultOpen, className: className2 } = props;
63
+ return /* @__PURE__ */ jsx("li", { "data-slot": "site-nav-item", children: /* @__PURE__ */ jsx(
64
+ Menu,
65
+ {
66
+ defaultOpen,
67
+ trigger: /* @__PURE__ */ jsxs(
68
+ "button",
69
+ {
70
+ ref,
71
+ type: "button",
72
+ "data-slot": "site-nav-group",
73
+ className: styles.trigger({ class: className2 }),
74
+ children: [
75
+ label,
76
+ /* @__PURE__ */ jsx(ChevronDown, { size: 14, strokeWidth: 3, "aria-hidden": "true" })
77
+ ]
78
+ }
79
+ ),
80
+ children: /* @__PURE__ */ jsx(SiteNavContext.Provider, { value: { ...context, inMenu: true }, children: children2 })
81
+ }
82
+ ) });
83
+ }
84
+ const { href, children, current, className, onClick, ...rest } = props;
85
+ const handleClick = (event) => {
86
+ onClick?.(event);
87
+ if (!event.defaultPrevented) context.onNavigate?.();
88
+ };
89
+ if (context.inMenu) {
90
+ return /* @__PURE__ */ jsx(
91
+ MenuLink,
92
+ {
93
+ ref,
94
+ ...rest,
95
+ href,
96
+ current,
97
+ className,
98
+ onClick: handleClick,
99
+ children
100
+ }
101
+ );
102
+ }
103
+ return /* @__PURE__ */ jsx("li", { "data-slot": "site-nav-item", children: /* @__PURE__ */ jsx(
104
+ SiteLink,
105
+ {
106
+ ref,
107
+ ...rest,
108
+ href,
109
+ current,
110
+ onClick: handleClick,
111
+ "data-slot": "site-nav-link",
112
+ className: styles.link({ class: className }),
113
+ children
114
+ }
115
+ ) });
116
+ }
117
+ );
118
+ var MenuLink = forwardRef(function MenuLink2({ href, current, className, children, ...rest }, ref) {
119
+ const parts = floatingParts();
120
+ return /* @__PURE__ */ jsx(
121
+ BaseMenu.LinkItem,
122
+ {
123
+ ref,
124
+ closeOnClick: true,
125
+ "data-slot": "site-nav-menu-link",
126
+ className: cn(
127
+ parts.item(),
128
+ "no-underline aria-[current=page]:font-bold aria-[current=page]:underline",
129
+ className
130
+ ),
131
+ render: (renderProps) => /* @__PURE__ */ jsx(SiteLink, { ...renderProps, href, current }),
132
+ ...rest,
133
+ children
134
+ }
135
+ );
136
+ });
137
+ export {
138
+ SiteNav,
139
+ SiteNavItem
140
+ };
141
+ //# sourceMappingURL=SiteNav.js.map