@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
package/LICENSE ADDED
@@ -0,0 +1,37 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 William Teig (orbytes.io)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ PARTS OF THIS PACKAGE ARE VENDORED FROM ASTROBOOK, ALSO UNDER MIT.
26
+
27
+ `src/core/`, `src/ui/` and `src/types/` are vendored from Astrobook
28
+ (https://github.com/ocavue/astrobook), version 0.13.3, commit
29
+ a79f03cdfd32eb08330b33fa7d5830ac506f9272 — Copyright (c) 2024, 2025 ocavue.
30
+
31
+ Their upstream copyright and permission notices are reproduced in full in
32
+ `src/core/LICENSE-astrobook`, which also records exactly which files diverge
33
+ from upstream and how. That file ships inside the published package, which is
34
+ what MIT redistribution requires; do not remove it from the `files` list in
35
+ package.json, and do not move it without checking that it still ships.
36
+
37
+ Everything outside those three directories is original to this package.
package/README.md ADDED
@@ -0,0 +1,410 @@
1
+ # @orbytes/astrolab
2
+
3
+ Two dev surfaces for an Astro site, in one package and one integration call.
4
+
5
+ **The component lab** at `/lab` — an [Astrobook](https://github.com/ocavue/astrobook) shell. Every
6
+ section, component and experiment in a build, rendered live, with the one thing a component
7
+ playground normally cannot tell you — **which of these is actually on the site right now**, derived
8
+ from the pages rather than declared.
9
+
10
+ **The pin board** at `/pin` — click a rendered element on `astro dev`, leave a comment, and a
11
+ markdown ticket plus a screenshot land on local disk for an agent to pick up
12
+ ([docs/PIN.md](./docs/PIN.md), contract at [docs/PIN-CONTRACT.md](./docs/PIN-CONTRACT.md)). It moved
13
+ in from its own package on 2026-09-22, so the two install, configure and version as one thing. Dev
14
+ only, switchable off with `pin: false`.
15
+
16
+ It ships mostly **source**. The `.astro` files, and everything under `src/shell/`, `src/ui/` and
17
+ `src/types/`, are compiled by the consumer's own Vite, the way Starlight ships its components —
18
+ which is why the lab picks up the consumer's tokens, fonts and breakpoints in a story preview: it
19
+ renders inside the consumer's pipeline. The one exception is `src/core/`, transpiled to `dist/` by
20
+ `scripts/build-core.mjs` at `prepack` and `prepare`, because `astro.config.mjs` is loaded by Node's
21
+ own ESM loader and Node refuses to strip TypeScript under `node_modules`. Without that step an
22
+ installed copy cannot be loaded at all. The script's header comment is the full account.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ npm i -D @orbytes/astrolab
28
+ ```
29
+
30
+ Then add the integration:
31
+
32
+ ```js
33
+ // astro.config.mjs
34
+ import { defineConfig } from "astro/config";
35
+ import orbytesLab from "@orbytes/astrolab";
36
+
37
+ export default defineConfig({
38
+ integrations: [
39
+ orbytesLab({
40
+ css: ["./src/styles/global.css"], // your token and base stylesheets
41
+ }),
42
+ ],
43
+ });
44
+ ```
45
+
46
+ That is the whole configuration. In particular, **do not add an `ssr.noExternal` entry for this
47
+ package** — the integration adds its own, because the package ships `.astro` files that Vite would
48
+ otherwise externalise and hand to Node, which cannot read them. A consumer needs nothing. (Two
49
+ older orbytes repos set it by hand; it is redundant, and copying it from them is copying noise.)
50
+
51
+ Now write a story. Stories live under `src/lab/`, in one directory per tier:
52
+
53
+ ```
54
+ src/lab/
55
+ sections/Section01Hero/V1/Section01Hero.astro + Section01Hero.stories.ts
56
+ components/Button.astro + Button.stories.ts
57
+ explorations/KeycapButton.astro + KeycapButton.stories.ts
58
+ decorators/ ← yours, built against YOUR page shell (see below)
59
+ cull.json ← written by the lab, commit it
60
+ responsive.json ← written by the lab, commit it
61
+ ```
62
+
63
+ A stories file has a default export naming the component, and one named export per story. The
64
+ export's name is what the sidebar shows; its `args` are the component's props.
65
+
66
+ ```ts
67
+ // src/lab/components/Button.stories.ts
68
+ import Button from "./Button.astro";
69
+
70
+ export default {
71
+ component: Button,
72
+ meta: { tags: ["cta", "v2"] }, // optional — the sidebar search matches tags
73
+ };
74
+
75
+ export const Subtle = { args: { label: "See pricing", variant: "subtle" } };
76
+ export const Strong = { args: { label: "Book a call", variant: "strong" } };
77
+ ```
78
+
79
+ Then `npm run dev` and open **`http://localhost:4321/lab`** — the home page lists every tier and
80
+ renders every story live. `subpath` moves it; `directory` moves the stories.
81
+
82
+ **Peers.** Astro **7 or newer**, narrowed from `>=5.2.0` on 2026-09-22 when the pin board moved in:
83
+ the board's dev-toolbar app imports `astro/client/dev-toolbar/apps/utils/highlight.js` and
84
+ `.../window.js`, which are Astro 7 internals and part of no published API, so the old range was a
85
+ promise this package cannot keep. `playwright` is a second peer and an **optional** one — it is
86
+ loaded by dynamic import, only when a screenshot is actually taken, so a site without it runs the
87
+ lab and writes pin tickets without pictures rather than failing to load its config. Add it with
88
+ `npm i -D playwright && npx playwright install chromium` if you want the screenshots.
89
+
90
+ ### Keeping the lab out of production
91
+
92
+ The integration does not decide this; you do. Wrap it in whatever condition your deployment gives
93
+ you, and the lab is simply absent from any build where the condition is false:
94
+
95
+ ```js
96
+ // The lab is a staging surface. Ship it to dev and staging, never to production.
97
+ const includeLab = process.argv.includes("dev") || process.env.MY_DEPLOY_ENV === "staging";
98
+
99
+ export default defineConfig({
100
+ integrations: [includeLab ? orbytesLab({ css: ["./src/styles/global.css"] }) : null],
101
+ });
102
+ ```
103
+
104
+ The environment variable is **yours**. orbytes' own sites use `PUBLIC_DEPLOY_ENV=staging`; that is
105
+ a deployment convention and no part of this package, which never reads any environment variable to
106
+ decide whether it loads. Every lab page also carries an unconditional `noindex`, so the two
107
+ mechanisms are independent — see *What you get*.
108
+
109
+ ### One integration, not three
110
+
111
+ `orbytesLab()` returns the vendored Astrobook integration, the lab shell *and* the pin board as an
112
+ array; Astro flattens integration arrays, so that single entry configures all of them. Do not
113
+ install and add the `astrobook` package yourself — a second copy would fight this one over the same
114
+ routes and the same `virtual:astrobook/*` module ids.
115
+
116
+ The lab alone, for a site that does not want the board:
117
+
118
+ ```js
119
+ orbytesLab({ css: ["./src/styles/global.css"], pin: false })
120
+ ```
121
+
122
+ ### Editing this package from a git worktree
123
+
124
+ Only relevant if you are developing **the lab itself** from a git worktree of a consuming repo. A
125
+ worktree has no `node_modules` of its own, so node walks up and resolves a bare `@orbytes/astrolab`
126
+ to the *main* clone's copy — you edit the package and the running site shows no change at all. The
127
+ whole shell follows that first resolution, because the integration anchors its route entrypoints
128
+ with `new URL(…, import.meta.url)`.
129
+
130
+ The fix has two halves. Import the package by **relative path** rather than by the bare specifier,
131
+ and add a `vite.resolve.alias` entry per **subpath** (`@orbytes/astrolab/head`, `/home`, `/params`
132
+ and friends), because those are resolved by Vite rather than by that import.
133
+ `testbeds/orbytes-v2/astro.config.mjs` builds the alias map from this package's own `name` and
134
+ `exports` so neither half can drift from it — copy that file.
135
+
136
+ **An installed copy needs none of this.** Measured 2026-09-22 against a packed tarball in a clean
137
+ Astro project: the bare specifier resolved, and `/head`, `/home` and `/params` resolved through
138
+ `node_modules` with no alias map at all.
139
+
140
+ ### Installing from a folder copy — the legacy path
141
+
142
+ Before this package was published, a site got it by **copying the folder in** and linking it as a
143
+ workspace. That path still works and at least one orbytes site is still on it, so it is documented
144
+ rather than removed; a new install should use npm.
145
+
146
+ ```sh
147
+ rsync -a --delete --exclude='.git' --exclude='.DS_Store' --exclude='ops/' --exclude='node_modules' \
148
+ --exclude='testbeds/' --exclude='.claude/' --exclude='package-lock.json' \
149
+ <this repo>/ <site>/packages/astro-lab/
150
+ ```
151
+
152
+ The excludes matter: `testbeds/` is this package's own consuming site, not part of what a site
153
+ installs, and a plain `cp -R` would carry it in along with this repo's `workspaces` and `scripts`
154
+ keys. Then, in the site's `package.json`:
155
+
156
+ ```json
157
+ {
158
+ "workspaces": ["packages/*"],
159
+ "devDependencies": { "@orbytes/astrolab": "*" }
160
+ }
161
+ ```
162
+
163
+ `npm install` links it from the site's own repo, so a CI build needs no token and no registry. On
164
+ this path **this repo is canonical**: fix it here first, bump the `version`, then refresh each
165
+ site's copy — that version is the stamp saying which copy a site is on, so drift is visible rather
166
+ than silent.
167
+
168
+ ### What is vendored
169
+
170
+ **Astrobook is not a dependency**, and neither is anything it used to pull in. All three of its
171
+ packages are vendored at version `0.13.3` — the core into [`src/core/`](./src/core/), the UI into
172
+ [`src/ui/`](./src/ui/), the types into [`src/types/`](./src/types/) — and `astro-theme-toggle` with
173
+ them. Nothing outside Astro is installed, and there is nothing to keep in step but this folder.
174
+
175
+ Attribution, the upstream commit, and every deliberate divergence from it are recorded in
176
+ [`src/core/LICENSE-astrobook`](./src/core/LICENSE-astrobook) (MIT, Copyright (c) 2024 ocavue; the
177
+ theme toggle's own notice is at the end of the same file). Read it before touching anything under
178
+ `src/core/`, `src/ui/` or `src/types/` — each mirrors its upstream layout on purpose, so a future
179
+ version can still be diffed in.
180
+
181
+ This package is MIT too; see [LICENSE](./LICENSE).
182
+
183
+ ### The chrome is not styled by the website
184
+
185
+ **Decided 2026-09-22.** The lab's own furniture looks the same in every site it is installed in,
186
+ and only the rendered story takes the site's styling. A site that wants a different-looking lab
187
+ edits this package's source: there is no theme API and no token override, on purpose.
188
+
189
+ So the chrome — layout, sidebar, search, the home and folder pages, the viewport configurator — is
190
+ drawn by one stylesheet that ships with this package, [`src/ui/lab.css`](./src/ui/lab.css), in
191
+ `--lab-*` tokens that cannot collide with a site's own, on a system font stack, with light and dark
192
+ themes of its own. It never receives the `css` list or the `head` component.
193
+
194
+ The **preview** — the story at `/lab/stories/<id>`, and the story rendered inside
195
+ `/lab/dashboard/<id>` — still gets both, because seeing a component as the real site renders it is
196
+ the whole point. Isolation runs both ways: the chrome's stylesheet is not loaded on a bare story
197
+ page, and a component styled by lab CSS the real site has never heard of would be a lab that lies.
198
+ The mechanism, and what it costs, is documented at the top of `lab.css`.
199
+
200
+ ## Options
201
+
202
+ Every option is optional.
203
+
204
+ | Option | Default | What it does |
205
+ | --- | --- | --- |
206
+ | `directory` | `"src/lab"` | Where the stories live, relative to the project root. |
207
+ | `subpath` | `"/lab"` | Where the lab is served. `""` serves it at the site root. |
208
+ | `css` | `[]` | The site's token and base stylesheets. Astrobook injects them into its own pages; the lab's own pages (folder pages, viewport) pull the same list. |
209
+ | `head` | — | A path to the site's own head component, rendered **inside** the lab's head. This is how fonts arrive: lab stories never render through the site's layout, so whatever that layout registers has to be repeated here. |
210
+ | `title` | `"Component lab"` | The lab's name, on the home page and in the dashboard. |
211
+ | `stagingUrl` | — | The deployed lab's base URL — the real minified build, which a dev server is not. Every card and the story header link to the same story there. Omit it and those links do not appear. |
212
+ | `previewUrlTemplate` | — | A branch-preview URL with a `{branch}` placeholder, e.g. `"https://{branch}-my-site.workers.dev"`. The home page then lists the local git branches with a link each. Omit it and there is no branch list. It is a string, not a function, because the config crosses a virtual module and has to stay JSON-serialisable. |
213
+ | `feedbucketKey` | — | Set it and every lab page carries the [Feedbucket](https://feedbucket.app) tag. The package takes a key or nothing: the consumer keeps its own staging gate, so pass the key only where the widget belongs. |
214
+ | `tiers` | `["sections", "components", "explorations"]` | The tiers, in reading order. A tier is the first path segment under `directory`. |
215
+ | `pin` | `true` | The pin board. `false` omits it entirely; an object is the board's own options — see below. |
216
+
217
+ ### Tiers
218
+
219
+ Tier names are a convention, not a law. Pass strings to rename or reorder them, or objects to say
220
+ what each one *is*:
221
+
222
+ ```js
223
+ tiers: [
224
+ { id: "pages", label: "Page sections", responsive: true },
225
+ "atoms",
226
+ { id: "sketches", label: "Sketches", cullable: true },
227
+ ]
228
+ ```
229
+
230
+ - **`responsive`** — this tier holds page sections. Its stories carry the responsive mark, and they
231
+ render at page width (1440×900) in thumbnails and as the viewport's default frame; everything
232
+ else renders on a component stage (960×540). Defaults to true for a tier named `sections`.
233
+ - **`cullable`** — this tier's stories may be marked for deletion in the sidebar and removed by
234
+ `orbytes-lab-cull`. Nothing else can be: version history and shared chrome are never deleted from
235
+ a browser. Defaults to true for a tier named `explorations`.
236
+
237
+ At most one tier holds each role.
238
+
239
+ ## What you get
240
+
241
+ **`<subpath>/`** — the home page: a card per tier, then every story as a scaled-down live render.
242
+
243
+ **`<subpath>/browse/<path>`** — a folder page per folder in the tree, with a filter box.
244
+
245
+ **`<subpath>/viewport/<story>`** — the viewport configurator. Drag, type or pick the frame's width
246
+ and height, zoom independently of it, and read the breakpoint band. The viewport is in the URL
247
+ (`?w=390&h=844&z=0.6`), so a link — or a screenshot tool — names the exact frame. `[` `]` step the
248
+ width presets, `r` rotates, `f` fits, `0` is 1:1.
249
+
250
+ **`<subpath>/index.json`** — the whole index as a static file: every story with its id, URLs,
251
+ files, tier/section/version, live slot, used-by list, tags and summary. Scripts and agent briefs
252
+ read this instead of globbing.
253
+
254
+ **The sidebar**, decorated: a **live** pill on every story a page actually mounts (`live · slot 3
255
+ of 12`, or `live · /about, slot 3` when the page is not the home page), a **used by** pill on
256
+ shared chrome, a **responsive** pill and two checkboxes on section versions, a cull switch in the
257
+ header, and the search widened so `live`, `v1`, `responsive` or a tag all match. `⌘B` collapses the
258
+ sidebar, `⌘K` focuses search, `j`/`k` walk the stories.
259
+
260
+ **An unconditional `noindex`** on every lab page. Lab pages do not render through the site's
261
+ layout, so they never inherit its staging `noindex` — this one is not gated on the environment, on
262
+ purpose. Keep your own build gate as well: two mechanisms, because the rule is absolute.
263
+
264
+ ### "Live" is derived, never declared
265
+
266
+ A component is live when a page under `src/pages/` imports it **and** mounts it as a tag. Every
267
+ page is walked, the layout is excluded, and the slot is the component's position in that page's
268
+ document order. Nothing is configured, so the pill cannot go stale.
269
+
270
+ ## Marks
271
+
272
+ Two JSON files under `directory`, both written by a **dev-only** API (`PUT /__lab/cull`,
273
+ `PUT /__lab/responsive`) and both meant to be committed:
274
+
275
+ - **`responsive.json`** — `{ done, approved }` per section version. Responsive work is
276
+ approval-gated and neither fact is readable from the code, so both are ticked by hand in the
277
+ sidebar.
278
+ - **`cull.json`** — `{ marked }`: stories in the cullable tier, marked for deletion.
279
+
280
+ The API refuses anything outside the right tier, anything that is not a stories file on disk, and
281
+ anything live or used by a live section — with the offender and the reason. In a build there is no
282
+ dev server, so there is no write path at all and the switches hide themselves.
283
+
284
+ ### `orbytes-lab-cull`
285
+
286
+ The marks delete nothing. This binary does, and only when told to:
287
+
288
+ ```sh
289
+ orbytes-lab-cull # dry run — print the plan, remove nothing
290
+ orbytes-lab-cull --json # the same plan as JSON
291
+ orbytes-lab-cull --yes # perform it: `git rm` (staged, not committed)
292
+ orbytes-lab-cull --dir=… # when the lab is not at src/lab
293
+ ```
294
+
295
+ It runs in the current working directory, so wire it as a script in the consumer:
296
+
297
+ ```json
298
+ "scripts": { "lab:cull": "orbytes-lab-cull" }
299
+ ```
300
+
301
+ It removes each marked stories file, the component it renders, and anything under the lab
302
+ directory that only the removal set imports — then refuses the whole run if any entry is blocked.
303
+
304
+ ## Parameters panel
305
+
306
+ A component with something to tune declares it and renders no UI of its own. The panel is drawn by
307
+ the viewport configurator, under the top bar, and only there — the dashboard and the bare story
308
+ page show nothing.
309
+
310
+ ```ts
311
+ import { registerLabParams } from "@orbytes/astrolab/params";
312
+
313
+ const panel = registerLabParams({
314
+ id: "hero-shader", // stable — it is the storage key
315
+ title: "hero shader",
316
+ controls: [
317
+ { kind: "range", id: "speed", label: "Speed", min: 0, max: 4, step: 0.05, value: 1, unit: "×" },
318
+ { kind: "toggle", id: "grain", label: "Grain", value: true },
319
+ ],
320
+ apply(values) { /* push them into whatever they configure */ },
321
+ });
322
+
323
+ const values = panel.values; // LIVE — mutated in place, safe to hold in a render loop
324
+ panel.status("WebGL2 · 0.8 ms/frame");
325
+ ```
326
+
327
+ `apply` is called once at registration with the restored values, so it must tolerate running before
328
+ the thing it configures exists. Values persist per group in `localStorage`. Reset, Copy settings,
329
+ the readouts and the collapse are the panel's job, not the component's.
330
+
331
+ Exported types: `LabParamControl`, `LabParamGroup`, `LabParamHandle`, `LabParamValue`,
332
+ `LabParamValues`, `LabParamEntry`.
333
+
334
+ ## The pin board at `/pin`
335
+
336
+ Dev only, on by default, and it needs no configuration: `orbytesLab()` already gives it the lab's
337
+ resolved `subpath` so the board's own "Lab" link points wherever the lab actually is. Everything it
338
+ does is in [docs/PIN.md](./docs/PIN.md); what matters at the integration boundary is here.
339
+
340
+ ```js
341
+ orbytesLab({
342
+ css: ["./src/styles/global.css"],
343
+ pin: {
344
+ stamp: ["src/lab/sections", "src/components"], // site-relative
345
+ project: "acme-website", // the screenshot archive folder
346
+ },
347
+ })
348
+ ```
349
+
350
+ | Option | Default | What it does |
351
+ | --- | --- | --- |
352
+ | `stamp` | `["src/lab/sections", "src/components"]` | **Site-relative** directories whose `.astro` files get `data-orbytes-src`, so a pin can name the file it came from. |
353
+ | `stampSkip` | `[]` | **Site-relative** `.astro` paths exempt from the stamp's hard failure. A repo-relative path is accepted too, for configs written before 2026-09-22. |
354
+ | `backlogDir` | `"backlog"` | Repo-relative board directory: tickets in `tasks/`, PNGs in `assets/`. |
355
+ | `archiveDir` | `~/.orbytes/feedback-archive` | Canonical screenshot home; the repo copy is a hardlink into it. |
356
+ | `project` | the checkout's folder name | The folder under `archiveDir` this project's screenshots live in. |
357
+ | `shots` | `true` | `false` writes tickets and never loads playwright. |
358
+ | `route` | `"/pin"` | Where the board is served. |
359
+ | `links` | the lab's resolved subpath | The board's corner links. `[]` draws none. |
360
+ | `appId` / `appName` / `icon` | `"orbytes-pin"` / `"Pin"` / `"bug"` | The dev-toolbar app's identity. |
361
+
362
+ Three things about it are worth knowing before they surprise you:
363
+
364
+ - **`route` shadows a host page at the same path.** The board is middleware installed ahead of
365
+ Astro's own request handler, so a site with its own `src/pages/pin.astro` serves the board
366
+ instead — in dev, with nothing on screen to say why. The default stays `/pin` because it is the
367
+ address every document here names; a site that owns that path sets `pin: { route: "/__pin" }`.
368
+ - **`project` defaults to the checkout's directory name**, which two clones of *different* repos
369
+ under the same folder name silently share. One's `pin-007.png` overwrites the other's and both
370
+ boards keep rendering. Set it explicitly where that is possible.
371
+ - **`stamp` throws on a matched `.astro` file with no element to stamp** — a template whose first
372
+ top-level tag is a component, say. That is deliberate (a warned-past file produces tickets with no
373
+ source), and `stampSkip` is the way past it. Point `stamp` at directories you know, not at
374
+ `src/`.
375
+
376
+ ### `orbytes-pin-gallery`
377
+
378
+ The board is live at `/pin` and read fresh on every request, so this binary is only for the file: a
379
+ snapshot that opens with no dev server running.
380
+
381
+ ```sh
382
+ orbytes-pin-gallery # → backlog/gallery.html
383
+ orbytes-pin-gallery --out somewhere.html
384
+ orbytes-pin-gallery --backlog-dir feedback # when the site moved its board
385
+ ```
386
+
387
+ It renders from the same module the live board does and is read-only — a `file://` page has nothing
388
+ listening behind it. It runs in the current working directory, like `orbytes-lab-cull`, so wire it
389
+ as a script in the consumer:
390
+
391
+ ```json
392
+ "scripts": { "pin:gallery": "orbytes-pin-gallery" }
393
+ ```
394
+
395
+ ## What stays yours
396
+
397
+ - **`decorators/`** — a decorator wraps a story in the page shell it will really live in. That
398
+ shell is different in every build, so the package ships none: write your own against your own
399
+ layout.
400
+ - **`cull.json` / `responsive.json`** — data about your components, committed with them.
401
+ - **The build gate** — whether the lab is in this build is the consumer's decision, in the
402
+ consumer's `astro.config.mjs`, and worth asserting with a check of your own.
403
+ - **The head component** — fonts and anything else the site's layout registers.
404
+
405
+ ## Notes
406
+
407
+ - The `tags` a story declares (`export default { component, meta: { tags: [...] } }`) are read
408
+ through `import.meta.glob("/src/**/*.stories.ts")`, so a `directory` outside `src/` gets no tags.
409
+ - Editing the integration or its `.mjs` helpers is not picked up by the dev server's config-change
410
+ restart — they stay in Node's ESM cache. Stop and start the server.