@orbytes/astrolab 0.3.0

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 (94) hide show
  1. package/LICENSE +37 -0
  2. package/README.md +410 -0
  3. package/bin/lab-cull.mjs +401 -0
  4. package/bin/pin-gallery.mjs +121 -0
  5. package/defaults.mjs +120 -0
  6. package/dist/core/astro-integration.js +130 -0
  7. package/dist/core/index.js +6 -0
  8. package/dist/core/lib-paths.js +29 -0
  9. package/dist/core/options.js +173 -0
  10. package/dist/core/utils/get-exports.js +52 -0
  11. package/dist/core/utils/invariant.js +12 -0
  12. package/dist/core/utils/kebab-case.js +15 -0
  13. package/dist/core/utils/path-builder.js +22 -0
  14. package/dist/core/utils/path.js +53 -0
  15. package/dist/core/virtual-module/get-story-modules.js +60 -0
  16. package/dist/core/virtual-module/story-modules.js +8 -0
  17. package/dist/core/virtual-module/virtual-module-ids.js +22 -0
  18. package/dist/core/virtual-module/virtual-routes.js +83 -0
  19. package/dist/core/virtual-module/vite-plugin.js +98 -0
  20. package/docs/PIN-CONTRACT.md +263 -0
  21. package/docs/PIN.md +429 -0
  22. package/index.d.ts +279 -0
  23. package/index.mjs +347 -0
  24. package/package.json +93 -0
  25. package/src/Empty.astro +4 -0
  26. package/src/Home.astro +298 -0
  27. package/src/LabHead.astro +1102 -0
  28. package/src/core/LICENSE-astrobook +166 -0
  29. package/src/core/astro-integration.ts +166 -0
  30. package/src/core/client.ts +89 -0
  31. package/src/core/index.ts +7 -0
  32. package/src/core/lib/components/empty.astro +1 -0
  33. package/src/core/lib/components/head.astro +1 -0
  34. package/src/core/lib/components/home.astro +8 -0
  35. package/src/core/lib/components/with-decorators.astro +22 -0
  36. package/src/core/lib/pages/app.astro +19 -0
  37. package/src/core/lib/pages/preview.astro +17 -0
  38. package/src/core/lib/pages/story.astro +16 -0
  39. package/src/core/lib-paths.ts +72 -0
  40. package/src/core/options.ts +262 -0
  41. package/src/core/utils/get-exports.ts +59 -0
  42. package/src/core/utils/invariant.ts +13 -0
  43. package/src/core/utils/kebab-case.ts +30 -0
  44. package/src/core/utils/path-builder.ts +45 -0
  45. package/src/core/utils/path.ts +80 -0
  46. package/src/core/virtual-module/get-story-modules.ts +110 -0
  47. package/src/core/virtual-module/story-modules.ts +9 -0
  48. package/src/core/virtual-module/virtual-module-ids.ts +17 -0
  49. package/src/core/virtual-module/virtual-routes.ts +130 -0
  50. package/src/core/virtual-module/vite-plugin.ts +125 -0
  51. package/src/pin/board.mjs +1521 -0
  52. package/src/pin/index.mjs +666 -0
  53. package/src/pin/shot.mjs +427 -0
  54. package/src/pin/source-stamp.mjs +159 -0
  55. package/src/pin/tickets.mjs +697 -0
  56. package/src/pin/toolbar.js +3181 -0
  57. package/src/shell/Browse.astro +371 -0
  58. package/src/shell/CardGrid.astro +297 -0
  59. package/src/shell/Viewport.astro +1330 -0
  60. package/src/shell/index.json.ts +12 -0
  61. package/src/shell/lab-index.ts +344 -0
  62. package/src/shell/lab-params.ts +245 -0
  63. package/src/shell/live-files.mjs +164 -0
  64. package/src/shell/marks.mjs +136 -0
  65. package/src/types/index.ts +6 -0
  66. package/src/types/types.ts +239 -0
  67. package/src/types/virtual.d.ts +29 -0
  68. package/src/ui/components/app.astro +13 -0
  69. package/src/ui/components/build-path.ts +13 -0
  70. package/src/ui/components/build-tree.ts +108 -0
  71. package/src/ui/components/collapse-duration.ts +28 -0
  72. package/src/ui/components/compress-terms.ts +10 -0
  73. package/src/ui/components/dashboard-layout.astro +39 -0
  74. package/src/ui/components/home.astro +65 -0
  75. package/src/ui/components/layout.astro +110 -0
  76. package/src/ui/components/preview-layout.astro +109 -0
  77. package/src/ui/components/sidebar-button-fullscreen.astro +38 -0
  78. package/src/ui/components/sidebar-button-search.astro +23 -0
  79. package/src/ui/components/sidebar-button-theme.astro +9 -0
  80. package/src/ui/components/sidebar-button.astro +24 -0
  81. package/src/ui/components/sidebar-resize-handle.astro +74 -0
  82. package/src/ui/components/sidebar-search-panel.astro +41 -0
  83. package/src/ui/components/sidebar-search-script.ts +103 -0
  84. package/src/ui/components/sidebar-title.astro +17 -0
  85. package/src/ui/components/sidebar-tree-node.astro +143 -0
  86. package/src/ui/components/sidebar-tree.astro +84 -0
  87. package/src/ui/components/sidebar.astro +29 -0
  88. package/src/ui/components/theme-message.ts +26 -0
  89. package/src/ui/components/theme-script.astro +71 -0
  90. package/src/ui/components/theme-toggle.astro +63 -0
  91. package/src/ui/components/theme.ts +32 -0
  92. package/src/ui/index.ts +4 -0
  93. package/src/ui/lab.css +549 -0
  94. package/virtual.d.ts +42 -0
@@ -0,0 +1,164 @@
1
+ // Which lab components are LIVE on the site — derived from the pages under src/pages/, never declared.
2
+ //
3
+ // Dependency-free Node (fs + path only) on purpose: it is imported by three consumers that cannot
4
+ // share a runtime — the build-time index (./lab-index.ts, inside Vite), the dev-only mark APIs
5
+ // (../../index.mjs, a Vite middleware) and the cull binary (../../bin/lab-cull.mjs, plain Node).
6
+ // One parser, so the three can never disagree about what "live" means.
7
+ //
8
+ // The rule: a component file is live when a page imports it AND mounts it as a tag. Slot is its
9
+ // 1-based position among the mounted tags in that page's document order (the Footer sits outside
10
+ // <main>, so the whole template is scanned, not just <main>). The page layout (src/layouts/) is a
11
+ // wrapper, not a section, and is excluded — everything else the page mounts counts.
12
+ //
13
+ // EVERY page is walked, not just the home page (2026-09-06). orbytes.io is one page, but the lab
14
+ // is a shared package every orbytes Astro site installs, and on a site with an /about page a
15
+ // home-page-only reader reports nothing, or the wrong slot, for everything that is not on the
16
+ // home page. Pages come home first, then alphabetically, so a component mounted on
17
+ // several pages reports the home page's slot as its primary one.
18
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
19
+ import path from "node:path";
20
+
21
+ export const PAGES_DIR = "src/pages";
22
+ export const HOME_PAGE = "src/pages/index.astro";
23
+
24
+ /** @param {string} rootDir @param {string} absFile */
25
+ export const toRepoRelative = (rootDir, absFile) =>
26
+ path.relative(rootDir, absFile).split(path.sep).join("/");
27
+
28
+ /** The `---` fenced frontmatter of an .astro source, or "" when there is none. @param {string} source */
29
+ export const frontmatterOf = (source) => {
30
+ const m = source.match(/^---\r?\n([\s\S]*?)\r?\n---/);
31
+ return m ? m[1] : "";
32
+ };
33
+
34
+ /** Everything after the frontmatter fence (the template). @param {string} source */
35
+ export const templateOf = (source) => {
36
+ const m = source.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
37
+ return m ? m[1] : source;
38
+ };
39
+
40
+ /**
41
+ * Every `import Name from "<relative>"` in a source file, resolved to repo-relative paths.
42
+ * For .astro files only the frontmatter is read; for .ts/.mjs the whole file. Package and
43
+ * virtual-module imports are skipped — they are not files in this repo.
44
+ * @param {string} rootDir absolute repo root
45
+ * @param {string} fileRel repo-relative path of the file to read
46
+ * @returns {{ name: string; file: string }[]}
47
+ */
48
+ export function defaultImports(rootDir, fileRel) {
49
+ const abs = path.join(rootDir, fileRel);
50
+ if (!existsSync(abs)) return [];
51
+ const source = readFileSync(abs, "utf8");
52
+ const code = fileRel.endsWith(".astro") ? frontmatterOf(source) : source;
53
+ /** @type {{ name: string; file: string }[]} */
54
+ const out = [];
55
+ const re = /^\s*import\s+([A-Za-z_$][\w$]*)\s+from\s+["']([^"']+)["']/gm;
56
+ let m;
57
+ while ((m = re.exec(code))) {
58
+ const spec = m[2];
59
+ if (!spec.startsWith(".")) continue;
60
+ out.push({ name: m[1], file: toRepoRelative(rootDir, path.resolve(path.dirname(abs), spec)) });
61
+ }
62
+ return out;
63
+ }
64
+
65
+ /**
66
+ * Every routable page under src/pages, repo-relative, home page first then alphabetical.
67
+ * A name starting with `_` (or `.`) is skipped, file or directory, exactly as Astro's own router
68
+ * skips it — `src/pages/_partials/Card.astro` is not a route and must never claim a slot.
69
+ * @param {string} rootDir absolute repo root
70
+ * @returns {string[]}
71
+ */
72
+ export function sitePages(rootDir) {
73
+ /** @type {string[]} */
74
+ const out = [];
75
+ /** @param {string} rel */
76
+ const walk = (rel) => {
77
+ const abs = path.join(rootDir, rel);
78
+ if (!existsSync(abs)) return;
79
+ for (const entry of readdirSync(abs, { withFileTypes: true })) {
80
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
81
+ const child = `${rel}/${entry.name}`;
82
+ if (entry.isDirectory()) walk(child);
83
+ else if (entry.name.endsWith(".astro")) out.push(child);
84
+ }
85
+ };
86
+ walk(PAGES_DIR);
87
+ return out.sort((a, b) => (a === HOME_PAGE ? -1 : b === HOME_PAGE ? 1 : a.localeCompare(b)));
88
+ }
89
+
90
+ /**
91
+ * The route a page file serves: index.astro → "/", about.astro → "/about", blog/index.astro →
92
+ * "/blog". A dynamic route keeps its brackets ("/[slug]") — this is a label for a pill, not a URL.
93
+ * @param {string} pageRel repo-relative path under src/pages
94
+ */
95
+ export const pageRoute = (pageRel) => {
96
+ const rel = pageRel.slice(PAGES_DIR.length + 1).replace(/\.astro$/, "");
97
+ const route = rel.replace(/(^|\/)index$/, "");
98
+ return `/${route}`.replace(/\/{2,}/g, "/");
99
+ };
100
+
101
+ /**
102
+ * The component files one page mounts, in document order.
103
+ * @param {string} rootDir absolute repo root
104
+ * @param {string} pageRel repo-relative path of the page
105
+ * @returns {string[]}
106
+ */
107
+ export function pageComponentFiles(rootDir, pageRel) {
108
+ const abs = path.join(rootDir, pageRel);
109
+ if (!existsSync(abs)) return [];
110
+ const source = readFileSync(abs, "utf8");
111
+ const byName = new Map(
112
+ defaultImports(rootDir, pageRel)
113
+ .filter((i) => i.file.endsWith(".astro") && !i.file.startsWith("src/layouts/"))
114
+ .map((i) => [i.name, i.file]),
115
+ );
116
+ // HTML and JSX comments name components in prose (the order table does) — strip them first.
117
+ const template = templateOf(source)
118
+ .replace(/<!--[\s\S]*?-->/g, "")
119
+ .replace(/\{\/\*[\s\S]*?\*\/\}/g, "");
120
+ /** @type {string[]} */
121
+ const mounted = [];
122
+ const re = /<([A-Z][\w$]*)(?=[\s/>])/g;
123
+ let m;
124
+ while ((m = re.exec(template))) {
125
+ const file = byName.get(m[1]);
126
+ if (file && !mounted.includes(file)) mounted.push(file);
127
+ }
128
+ return mounted;
129
+ }
130
+
131
+ /**
132
+ * Every live mount on the site: one entry per (page, component), pages in `sitePages` order and
133
+ * components in each page's document order. `of` is how many components that page mounts, so a
134
+ * slot reads "slot 3 of 12" without a second pass, and a two-page site never mixes the two counts.
135
+ * @param {string} rootDir absolute repo root
136
+ * @returns {{ file: string; page: string; slot: number; of: number }[]}
137
+ */
138
+ export function liveComponentFiles(rootDir) {
139
+ /** @type {{ file: string; page: string; slot: number; of: number }[]} */
140
+ const out = [];
141
+ for (const pageRel of sitePages(rootDir)) {
142
+ const files = pageComponentFiles(rootDir, pageRel);
143
+ const page = pageRoute(pageRel);
144
+ files.forEach((file, i) => out.push({ file, page, slot: i + 1, of: files.length }));
145
+ }
146
+ return out;
147
+ }
148
+
149
+ /**
150
+ * The .astro file a stories module renders — the identifier on `component:` in its default
151
+ * export, followed back to its import. Null when the file is missing or the shape is unfamiliar.
152
+ * @param {string} rootDir absolute repo root
153
+ * @param {string} storiesRel repo-relative path of the *.stories.ts file
154
+ * @returns {string | null}
155
+ */
156
+ export function storyComponentFile(rootDir, storiesRel) {
157
+ const abs = path.join(rootDir, storiesRel);
158
+ if (!existsSync(abs)) return null;
159
+ const source = readFileSync(abs, "utf8");
160
+ const m = source.match(/export\s+default\s*\{[\s\S]*?\bcomponent\s*:\s*([A-Za-z_$][\w$]*)/);
161
+ if (!m) return null;
162
+ const hit = defaultImports(rootDir, storiesRel).find((i) => i.name === m[1]);
163
+ return hit ? hit.file : null;
164
+ }
@@ -0,0 +1,136 @@
1
+ // The lab's hand-set marks — everything a person ticks in the sidebar rather than something the
2
+ // lab derives, plus the one path rule both mark files obey.
3
+ //
4
+ // <directory>/responsive.json { "done": string[], "approved": string[], "updated": string|null }
5
+ // <directory>/cull.json { "marked": string[], "updated": string|null }
6
+ //
7
+ // Both are keyed by the story module's repo-relative path
8
+ // (`<directory>/<tier>/…/<Name>.stories.ts`) — one row means one key everywhere — and both are
9
+ // validated through `pathOffence` below, so they can never disagree about what a markable row is.
10
+ //
11
+ // Why the responsive state is marked and not measured: responsive work is approval-gated and
12
+ // final-versions-only (ruled 2026-09-06) — a version is worked on only once it has been approved
13
+ // for responsiveness, approval is NOT the same as the version being final, and that approval can
14
+ // be withdrawn later. Neither fact is readable from the code, so both are ticked here.
15
+ //
16
+ // Dependency-free Node (fs + path only) on purpose: this module is imported inside Vite
17
+ // (./lab-index.ts, at build), by a plain Vite middleware (../../index.mjs) and by the cull binary
18
+ // (../../bin/lab-cull.mjs, plain Node). One parser, three runtimes.
19
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
20
+ import path from "node:path";
21
+
22
+ /** @typedef {import("../../defaults.mjs").LabConfig} LabConfig */
23
+
24
+ /** A story module inside the lab directory: the only thing any mark may name. */
25
+ export const storiesPattern = (directory) =>
26
+ new RegExp(`^${directory.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/(?:[\\w.-]+/)*[\\w.-]+\\.stories\\.ts$`);
27
+
28
+ /**
29
+ * Why `entry` is not a markable story module under `tier`, or null when it is.
30
+ * @param {string} rootDir absolute repo root
31
+ * @param {LabConfig} config
32
+ * @param {unknown} entry
33
+ * @param {string | null} tier repo-relative directory prefix the mark is restricted to
34
+ * @param {string} tierReason what to say when the entry is outside that tier
35
+ * @returns {string | null}
36
+ */
37
+ export const pathOffence = (rootDir, config, entry, tier, tierReason) => {
38
+ if (typeof entry !== "string") return "not a string";
39
+ if (!storiesPattern(config.directory).test(entry) || entry.includes(".."))
40
+ return `not a ${config.directory}/**/*.stories.ts path`;
41
+ if (!tier) return tierReason;
42
+ if (!entry.startsWith(tier)) return tierReason;
43
+ if (!existsSync(path.join(rootDir, entry))) return "no such file";
44
+ return null;
45
+ };
46
+
47
+ /** @param {string} rootDir @param {LabConfig} config @param {unknown} entry */
48
+ export const responsiveOffence = (rootDir, config, entry) =>
49
+ pathOffence(
50
+ rootDir,
51
+ config,
52
+ entry,
53
+ config.responsiveDir,
54
+ "only section versions can be marked responsive",
55
+ );
56
+
57
+ /** @param {string} rootDir @param {LabConfig} config @param {unknown} entry */
58
+ export const cullOffence = (rootDir, config, entry) =>
59
+ pathOffence(rootDir, config, entry, config.cullDir, "only the explorations tier can be marked");
60
+
61
+ /** @param {unknown} value */
62
+ const stringList = (value) =>
63
+ Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
64
+
65
+ /** @param {string} rootDir @param {string} file */
66
+ const readJson = (rootDir, file) => {
67
+ const abs = path.join(rootDir, file);
68
+ if (!existsSync(abs)) return null;
69
+ try {
70
+ return JSON.parse(readFileSync(abs, "utf8"));
71
+ } catch {
72
+ return null;
73
+ }
74
+ };
75
+
76
+ /**
77
+ * The responsive marks as written. Missing or unreadable file → empty, never a throw: the lab
78
+ * renders the same either way, it just shows everything as not responsive.
79
+ * @param {string} rootDir absolute repo root
80
+ * @param {LabConfig} config
81
+ * @returns {{ done: string[]; approved: string[]; updated: string | null }}
82
+ */
83
+ export function readResponsive(rootDir, config) {
84
+ const data = readJson(rootDir, config.responsiveFile);
85
+ if (!data) return { done: [], approved: [], updated: null };
86
+ return {
87
+ done: stringList(data.done),
88
+ approved: stringList(data.approved),
89
+ updated: typeof data.updated === "string" ? data.updated : null,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Replace the responsive marks. Both lists are de-duplicated and sorted so the tracked file's diff
95
+ * is the change and nothing else.
96
+ * @param {string} rootDir absolute repo root
97
+ * @param {LabConfig} config
98
+ * @param {string[]} done
99
+ * @param {string[]} approved
100
+ */
101
+ export function writeResponsive(rootDir, config, done, approved) {
102
+ const data = {
103
+ done: [...new Set(done)].sort(),
104
+ approved: [...new Set(approved)].sort(),
105
+ updated: new Date().toISOString(),
106
+ };
107
+ writeFileSync(path.join(rootDir, config.responsiveFile), JSON.stringify(data, null, 2) + "\n");
108
+ return data;
109
+ }
110
+
111
+ /**
112
+ * The cull marks as written.
113
+ * @param {string} rootDir absolute repo root
114
+ * @param {LabConfig} config
115
+ * @returns {{ marked: string[]; updated: string | null }}
116
+ */
117
+ export function readCull(rootDir, config) {
118
+ const data = readJson(rootDir, config.cullFile);
119
+ if (!data) return { marked: [], updated: null };
120
+ return {
121
+ marked: stringList(data.marked),
122
+ updated: typeof data.updated === "string" ? data.updated : null,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Replace the cull marks.
128
+ * @param {string} rootDir absolute repo root
129
+ * @param {LabConfig} config
130
+ * @param {string[]} marked
131
+ */
132
+ export function writeCull(rootDir, config, marked) {
133
+ const data = { marked: [...new Set(marked)].sort(), updated: new Date().toISOString() };
134
+ writeFileSync(path.join(rootDir, config.cullFile), JSON.stringify(data, null, 2) + "\n");
135
+ return data;
136
+ }
@@ -0,0 +1,6 @@
1
+ // Vendored from astrobook 0.13.3 (packages/types/lib/index.d.ts + index.js) — see
2
+ // ../core/LICENSE-astrobook. Upstream splits this into a `.d.ts` that re-exports and an
3
+ // `index.js` that is `export {}`; one `.ts` does both jobs here, because nothing bundles.
4
+ /// <reference path="./virtual.d.ts" />
5
+
6
+ export type * from './types.ts'
@@ -0,0 +1,239 @@
1
+ // Vendored from astrobook 0.13.3 (packages/types/lib/types.d.ts) — see ../core/LICENSE-astrobook.
2
+ // Divergence: `.d.ts` → `.ts`, so the vendored core can import these by relative path without
3
+ // `allowImportingTsExtensions` gymnastics. The body is interfaces and one `declare global` only,
4
+ // so it emits nothing at runtime and every importer uses `import type`.
5
+ // Divergence: the option doc comments upstream carries for options this package does not expose
6
+ // are kept verbatim — they document the vendored core's own behaviour, which is unchanged.
7
+
8
+ export interface IntegrationOptions {
9
+ /**
10
+ * The directory to scan for stories.
11
+ *
12
+ * @default '.'
13
+ */
14
+ directory?: string
15
+
16
+ /**
17
+ * The subpath to deploy Astrobook to relative to the [base
18
+ * URL](https://docs.astro.build/en/reference/configuration-reference/#base)
19
+ * of your Astro project.
20
+ */
21
+ subpath?: string
22
+
23
+ /**
24
+ * The subpath for dashboard pages relative to the main subpath.
25
+ * A dashboard page has a sidebar for navigation between components.
26
+ *
27
+ * @default '/dashboard/'
28
+ */
29
+ dashboardSubpath?: string
30
+
31
+ /**
32
+ * The subpath for preview pages relative to the main subpath.
33
+ * A preview page displays an individual component without the sidebar.
34
+ *
35
+ * @default '/stories/'
36
+ */
37
+ previewSubpath?: string
38
+
39
+ /**
40
+ * Set the title for your website. Will be used in metadata and in the browser tab title.
41
+ *
42
+ * @default 'Astrobook'
43
+ */
44
+ title?: string
45
+
46
+ /**
47
+ * Provide CSS files to customize the look and feel of your Astrobook project.
48
+ *
49
+ * Supports local CSS files relative to the root of your project,
50
+ * e.g. `'./src/custom.css'`, and CSS you installed as an npm
51
+ * module, e.g. `'@fontsource/roboto'`.
52
+ *
53
+ * In this package these files reach the PREVIEW only — the story render — never the lab's own
54
+ * chrome (decided 2026-09-22). See src/ui/lab.css.
55
+ */
56
+ css?: string[]
57
+
58
+ /**
59
+ * The path to an Astro component to provide custom tags in the `<head>`.
60
+ * It can be an `.astro` or `.html` file.
61
+ */
62
+ head?: string
63
+
64
+ /**
65
+ * Controls which Astro component is rendered on the dashboard homepage.
66
+ *
67
+ * - Pass a string (relative path or package identifier) to fully replace
68
+ * the built-in home page with a custom Astro component.
69
+ * - Pass `false` to render an empty home page.
70
+ * - Omit the option to use Astrobook's built-in home.
71
+ *
72
+ * @default 'astrobook/components/home.astro'
73
+ */
74
+ home?: string | false
75
+
76
+ /**
77
+ * Customize the content of Astrobook's built-in home page (title, subtitle,
78
+ * version badge, GitHub badge).
79
+ */
80
+ homeContent?: HomeContentOptions
81
+ }
82
+
83
+ /**
84
+ * Options for customizing the content of the built-in home page.
85
+ */
86
+ export interface HomeContentOptions {
87
+ /**
88
+ * The main title shown on the home page. Set to `false` to hide.
89
+ * @default 'Astrobook'
90
+ */
91
+ title?: string | false
92
+
93
+ /**
94
+ * The subtitle shown beneath the title. Set to `false` to hide.
95
+ * @default 'The minimal UI component playground'
96
+ */
97
+ subtitle?: string | false
98
+
99
+ /** Configuration for the version badge. Set to `false` to hide. */
100
+ version?: HomeVersionOptions | false
101
+
102
+ /** Configuration for the GitHub repository badge. Set to `false` to hide. */
103
+ repo?: HomeRepoOptions | false
104
+ }
105
+
106
+ export interface HomeVersionOptions {
107
+ /** The URL the version badge links to. */
108
+ href?: string
109
+
110
+ /** The label rendered inside the version badge. */
111
+ label?: string
112
+ }
113
+
114
+ export interface HomeRepoOptions {
115
+ /** The URL the repository badge links to. */
116
+ href?: string
117
+
118
+ /**
119
+ * The label rendered inside the repository badge.
120
+ * @default 'Star on GitHub'
121
+ */
122
+ label?: string
123
+ }
124
+
125
+ export interface StoryModule {
126
+ /**
127
+ * The id of the story module.
128
+ *
129
+ * @example 'components/ui/button'
130
+ */
131
+ id: string
132
+
133
+ /**
134
+ * The name of the story module.
135
+ *
136
+ * @example 'Button'
137
+ */
138
+ name: string
139
+
140
+ /**
141
+ * The directory of the story module. It might be an empty string.
142
+ *
143
+ * @example 'components/ui'
144
+ */
145
+ directory: string
146
+
147
+ /**
148
+ * The import path of the story module.
149
+ *
150
+ * @example '/Users/john/projects/my-project/components/ui/Button.stories.js'
151
+ * @example '../ui/Button.stories.ts'
152
+ */
153
+ importPath: string
154
+
155
+ /** All stories in the story module. */
156
+ stories: Story[]
157
+ }
158
+
159
+ export interface Story {
160
+ /**
161
+ * The id of the story.
162
+ *
163
+ * @example 'components/ui/button/primary-button'
164
+ */
165
+ id: string
166
+
167
+ /**
168
+ * The name of the story.
169
+ *
170
+ * @example 'PrimaryButton'
171
+ */
172
+ name: string
173
+ }
174
+
175
+ export interface GlobalConfig {
176
+ /**
177
+ * The computed base URL path for Astrobook, combining the Astro base URL with
178
+ * the optional subpath.
179
+ */
180
+ astrobookBase: string
181
+ dashboardBase: string
182
+ storyBase: string
183
+
184
+ /** The path to an Astro component to provide custom tags in the `<head>`. */
185
+ head: string
186
+
187
+ /**
188
+ * The path to an Astro component to render on dashboard homepage.
189
+ * @default astrobook/components/home.astro
190
+ */
191
+ home: string
192
+
193
+ /**
194
+ * The fully resolved content for the built-in home page. When the user
195
+ * provides a custom `home` component path or `home: false`, this still
196
+ * holds the default values but is not consumed by the rendered component.
197
+ */
198
+ homeContent: {
199
+ title: string | false
200
+ subtitle: string | false
201
+ version: { href: string; label: string } | false
202
+ repo: { href: string; label: string } | false
203
+ }
204
+
205
+ /** Array of CSS file paths to import into the PREVIEW. */
206
+ css: string[]
207
+
208
+ /** The title for the Astrobook website. */
209
+ title: string
210
+
211
+ trailingSlash: import('astro').AstroConfig['trailingSlash']
212
+ }
213
+
214
+ /** @internal */
215
+ export interface StoryDecorator {
216
+ /** The component to use as a decorator. */
217
+ component: unknown
218
+
219
+ /** The props to pass to the decorator. */
220
+ props?: object | null | undefined
221
+ }
222
+
223
+ /** @internal */
224
+ export interface StoryNamedExport {
225
+ /** The decorators to apply to the story. */
226
+ decorators?: StoryDecorator[] | null | undefined
227
+
228
+ /** The arguments to pass to the story. */
229
+ args?: object | null | undefined
230
+ }
231
+
232
+ declare global {
233
+ interface Window {
234
+ astrobook?: {
235
+ setTheme?: (theme: 'dark' | 'light') => void
236
+ getTheme?: () => 'dark' | 'light'
237
+ }
238
+ }
239
+ }
@@ -0,0 +1,29 @@
1
+ // Vendored from astrobook 0.13.3 (packages/types/lib/virtual.d.ts) — see ../core/LICENSE-astrobook.
2
+ // Divergence: `./types` → `./types.js`, which is how TypeScript's node16/bundler resolution names
3
+ // the sibling `types.ts` module.
4
+ //
5
+ // THE IDS BELOW ARE UPSTREAM'S, BYTE FOR BYTE, AND MUST STAY THAT WAY. The vendored core emits
6
+ // them (src/core/virtual-module/virtual-module-ids.ts) and the vendored UI imports them by name;
7
+ // renaming both sides is possible now that we own both, but it is pure churn with silent-breakage
8
+ // risk, so it is later cleanup and not part of this pass.
9
+ declare module 'virtual:astrobook/story-modules.mjs' {
10
+ const entries: import('./types.js').StoryModule[]
11
+ export default entries
12
+ }
13
+
14
+ declare module 'virtual:astrobook/global-config.mjs' {
15
+ const config: import('./types.js').GlobalConfig
16
+ export default config
17
+ }
18
+
19
+ declare module 'virtual:astrobook/components/head.mjs' {
20
+ const Head: (props: unknown) => unknown
21
+ export default Head
22
+ }
23
+
24
+ declare module 'virtual:astrobook/components/home.mjs' {
25
+ const Home: (props: unknown) => unknown
26
+ export default Home
27
+ }
28
+
29
+ declare module 'virtual:astrobook/user-css.mjs' {}
@@ -0,0 +1,13 @@
1
+ ---
2
+ // Vendored from astrobook 0.13.3 (packages/ui/src/components/app.astro) — see
3
+ // ../../core/LICENSE-astrobook. Unchanged apart from the .astro extension on the relative import.
4
+ //
5
+ // The `/lab` home page: pure chrome, so it renders through ./layout.astro and gets no consumer
6
+ // CSS at all (ruled 2026-09-22 — ../lab.css).
7
+ import Layout from './layout.astro'
8
+ import Home from 'virtual:astrobook/components/home.mjs'
9
+ ---
10
+
11
+ <Layout story={undefined} hasSidebar={true}>
12
+ <Home />
13
+ </Layout>
@@ -0,0 +1,13 @@
1
+ // Vendored from astrobook 0.13.3 (packages/ui/src/components/build-path.ts) — see
2
+ // ../../core/LICENSE-astrobook.
3
+ // Divergence: `@astrobook/core/client` → the vendored core's own path-builder, by relative path
4
+ // with the .ts extension. The `import '@astrobook/types'` side-effect line is gone: the ambient
5
+ // declarations it pulled in now live in ../../types/virtual.d.ts, referenced from the package's
6
+ // own virtual.d.ts.
7
+ import config from 'virtual:astrobook/global-config.mjs'
8
+
9
+ import { createPathBuilder } from '../../core/utils/path-builder.ts'
10
+
11
+ export const buildPath: (...parts: string[]) => string = createPathBuilder({
12
+ trailingSlash: config.trailingSlash,
13
+ })