@repraze/kernelkit 0.1.0-alpha.2 → 0.1.0-alpha.4

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 (186) hide show
  1. package/README.md +17 -0
  2. package/dist/components/alert.d.ts.map +1 -1
  3. package/dist/components/alert.js +4 -6
  4. package/dist/components/alert.js.map +1 -1
  5. package/dist/components/avatar.js +1 -1
  6. package/dist/components/avatar.js.map +1 -1
  7. package/dist/components/badge.js +1 -1
  8. package/dist/components/badge.js.map +1 -1
  9. package/dist/components/breadcrumb.d.ts +1 -0
  10. package/dist/components/breadcrumb.d.ts.map +1 -1
  11. package/dist/components/breadcrumb.js +7 -6
  12. package/dist/components/breadcrumb.js.map +1 -1
  13. package/dist/components/callout.d.ts +8 -4
  14. package/dist/components/callout.d.ts.map +1 -1
  15. package/dist/components/callout.js +22 -12
  16. package/dist/components/callout.js.map +1 -1
  17. package/dist/components/chart.js +2 -2
  18. package/dist/components/chart.js.map +1 -1
  19. package/dist/components/code-block.js +1 -1
  20. package/dist/components/code-block.js.map +1 -1
  21. package/dist/components/command.js +1 -1
  22. package/dist/components/command.js.map +1 -1
  23. package/dist/components/data-table/elements/data-table-info-row.js +1 -1
  24. package/dist/components/data-table/elements/data-table-info-row.js.map +1 -1
  25. package/dist/components/dropdown-menu.d.ts +1 -1
  26. package/dist/components/dropdown-menu.d.ts.map +1 -1
  27. package/dist/components/dropdown-menu.js +1 -1
  28. package/dist/components/dropdown-menu.js.map +1 -1
  29. package/dist/components/kbd.d.ts.map +1 -1
  30. package/dist/components/kbd.js +2 -4
  31. package/dist/components/kbd.js.map +1 -1
  32. package/dist/components/message.js +2 -2
  33. package/dist/components/message.js.map +1 -1
  34. package/dist/components/meter.js +1 -1
  35. package/dist/components/meter.js.map +1 -1
  36. package/dist/components/progress.js +1 -1
  37. package/dist/components/progress.js.map +1 -1
  38. package/dist/components/questionnaire.js +1 -1
  39. package/dist/components/questionnaire.js.map +1 -1
  40. package/dist/components/search-dialog.d.ts +6 -1
  41. package/dist/components/search-dialog.d.ts.map +1 -1
  42. package/dist/components/search-dialog.js +13 -1
  43. package/dist/components/search-dialog.js.map +1 -1
  44. package/dist/components/search-trigger.d.ts +1 -1
  45. package/dist/components/search-trigger.d.ts.map +1 -1
  46. package/dist/components/search-trigger.js +2 -2
  47. package/dist/components/search-trigger.js.map +1 -1
  48. package/dist/components/section.js +1 -1
  49. package/dist/components/section.js.map +1 -1
  50. package/dist/components/sidebar.js +1 -1
  51. package/dist/components/sidebar.js.map +1 -1
  52. package/dist/components/site-footer.js +1 -1
  53. package/dist/components/site-footer.js.map +1 -1
  54. package/dist/components/site-header.js +2 -2
  55. package/dist/components/site-header.js.map +1 -1
  56. package/dist/components/stat.js +2 -2
  57. package/dist/components/stat.js.map +1 -1
  58. package/dist/components/tabs.d.ts +4 -2
  59. package/dist/components/tabs.d.ts.map +1 -1
  60. package/dist/components/tabs.js +13 -7
  61. package/dist/components/tabs.js.map +1 -1
  62. package/dist/components/theme-menu.js +1 -1
  63. package/dist/components/theme-menu.js.map +1 -1
  64. package/dist/components/toc.js +1 -1
  65. package/dist/components/toc.js.map +1 -1
  66. package/dist/components/tooltip.d.ts.map +1 -1
  67. package/dist/components/tooltip.js +5 -2
  68. package/dist/components/tooltip.js.map +1 -1
  69. package/dist/components/trace.js +1 -1
  70. package/dist/components/trace.js.map +1 -1
  71. package/dist/docs/doc-kit.d.ts +72 -0
  72. package/dist/docs/doc-kit.d.ts.map +1 -0
  73. package/dist/docs/doc-kit.js +45 -0
  74. package/dist/docs/doc-kit.js.map +1 -0
  75. package/dist/docs/doc-page.d.ts +27 -0
  76. package/dist/docs/doc-page.d.ts.map +1 -0
  77. package/dist/docs/doc-page.js +33 -0
  78. package/dist/docs/doc-page.js.map +1 -0
  79. package/dist/docs/docs-index.d.ts +60 -0
  80. package/dist/docs/docs-index.d.ts.map +1 -0
  81. package/dist/docs/docs-index.js +45 -0
  82. package/dist/docs/docs-index.js.map +1 -0
  83. package/dist/docs/docs-nav.d.ts +13 -0
  84. package/dist/docs/docs-nav.d.ts.map +1 -0
  85. package/dist/docs/docs-nav.js +8 -0
  86. package/dist/docs/docs-nav.js.map +1 -0
  87. package/dist/docs/docs-search.d.ts +19 -0
  88. package/dist/docs/docs-search.d.ts.map +1 -0
  89. package/dist/docs/docs-search.js +33 -0
  90. package/dist/docs/docs-search.js.map +1 -0
  91. package/dist/docs/index.d.ts +7 -0
  92. package/dist/docs/index.d.ts.map +1 -0
  93. package/dist/docs/index.js +7 -0
  94. package/dist/docs/index.js.map +1 -0
  95. package/dist/docs/review-matrix.d.ts +25 -0
  96. package/dist/docs/review-matrix.d.ts.map +1 -0
  97. package/dist/docs/review-matrix.js +31 -0
  98. package/dist/docs/review-matrix.js.map +1 -0
  99. package/dist/hooks/use-theme-palette.d.ts +8 -0
  100. package/dist/hooks/use-theme-palette.d.ts.map +1 -0
  101. package/dist/hooks/use-theme-palette.js +24 -0
  102. package/dist/hooks/use-theme-palette.js.map +1 -0
  103. package/dist/lib/recipes.d.ts +10 -0
  104. package/dist/lib/recipes.d.ts.map +1 -1
  105. package/dist/lib/recipes.js +12 -0
  106. package/dist/lib/recipes.js.map +1 -1
  107. package/dist/provider/theme-provider.d.ts +10 -1
  108. package/dist/provider/theme-provider.d.ts.map +1 -1
  109. package/dist/provider/theme-provider.js +23 -8
  110. package/dist/provider/theme-provider.js.map +1 -1
  111. package/dist/site/brand.d.ts +20 -0
  112. package/dist/site/brand.d.ts.map +1 -0
  113. package/dist/site/brand.js +17 -0
  114. package/dist/site/brand.js.map +1 -0
  115. package/dist/site/github-icon.d.ts +4 -0
  116. package/dist/site/github-icon.d.ts.map +1 -0
  117. package/dist/site/github-icon.js +6 -0
  118. package/dist/site/github-icon.js.map +1 -0
  119. package/dist/site/index.d.ts +6 -0
  120. package/dist/site/index.d.ts.map +1 -0
  121. package/dist/site/index.js +6 -0
  122. package/dist/site/index.js.map +1 -0
  123. package/dist/site/suite-footer.d.ts +24 -0
  124. package/dist/site/suite-footer.d.ts.map +1 -0
  125. package/dist/site/suite-footer.js +19 -0
  126. package/dist/site/suite-footer.js.map +1 -0
  127. package/dist/site/suite-header.d.ts +40 -0
  128. package/dist/site/suite-header.d.ts.map +1 -0
  129. package/dist/site/suite-header.js +19 -0
  130. package/dist/site/suite-header.js.map +1 -0
  131. package/dist/site/use-page-title.d.ts +3 -0
  132. package/dist/site/use-page-title.d.ts.map +1 -0
  133. package/dist/site/use-page-title.js +8 -0
  134. package/dist/site/use-page-title.js.map +1 -0
  135. package/dist/vite/index.d.ts +65 -0
  136. package/dist/vite/index.d.ts.map +1 -0
  137. package/dist/vite/index.js +242 -0
  138. package/dist/vite/index.js.map +1 -0
  139. package/package.json +39 -3
  140. package/src/components/alert.tsx +4 -6
  141. package/src/components/avatar.tsx +1 -1
  142. package/src/components/badge.tsx +1 -1
  143. package/src/components/breadcrumb.tsx +8 -7
  144. package/src/components/callout.tsx +27 -12
  145. package/src/components/chart.tsx +2 -2
  146. package/src/components/code-block.tsx +1 -1
  147. package/src/components/command.tsx +1 -1
  148. package/src/components/data-table/elements/data-table-info-row.tsx +1 -1
  149. package/src/components/dropdown-menu.tsx +1 -1
  150. package/src/components/kbd.tsx +2 -4
  151. package/src/components/message.tsx +2 -2
  152. package/src/components/meter.tsx +1 -1
  153. package/src/components/progress.tsx +1 -1
  154. package/src/components/questionnaire.tsx +1 -1
  155. package/src/components/search-dialog.tsx +14 -1
  156. package/src/components/search-trigger.tsx +2 -3
  157. package/src/components/section.tsx +1 -1
  158. package/src/components/sidebar.tsx +1 -1
  159. package/src/components/site-footer.tsx +1 -1
  160. package/src/components/site-header.tsx +2 -2
  161. package/src/components/stat.tsx +2 -2
  162. package/src/components/tabs.tsx +17 -9
  163. package/src/components/theme-menu.tsx +1 -1
  164. package/src/components/toc.tsx +1 -1
  165. package/src/components/tooltip.tsx +15 -3
  166. package/src/components/trace.tsx +1 -1
  167. package/src/docs/doc-kit.tsx +176 -0
  168. package/src/docs/doc-page.tsx +126 -0
  169. package/src/docs/docs-index.ts +108 -0
  170. package/src/docs/docs-nav.tsx +47 -0
  171. package/src/docs/docs-search.tsx +88 -0
  172. package/src/docs/index.ts +6 -0
  173. package/src/docs/review-matrix.tsx +67 -0
  174. package/src/hooks/use-theme-palette.ts +25 -0
  175. package/src/lib/recipes.ts +14 -0
  176. package/src/provider/theme-provider.tsx +34 -9
  177. package/src/site/brand.tsx +41 -0
  178. package/src/site/github-icon.tsx +10 -0
  179. package/src/site/index.ts +5 -0
  180. package/src/site/suite-footer.tsx +68 -0
  181. package/src/site/suite-header.tsx +114 -0
  182. package/src/site/use-page-title.ts +8 -0
  183. package/src/styles/kernelkit.css +6 -0
  184. package/src/styles/site.css +15 -0
  185. package/src/vite/client.d.ts +11 -0
  186. package/src/vite/index.ts +298 -0
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import {Button} from "@repraze/kernelkit/components/button";
4
+ import {Separator} from "@repraze/kernelkit/components/separator";
5
+ import {
6
+ SiteHeader,
7
+ SiteHeaderActions,
8
+ SiteHeaderBrand,
9
+ SiteHeaderContent,
10
+ SiteHeaderMenu,
11
+ SiteNav,
12
+ SiteNavLink,
13
+ } from "@repraze/kernelkit/components/site-header";
14
+ import {ThemeMenu} from "@repraze/kernelkit/components/theme-menu";
15
+ import type * as React from "react";
16
+ import {useEffect, useState} from "react";
17
+
18
+ import {GithubIcon} from "./github-icon";
19
+
20
+ export interface SuiteNavItem {
21
+ label: string;
22
+ /** A router link as a `render` element (`<Link to="/ports" />`); or `href` for a plain anchor. */
23
+ render?: React.ReactElement;
24
+ href?: string;
25
+ /** Marks the entry current (`aria-current="page"`): the section the open page belongs to. */
26
+ current?: boolean;
27
+ }
28
+
29
+ export interface SuiteHeaderProps {
30
+ /** The brand (a BrandLogo), in the header and as the phone menu's title. */
31
+ brand: React.ReactNode;
32
+ /** Accessible name of the home link, e.g. "KernelKit home". */
33
+ brandLabel: string;
34
+ /** The home link as a `render` element (`<Link to="/" />`); a plain "/" anchor otherwise. */
35
+ home?: React.ReactElement;
36
+ nav: SuiteNavItem[];
37
+ /** Accessible name of the nav (default "Main"). */
38
+ navLabel?: string;
39
+ /** The search (DocsSearch, or the site's own), before the separator. */
40
+ search?: React.ReactNode;
41
+ /** Replaces the default `<ThemeMenu />`. */
42
+ themeMenu?: React.ReactNode;
43
+ /** The repository button (a GitHub icon). */
44
+ repository?: {href: string; label?: string; icon?: React.ReactNode};
45
+ /** The phone menu's content after the nav links (the docs nav, a tree). */
46
+ menu?: React.ReactNode;
47
+ /** Closes the phone menu when it changes: pass the pathname. */
48
+ locationKey?: string;
49
+ }
50
+
51
+ /**
52
+ * The suite's site header (GUIDELINES §7b): phone menu, brand, mono nav, then search · theme · repository.
53
+ * Every site renders this; only the brand, nav and menu content differ.
54
+ */
55
+ export function SuiteHeader({
56
+ brand,
57
+ brandLabel,
58
+ home,
59
+ nav,
60
+ navLabel = "Main",
61
+ search,
62
+ themeMenu,
63
+ repository,
64
+ menu,
65
+ locationKey,
66
+ }: SuiteHeaderProps) {
67
+ const [menuOpen, setMenuOpen] = useState(false);
68
+ useEffect(() => setMenuOpen(false), [locationKey]);
69
+
70
+ const navLinks = nav.map((item) => (
71
+ <SiteNavLink
72
+ key={item.label}
73
+ href={item.href}
74
+ aria-current={item.current ? "page" : undefined}
75
+ render={item.render}
76
+ >
77
+ {item.label}
78
+ </SiteNavLink>
79
+ ));
80
+
81
+ return (
82
+ <SiteHeader>
83
+ <SiteHeaderContent>
84
+ <SiteHeaderMenu title={brand} open={menuOpen} onOpenChange={setMenuOpen}>
85
+ <nav aria-label={navLabel} className="flex flex-col gap-1 px-2 pt-2">
86
+ {navLinks}
87
+ </nav>
88
+ {menu}
89
+ </SiteHeaderMenu>
90
+ <SiteHeaderBrand aria-label={brandLabel} href={home ? undefined : "/"} render={home}>
91
+ {brand}
92
+ </SiteHeaderBrand>
93
+ <SiteNav aria-label={navLabel}>{navLinks}</SiteNav>
94
+ <SiteHeaderActions>
95
+ {search}
96
+ <Separator
97
+ orientation="vertical"
98
+ className="mx-1 hidden data-vertical:h-5 data-vertical:self-center sm:block"
99
+ />
100
+ {themeMenu ?? <ThemeMenu />}
101
+ {repository ? (
102
+ <Button
103
+ variant="ghost"
104
+ size="icon"
105
+ render={<a href={repository.href} aria-label={repository.label ?? "GitHub"} />}
106
+ >
107
+ {repository.icon ?? <GithubIcon className="size-4" />}
108
+ </Button>
109
+ ) : null}
110
+ </SiteHeaderActions>
111
+ </SiteHeaderContent>
112
+ </SiteHeader>
113
+ );
114
+ }
@@ -0,0 +1,8 @@
1
+ import {useEffect} from "react";
2
+
3
+ /** Sets the tab title: "Page · Brand", or the bare brand on the home page (GUIDELINES §7b). */
4
+ export function usePageTitle(title: string | undefined, brand: string) {
5
+ useEffect(() => {
6
+ document.title = title ? `${title} · ${brand}` : brand;
7
+ }, [title, brand]);
8
+ }
@@ -17,6 +17,8 @@
17
17
  @source "../provider";
18
18
  @source "../lib";
19
19
  @source "../hooks";
20
+ @source "../docs";
21
+ @source "../site";
20
22
 
21
23
  /* Nearest scope wins, so a .light preview nested in a .dark page isn't styled as dark. */
22
24
  @custom-variant dark (&:is(.dark, .dark *):not(.dark .light, .dark .light *));
@@ -133,6 +135,9 @@
133
135
  --kk-font-mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
134
136
  /* Titles: card, dialog, sheet, empty, alert. Mono by default (the TermiColor look). */
135
137
  --kk-font-heading: var(--kk-font-mono);
138
+ /* Labels and readouts: badges, tooltips, breadcrumbs, kbd, shortcuts, eyebrows, nav, values. Mono by
139
+ default (the TermiColor look); point it at any face. Code always stays on --kk-font-mono. */
140
+ --kk-font-alt: var(--kk-font-mono);
136
141
  /* Site shell (site-header, site-layout): content width, and the sticky header's height that
137
142
  sidebars and anchors offset by. A site with a banner above the header row adds it here. */
138
143
  --kk-site-width: 80rem;
@@ -159,6 +164,7 @@
159
164
  --font-sans: var(--kk-font-sans);
160
165
  --font-mono: var(--kk-font-mono);
161
166
  --font-heading: var(--kk-font-heading);
167
+ --font-alt: var(--kk-font-alt);
162
168
 
163
169
  --radius-xs: calc(var(--kk-radius) * 0.25);
164
170
  --radius-sm: calc(var(--kk-radius) * 0.5);
@@ -0,0 +1,15 @@
1
+ /*
2
+ * The suite's site stylesheet: the kit's styles and the suite fonts (Geist to read, JetBrains Mono for
3
+ * code, labels and the wordmark). A site's index.css:
4
+ *
5
+ * @import "tailwindcss";
6
+ * @import "@repraze/kernelkit/site.css";
7
+ */
8
+ @import "@fontsource-variable/geist";
9
+ @import "@fontsource-variable/jetbrains-mono";
10
+ @import "./kernelkit.css";
11
+
12
+ :root {
13
+ --kk-font-sans: "Geist Variable", ui-sans-serif, system-ui, sans-serif;
14
+ --kk-font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
15
+ }
@@ -0,0 +1,11 @@
1
+ // Ambient types for docsMetaPlugin's modules: add "@repraze/kernelkit/vite/client" to tsconfig `types`.
2
+
3
+ declare module "virtual:docs-meta" {
4
+ const meta: {slug: string; title: string; category: string; description: string; order?: number}[];
5
+ export default meta;
6
+ }
7
+
8
+ declare module "*?demo-sources" {
9
+ const sources: {functions: Record<string, string>};
10
+ export default sources;
11
+ }
@@ -0,0 +1,298 @@
1
+ /// <reference types="node" />
2
+ /**
3
+ * Vite plugins shared by the suite's sites (Node-side: no React, nothing here reaches the browser).
4
+ *
5
+ * ```ts
6
+ * import {docsMetaPlugin, faviconPlugin, spaFallback, suiteSourceConditions, themeScriptPlugin} from "@repraze/kernelkit/vite";
7
+ * ```
8
+ *
9
+ * `vite` is only imported for its types; `typescript` is loaded lazily by docsMetaPlugin / extractFunction.
10
+ */
11
+ import {lstatSync, readdirSync, readFileSync} from "node:fs";
12
+ import {copyFile} from "node:fs/promises";
13
+ import {createRequire} from "node:module";
14
+ import {dirname, join, resolve} from "node:path";
15
+
16
+ import type TS from "typescript";
17
+ import type {Plugin} from "vite";
18
+
19
+ import {themeScript, type ThemeState} from "../provider/theme.ts";
20
+
21
+ // ─── theme script ───────────────────────────────────────────────────────────────────────────────
22
+
23
+ export interface ThemeScriptPluginOptions {
24
+ /** The ThemeProvider's `storageKey`. */
25
+ storageKey: string;
26
+ /** The ThemeProvider's `defaultTheme`. */
27
+ defaults?: Partial<ThemeState>;
28
+ /** The comment in index.html replaced by the script (default `<!-- kernelkit:theme-script -->`). */
29
+ marker?: string;
30
+ /** Script source run first, in the same tag (a migration of an older storage format). */
31
+ prelude?: string;
32
+ }
33
+
34
+ /**
35
+ * Inline the kit's pre-paint theme script into index.html, so the stored mode, contrast, accent and radius
36
+ * apply before first paint. Replaces the marker comment, or goes first in <head> when there is none.
37
+ */
38
+ export function themeScriptPlugin({
39
+ storageKey,
40
+ defaults,
41
+ marker = "<!-- kernelkit:theme-script -->",
42
+ prelude,
43
+ }: ThemeScriptPluginOptions): Plugin {
44
+ const source = [prelude, themeScript({storageKey, defaults})].filter(Boolean).join("\n");
45
+ return {
46
+ name: "kernelkit-theme-script",
47
+ transformIndexHtml: (html) =>
48
+ html.includes(marker)
49
+ ? html.replace(marker, `<script>${source}</script>`)
50
+ : {html, tags: [{tag: "script", children: source, injectTo: "head-prepend"}]},
51
+ };
52
+ }
53
+
54
+ // ─── favicon ────────────────────────────────────────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Serve (dev) and emit (build) `favicon.svg` from the brand mark's SVG, so the icon never drifts from the
58
+ * logo. Link it with `href="%BASE_URL%favicon.svg"` so it works under a Pages base path.
59
+ */
60
+ export function faviconPlugin(svg: string | (() => string), fileName = "favicon.svg"): Plugin {
61
+ const source = typeof svg === "function" ? svg() : svg;
62
+ return {
63
+ name: "kernelkit-favicon",
64
+ configureServer(server) {
65
+ server.middlewares.use((req, res, next) => {
66
+ if (req.url?.split("?")[0]?.endsWith(`/${fileName}`)) {
67
+ res.setHeader("Content-Type", "image/svg+xml");
68
+ res.end(source);
69
+ return;
70
+ }
71
+ next();
72
+ });
73
+ },
74
+ generateBundle() {
75
+ this.emitFile({type: "asset", fileName, source});
76
+ },
77
+ };
78
+ }
79
+
80
+ // ─── SPA fallback ───────────────────────────────────────────────────────────────────────────────
81
+
82
+ /** GitHub Pages has no SPA rewrites: copy index.html to 404.html so deep links load the app. */
83
+ export function spaFallback(): Plugin {
84
+ return {
85
+ name: "kernelkit-spa-fallback",
86
+ apply: "build",
87
+ async writeBundle({dir}) {
88
+ if (dir) {
89
+ await copyFile(join(dir, "index.html"), join(dir, "404.html"));
90
+ }
91
+ },
92
+ };
93
+ }
94
+
95
+ // ─── linked suite packages ──────────────────────────────────────────────────────────────────────
96
+
97
+ /** Is `node_modules/<name>` a symlink (npm link, a workspace), looking up from `root`? */
98
+ function isLinked(name: string, root: string): boolean {
99
+ for (let dir = resolve(root); ; dir = dirname(dir)) {
100
+ try {
101
+ return lstatSync(join(dir, "node_modules", name)).isSymbolicLink();
102
+ } catch {
103
+ // not installed here: look in the parent
104
+ }
105
+ if (dirname(dir) === dir) {
106
+ return false;
107
+ }
108
+ }
109
+ }
110
+
111
+ export interface SuiteSourceOptions {
112
+ /** Packages with React contexts to keep as one copy when a linked package brings its own (default below). */
113
+ dedupe?: string[];
114
+ /** The exports condition pointing at TypeScript sources (default "@repraze/source"). */
115
+ condition?: string;
116
+ }
117
+
118
+ const DEDUPE = ["react", "react-dom", "@base-ui/react", "cmdk", "lucide-react"];
119
+
120
+ /**
121
+ * For each suite package (`@repraze/kernelkit`…) that is symlinked into node_modules (`npm link`, a
122
+ * workspace): serve it from its TypeScript sources (its `@repraze/source` exports condition), unbundled,
123
+ * so edits there hot-reload; dedupe React-context packages, since a linked package sits outside this tree.
124
+ * Installed from npm: nothing changes, its built dist is used and pre-bundled like any dependency.
125
+ */
126
+ export function suiteSourceConditions(packages: string[], options: SuiteSourceOptions = {}): Plugin {
127
+ const {dedupe = DEDUPE, condition = "@repraze/source"} = options;
128
+ return {
129
+ name: "kernelkit-suite-source-conditions",
130
+ enforce: "pre",
131
+ config(config) {
132
+ const linked = packages.filter((name) => isLinked(name, config.root ?? process.cwd()));
133
+ if (!linked.length) {
134
+ return undefined;
135
+ }
136
+ return {
137
+ resolve: {conditions: [condition, ...DEFAULT_CLIENT_CONDITIONS], dedupe},
138
+ optimizeDeps: {exclude: linked},
139
+ };
140
+ },
141
+ };
142
+ }
143
+
144
+ /** Vite's `defaultClientConditions`, inlined so this module needs no runtime import of vite. */
145
+ const DEFAULT_CLIENT_CONDITIONS = ["module", "browser", "development|production"];
146
+
147
+ // ─── docs manifest and demo sources ─────────────────────────────────────────────────────────────
148
+
149
+ const require = createRequire(import.meta.url);
150
+ let typescript: typeof TS | undefined;
151
+ /** TypeScript, loaded on first use (the sites already have it): only docs sites pay for it. */
152
+ const ts = () => (typescript ??= require("typescript") as typeof TS);
153
+
154
+ /** Static value of a string/number expression: literals, no-substitution templates, "a" + "b". */
155
+ function literal(node: TS.Expression): string | number | undefined {
156
+ const t = ts();
157
+ if (t.isStringLiteral(node) || t.isNoSubstitutionTemplateLiteral(node)) {
158
+ return node.text;
159
+ }
160
+ if (t.isNumericLiteral(node)) {
161
+ return Number(node.text);
162
+ }
163
+ if (t.isParenthesizedExpression(node)) {
164
+ return literal(node.expression);
165
+ }
166
+ if (t.isBinaryExpression(node) && node.operatorToken.kind === t.SyntaxKind.PlusToken) {
167
+ const a = literal(node.left);
168
+ const b = literal(node.right);
169
+ return a !== undefined && b !== undefined ? `${a}${b}` : undefined;
170
+ }
171
+ return undefined;
172
+ }
173
+
174
+ /** What `virtual:docs-meta` lists per page. */
175
+ export interface DocsMetaEntry {
176
+ slug: string;
177
+ title: string;
178
+ category: string;
179
+ description: string;
180
+ order?: number;
181
+ }
182
+
183
+ /** A docs page's manifest entry, read from its `defineDoc({...})` call without running it. */
184
+ export function readDocMeta(file: string, slug: string, source = readFileSync(file, "utf8")): DocsMetaEntry {
185
+ const t = ts();
186
+ const sf = t.createSourceFile(file, source, t.ScriptTarget.Latest, true);
187
+ let meta: DocsMetaEntry | undefined;
188
+ const visit = (node: TS.Node) => {
189
+ if (
190
+ !meta &&
191
+ t.isCallExpression(node) &&
192
+ t.isIdentifier(node.expression) &&
193
+ node.expression.text === "defineDoc" &&
194
+ node.arguments[0] &&
195
+ t.isObjectLiteralExpression(node.arguments[0])
196
+ ) {
197
+ const props: Record<string, string | number | undefined> = {};
198
+ for (const p of node.arguments[0].properties) {
199
+ if (t.isPropertyAssignment(p) && t.isIdentifier(p.name)) {
200
+ props[p.name.text] = literal(p.initializer);
201
+ }
202
+ }
203
+ if (typeof props.title !== "string" || typeof props.category !== "string") {
204
+ throw new Error(`${file}: defineDoc needs literal "title" and "category" (read at build time)`);
205
+ }
206
+ meta = {
207
+ slug,
208
+ title: props.title,
209
+ category: props.category,
210
+ description: typeof props.description === "string" ? props.description : "",
211
+ ...(typeof props.order === "number" ? {order: props.order} : {}),
212
+ };
213
+ }
214
+ t.forEachChild(node, visit);
215
+ };
216
+ visit(sf);
217
+ if (!meta) {
218
+ throw new Error(`${file}: no defineDoc({...}) call found`);
219
+ }
220
+ return meta;
221
+ }
222
+
223
+ /** Each top-level function or const of a file, by name, as written (without `export `). */
224
+ export function extractFunctions(source: string, fileName = "doc.tsx"): Record<string, string> {
225
+ const t = ts();
226
+ const sf = t.createSourceFile(fileName, source, t.ScriptTarget.Latest, true, t.ScriptKind.TSX);
227
+ const out: Record<string, string> = {};
228
+ const text = (node: TS.Node) => source.slice(node.getStart(sf), node.getEnd()).replace(/^export /, "");
229
+ for (const statement of sf.statements) {
230
+ if (t.isFunctionDeclaration(statement) && statement.name) {
231
+ out[statement.name.text] = text(statement);
232
+ } else if (t.isVariableStatement(statement)) {
233
+ for (const decl of statement.declarationList.declarations) {
234
+ if (t.isIdentifier(decl.name)) {
235
+ out[decl.name.text] = text(statement);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ return out;
241
+ }
242
+
243
+ /**
244
+ * A demo's own source: its declaration from `export function Name` (or `export const Name`) to its
245
+ * closing brace, parsed with TypeScript, so code after it (a `Notes()` helper, constants) stays out.
246
+ */
247
+ export function extractFunction(source: string, name: string): string {
248
+ return extractFunctions(source)[name] ?? "";
249
+ }
250
+
251
+ const META_ID = "virtual:docs-meta";
252
+ const META_RESOLVED = `\0${META_ID}`;
253
+ const SOURCES_QUERY = "demo-sources";
254
+
255
+ /**
256
+ * The docs build step, for `@repraze/kernelkit/docs`:
257
+ * - `virtual:docs-meta`: title, category, description and order of every page in `docsDir`, read
258
+ * statically from its `defineDoc({...})` call, so the nav and search don't pull every page's demos into
259
+ * the main bundle;
260
+ * - `<page>.tsx?demo-sources`: `{functions}`, each demo's own source for its Code tab (see extractFunction).
261
+ *
262
+ * Types: add `@repraze/kernelkit/vite/client` to the site's tsconfig `types`.
263
+ */
264
+ export function docsMetaPlugin(docsDir: string): Plugin {
265
+ const dir = resolve(docsDir);
266
+ return {
267
+ name: "kernelkit-docs-meta",
268
+ enforce: "pre",
269
+ resolveId: (id) => (id === META_ID ? META_RESOLVED : undefined),
270
+ load(id) {
271
+ if (id === META_RESOLVED) {
272
+ const files = readdirSync(dir).filter((f) => f.endsWith(".tsx"));
273
+ const metas = files.map((f) => {
274
+ const file = join(dir, f);
275
+ this.addWatchFile(file);
276
+ return readDocMeta(file, f.replace(/\.tsx$/, ""));
277
+ });
278
+ return `export default ${JSON.stringify(metas)};`;
279
+ }
280
+ const [file = "", query = ""] = id.split("?");
281
+ if (query.split("&").includes(SOURCES_QUERY)) {
282
+ this.addWatchFile(file);
283
+ const functions = extractFunctions(readFileSync(file, "utf8"), file);
284
+ return {code: `export default ${JSON.stringify({functions})};`, moduleType: "js"};
285
+ }
286
+ return undefined;
287
+ },
288
+ // a docs page added, removed or retitled → refresh the manifest
289
+ handleHotUpdate({file, server}) {
290
+ if (file.startsWith(dir)) {
291
+ const mod = server.moduleGraph.getModuleById(META_RESOLVED);
292
+ if (mod) {
293
+ server.moduleGraph.invalidateModule(mod);
294
+ }
295
+ }
296
+ },
297
+ };
298
+ }