@orbytes/astrolab 0.3.0 → 0.4.0-next.2

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 (82) hide show
  1. package/README.md +261 -116
  2. package/bin/pin-gallery.mjs +53 -19
  3. package/defaults.mjs +72 -20
  4. package/dist/core/virtual-module/virtual-routes.js +12 -1
  5. package/docs/PIN-CONTRACT.md +84 -10
  6. package/docs/PIN.md +117 -37
  7. package/index.d.ts +41 -23
  8. package/index.mjs +46 -91
  9. package/package.json +7 -3
  10. package/src/Home.astro +166 -264
  11. package/src/LabHead.astro +37 -1047
  12. package/src/chrome/ActionsMenu.astro +97 -0
  13. package/src/chrome/ComponentCard.astro +76 -0
  14. package/src/chrome/Icon.astro +21 -0
  15. package/src/chrome/LICENSE-icons +43 -0
  16. package/src/chrome/Nav.astro +131 -0
  17. package/src/chrome/Panel.astro +117 -0
  18. package/src/chrome/Properties.astro +104 -0
  19. package/src/chrome/SectionsTree.astro +128 -0
  20. package/src/chrome/Shell.astro +120 -0
  21. package/src/chrome/Sprite.astro +23 -0
  22. package/src/chrome/StoryView.astro +192 -0
  23. package/src/chrome/Tree.astro +86 -0
  24. package/src/chrome/ViewportControls.astro +173 -0
  25. package/src/chrome/ViewportStage.astro +55 -0
  26. package/src/chrome/fonts/OFL.txt +93 -0
  27. package/src/chrome/fonts/inter-latin-wght-normal.woff2 +0 -0
  28. package/src/chrome/icons.ts +68 -0
  29. package/src/chrome/marks-client.ts +75 -0
  30. package/src/chrome/model.ts +156 -0
  31. package/src/chrome/navbar-client.ts +324 -0
  32. package/src/chrome/params-client.ts +434 -0
  33. package/src/chrome/pins-data.ts +63 -0
  34. package/src/chrome/shell-client.ts +468 -0
  35. package/src/chrome/site-data.ts +230 -0
  36. package/src/chrome/trees.ts +257 -0
  37. package/src/chrome/viewport-client.ts +405 -0
  38. package/src/chrome/views/Assets.astro +125 -0
  39. package/src/chrome/views/Pages.astro +214 -0
  40. package/src/chrome/views/Placeholder.astro +37 -0
  41. package/src/chrome/views/Tasks.astro +79 -0
  42. package/src/core/LICENSE-astrobook +21 -0
  43. package/src/core/lib/components/home.astro +4 -2
  44. package/src/core/lib/pages/story.astro +12 -10
  45. package/src/core/utils/kebab-case.ts +2 -2
  46. package/src/core/virtual-module/virtual-routes.ts +20 -4
  47. package/src/pin/board.mjs +414 -190
  48. package/src/pin/index.mjs +67 -22
  49. package/src/pin/tickets.mjs +6 -5
  50. package/src/pin/toolbar.js +82 -4
  51. package/src/shell/Browse.astro +131 -353
  52. package/src/shell/Viewport.astro +22 -1315
  53. package/src/shell/lab-index.ts +28 -18
  54. package/src/shell/lab-params.ts +113 -6
  55. package/src/shell/live-files.mjs +212 -10
  56. package/src/shell/marks.mjs +17 -41
  57. package/src/ui/components/app.astro +5 -7
  58. package/src/ui/components/preview-layout.astro +17 -0
  59. package/src/ui/components/theme-script.astro +17 -5
  60. package/src/ui/lab.css +3754 -371
  61. package/virtual.d.ts +13 -4
  62. package/bin/lab-cull.mjs +0 -401
  63. package/src/shell/CardGrid.astro +0 -297
  64. package/src/ui/components/build-path.ts +0 -13
  65. package/src/ui/components/build-tree.ts +0 -108
  66. package/src/ui/components/collapse-duration.ts +0 -28
  67. package/src/ui/components/compress-terms.ts +0 -10
  68. package/src/ui/components/dashboard-layout.astro +0 -39
  69. package/src/ui/components/home.astro +0 -65
  70. package/src/ui/components/layout.astro +0 -110
  71. package/src/ui/components/sidebar-button-fullscreen.astro +0 -38
  72. package/src/ui/components/sidebar-button-search.astro +0 -23
  73. package/src/ui/components/sidebar-button-theme.astro +0 -9
  74. package/src/ui/components/sidebar-button.astro +0 -24
  75. package/src/ui/components/sidebar-resize-handle.astro +0 -74
  76. package/src/ui/components/sidebar-search-panel.astro +0 -41
  77. package/src/ui/components/sidebar-search-script.ts +0 -103
  78. package/src/ui/components/sidebar-title.astro +0 -17
  79. package/src/ui/components/sidebar-tree-node.astro +0 -143
  80. package/src/ui/components/sidebar-tree.astro +0 -84
  81. package/src/ui/components/sidebar.astro +0 -29
  82. package/src/ui/components/theme-toggle.astro +0 -63
package/README.md CHANGED
@@ -1,21 +1,24 @@
1
1
  # @orbytes/astrolab
2
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
3
+ A local dev tool for an Astro site, in one package and one integration call. Everything runs on
4
+ the site's own dev server at `/lab`; nothing leaves the machine.
5
+
6
+ **The lab** — every page, section and component in the build, rendered live on a canvas you size,
7
+ with the one thing a component playground normally cannot tell you: **which of these is actually on
8
+ the site right now**, derived from the pages rather than declared. Its engine began as
9
+ [Astrobook](https://github.com/ocavue/astrobook), vendored; its chrome is this package's own.
10
+
11
+ **Tasks the pin board** at `/lab/tasks` click a rendered element on `astro dev`, leave a
12
+ comment, and a markdown ticket plus a screenshot land on local disk for an agent to pick up
13
+ ([docs/PIN.md](./docs/PIN.md), contract at [docs/PIN-CONTRACT.md](./docs/PIN-CONTRACT.md)). Kanban and
14
+ table views in the lab, and a **Comment** button in its navbar that pins an element of the page it
15
+ is previewing. Dev only, and only in a site that is inside a git repository (see
16
+ [Tasks](#tasks--the-pin-board-at-labtasks)); switchable off with `pin: false`. `/pin`, its old
17
+ address, redirects there.
18
+
19
+ It ships mostly **source**. The `.astro` files, and everything under `src/chrome/`, `src/shell/`,
20
+ `src/ui/` and `src/types/`, are compiled by the consumer's own Vite, the way Starlight ships its components —
21
+ which is why the lab picks up the consumer's tokens, fonts and breakpoints in a variant's preview: it
19
22
  renders inside the consumer's pipeline. The one exception is `src/core/`, transpiled to `dist/` by
20
23
  `scripts/build-core.mjs` at `prepack` and `prepare`, because `astro.config.mjs` is loaded by Node's
21
24
  own ESM loader and Node refuses to strip TypeScript under `node_modules`. Without that step an
@@ -24,9 +27,12 @@ installed copy cannot be loaded at all. The script's header comment is the full
24
27
  ## Install
25
28
 
26
29
  ```sh
27
- npm i -D @orbytes/astrolab
30
+ npm i -D -E @orbytes/astrolab
28
31
  ```
29
32
 
33
+ `-E` (`--save-exact`) pins the version: a lab upgrade is a deliberate change to a site, never a
34
+ side effect of `npm install`.
35
+
30
36
  Then add the integration:
31
37
 
32
38
  ```js
@@ -37,18 +43,26 @@ import orbytesLab from "@orbytes/astrolab";
37
43
  export default defineConfig({
38
44
  integrations: [
39
45
  orbytesLab({
40
- css: ["./src/styles/global.css"], // your token and base stylesheets
46
+ css: ["./src/styles/global.css"], // optional: your site's own stylesheet(s), for the previews
41
47
  }),
42
48
  ],
43
49
  });
44
50
  ```
45
51
 
52
+ `css` is optional, and `./src/styles/global.css` is only an example: name whatever stylesheet your
53
+ site's layout imports. A new Astro project has no such file, so either point `css` at the one you
54
+ have or leave the option out. Left out, it is `[]` and the variant previews get no site stylesheet
55
+ at all, only each component's own scoped styles; the lab's chrome never gets it either way. Each
56
+ entry becomes a plain `import` in the preview's layout (a path starting with `.` resolves against
57
+ the project root, anything else as a package), so name only files that exist.
58
+
46
59
  That is the whole configuration. In particular, **do not add an `ssr.noExternal` entry for this
47
60
  package** — the integration adds its own, because the package ships `.astro` files that Vite would
48
61
  otherwise externalise and hand to Node, which cannot read them. A consumer needs nothing. (Two
49
62
  older orbytes repos set it by hand; it is redundant, and copying it from them is copying noise.)
50
63
 
51
- Now write a story. Stories live under `src/lab/`, in one directory per tier:
64
+ Each named export in a stories file is a story; the lab lists them as **Variants**. Now write a
65
+ story. Stories live under `src/lab/`, in one directory per tier:
52
66
 
53
67
  ```
54
68
  src/lab/
@@ -56,12 +70,11 @@ src/lab/
56
70
  components/Button.astro + Button.stories.ts
57
71
  explorations/KeycapButton.astro + KeycapButton.stories.ts
58
72
  decorators/ ← yours, built against YOUR page shell (see below)
59
- cull.json ← written by the lab, commit it
60
73
  responsive.json ← written by the lab, commit it
61
74
  ```
62
75
 
63
76
  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.
77
+ export's name is the variant's label in the lab; its `args` are the component's props.
65
78
 
66
79
  ```ts
67
80
  // src/lab/components/Button.stories.ts
@@ -76,8 +89,17 @@ export const Subtle = { args: { label: "See pricing", variant: "subtle" } };
76
89
  export const Strong = { args: { label: "Book a call", variant: "strong" } };
77
90
  ```
78
91
 
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.
92
+ Then `npm run dev` and open **`http://localhost:4321/lab`**. `subpath` moves it; `directory` moves
93
+ the stories.
94
+
95
+ **Test versions** are published under the `next` tag while the redesign settles:
96
+ `npm i -D -E @orbytes/astrolab@next`. `latest` stays on the last stable release until 1.0.
97
+
98
+ **Upgrading from 0.3.** The mark-for-deletion feature (the cull) is removed, decided 2026-09-24.
99
+ The `orbytes-lab-cull` command is gone, so delete any `lab:cull` script that calls it, and the
100
+ `src/lab/cull.json` it kept. The `cullTier`, `cullDir` and `cullFile` options and a tier's
101
+ `cullable` flag are no longer read: a config that still sets them builds, but they do nothing, and
102
+ a typed config will flag them. Nothing else a 0.3 site configured changes meaning.
81
103
 
82
104
  **Peers.** Astro **7 or newer**, narrowed from `>=5.2.0` on 2026-09-22 when the pin board moved in:
83
105
  the board's dev-toolbar app imports `astro/client/dev-toolbar/apps/utils/highlight.js` and
@@ -167,10 +189,15 @@ than silent.
167
189
 
168
190
  ### What is vendored
169
191
 
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.
192
+ **Astrobook is not a dependency**, and neither is anything it used to pull in. Its core is vendored
193
+ at version `0.13.3` into [`src/core/`](./src/core/), its types into [`src/types/`](./src/types/), and
194
+ what the preview still needs of its UI into [`src/ui/`](./src/ui/) — the rest of that UI was retired
195
+ by the redesign (2026-09-24), whose chrome is this package's own, in [`src/chrome/`](./src/chrome/).
196
+ Nothing outside Astro is installed, and there is nothing to keep in step but this folder.
197
+
198
+ The chrome bundles two third-party assets, each with its licence beside it: **Inter** (SIL OFL,
199
+ [`src/chrome/fonts/`](./src/chrome/fonts/)) and **Lucide** icons (ISC,
200
+ [`src/chrome/LICENSE-icons`](./src/chrome/LICENSE-icons)).
174
201
 
175
202
  Attribution, the upstream commit, and every deliberate divergence from it are recorded in
176
203
  [`src/core/LICENSE-astrobook`](./src/core/LICENSE-astrobook) (MIT, Copyright (c) 2024 ocavue; the
@@ -183,19 +210,19 @@ This package is MIT too; see [LICENSE](./LICENSE).
183
210
  ### The chrome is not styled by the website
184
211
 
185
212
  **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
213
+ and only the rendered variant takes the site's styling. A site that wants a different-looking lab
187
214
  edits this package's source: there is no theme API and no token override, on purpose.
188
215
 
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.
216
+ So the chrome — both sidebar levels, the navbar, every view is drawn by one stylesheet that ships
217
+ with this package, [`src/ui/lab.css`](./src/ui/lab.css), in `--lab-*` tokens that cannot collide with
218
+ a site's own, in Inter, with light and dark themes of its own. It never receives the `css` list or
219
+ the `head` component.
193
220
 
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`.
221
+ The **preview** — the bare variant at `/lab/stories/<id>`, and the site's own pages — gets both,
222
+ because seeing a component as the real site renders it is the whole point. Since the redesign the
223
+ chrome only ever shows a preview **inside an `<iframe>`**, so the two never share a document: no
224
+ chrome page carries a byte of the site's CSS, and the chrome's stylesheet never reaches a variant.
225
+ The preview also keeps its **own** light/dark, separate from the chrome's.
199
226
 
200
227
  ## Options
201
228
 
@@ -205,13 +232,14 @@ Every option is optional.
205
232
  | --- | --- | --- |
206
233
  | `directory` | `"src/lab"` | Where the stories live, relative to the project root. |
207
234
  | `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. |
235
+ | `css` | `[]` | The site's own token and base stylesheets, if you want the previews styled like the site (see [Install](#install)). They reach the variant **previews** only, never the lab's own chrome. |
236
+ | `head` | — | A path to the site's own head component, rendered inside every variant preview's head. This is how fonts arrive: variants never render through the site's layout, so whatever that layout registers has to be repeated here. |
237
+ | `title` | `"Component lab"` | The site's name as the lab shows it the tab title and the home page. |
238
+ | `stagingUrl` | — | The deployed lab's base URL — the real minified build, which a dev server is not. Every card and every component page link to the same variant there. Omit it and those links do not appear. |
212
239
  | `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
240
  | `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`. |
241
+ | `tiers` | `["sections", "components", "explorations"]` | The tiers, in reading order. A tier is the first path segment under `directory`. A tier with no stories files is left out of the menu. |
242
+ | `viewports` | the orbytes scale | The viewport's screen sizes: `design` widths (the device switch), `breakpoints` (the band the Viewport field names, and the presets) and named `devices`. See [Viewports](#viewports). |
215
243
  | `pin` | `true` | The pin board. `false` omits it entirely; an object is the board's own options — see below. |
216
244
 
217
245
  ### Tiers
@@ -223,39 +251,120 @@ what each one *is*:
223
251
  tiers: [
224
252
  { id: "pages", label: "Page sections", responsive: true },
225
253
  "atoms",
226
- { id: "sketches", label: "Sketches", cullable: true },
254
+ { id: "sketches", label: "Sketches" },
227
255
  ]
228
256
  ```
229
257
 
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`.
258
+ - **`responsive`** — this tier holds page sections, laid out `<tier>/<Section>/<Version>/`; the lab
259
+ reads the section and version from those folders whatever the tier is called. Its variants carry
260
+ the responsive mark, and they render at page width (1440×900) in thumbnails and as the viewport's
261
+ default frame; everything else renders on a component stage (960×540). Defaults to true for a
262
+ tier named `sections`.
236
263
 
237
- At most one tier holds each role.
264
+ At most one tier holds the role.
238
265
 
239
- ## What you get
266
+ ### Viewports
240
267
 
241
- **`<subpath>/`** — the home page: a card per tier, then every story as a scaled-down live render.
268
+ ```js
269
+ viewports: {
270
+ design: [
271
+ { label: "Desktop", width: 1440, icon: "monitor" },
272
+ { label: "Phone", width: 390, icon: "phone" }, // icons: monitor, tablet, phone-landscape, phone
273
+ ],
274
+ breakpoints: [{ name: "desktop", min: 1024 }, { name: "mobile", min: 0 }],
275
+ devices: [{ label: "Pixel 8", width: 412, height: 915 }],
276
+ }
277
+ ```
242
278
 
243
- **`<subpath>/browse/<path>`** a folder page per folder in the tree, with a filter box.
279
+ Each key is optional and replaces its default whole. `breakpoints` are lower edges, widest first,
280
+ the last one `0`; the presets offer both sides of every edge (1024 and 1023).
244
281
 
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.
282
+ ## What you get
283
+
284
+ Two sidebar levelsthe menu (Home; Site: Pages, one entry per tier, Assets; Tasks; Support and
285
+ Settings), with the lab's own light/dark switch (a moon) beside the logo, and a panel with a
286
+ breadcrumb, a search box and a tree — and a main area with a navbar over the content. The menu
287
+ shrinks to a 64px icon rail, where the moon sits under the logomark and each group opens as a
288
+ flyout under its own name. Kanban Board and All Tasks have no panel. The panel's tree has no icons
289
+ and reads at 12px.
290
+
291
+ **`<subpath>/`** — Home: counts for pages, each tier, assets and open tasks, and every page's
292
+ mounted components in slot order — what is on the site right now.
293
+
294
+ **`<subpath>/pages/<route>`** — the site's own pages, in a tree nested the way the routes nest, each
295
+ shown on the viewport canvas at its real URL. The navbar is the component page's, less the
296
+ variants: the page's name and route, the eye (below), a menu of the components the page mounts, in
297
+ slot order, and **Reload** and **Open page** beside the viewport controls. `<subpath>/pages` lists
298
+ them all.
299
+
300
+ **`<subpath>/browse/<tier>`** — a tier (Sections, Components, …): its components as cards with a live
301
+ thumbnail, under a title bar with filter chips (All · Live · Used · Unused). A live component's card
302
+ carries a green badge with its slot number; the others carry a *used by N* pill when something live
303
+ uses them, and a section carries its responsive state. On the sections tier the panel's filter
304
+ menu adds responsive state.
305
+
306
+ On the sections tier the panel is the **Sections tree**: one row per section with a slot badge —
307
+ green with its slot number, a grey dash when no page mounts it, violet on the row you are on. The
308
+ mounted sections come first; the rest sit under *Unslotted · on no page*. **Sort by** switches
309
+ between slot number and name (A–Z) and is remembered in this browser.
310
+
311
+ **`<subpath>/dashboard/<story>`** — a component's page. The tree stops at the component, and the
312
+ navbar carries the rest:
313
+
314
+ - **The name, the version and an eye.** Hovering the eye opens the **Properties** panel: where the
315
+ component is live and in which slot (or what uses it, or *Not on any page*), its open and resolved
316
+ pins, the two responsive marks as checkboxes (sections only, see [Marks](#marks)) and its source
317
+ file. Clicking the eye keeps the panel open; Escape or a click elsewhere closes it.
318
+ - **Variants** — always a dropdown ("Default 1 of 3"), whatever the count — and previous / next
319
+ buttons, with `j`/`k` stepping through every variant of the tier.
320
+ - **The device switch and the Viewport field.** The switch picks a design width. The field is the
321
+ frame's width × height, typed in place, and a button naming the band and the zoom ("tablet Fit")
322
+ that opens the presets: Rotate, both sides of every breakpoint edge, the named devices and a zoom
323
+ row (Fit, 25–200%). Drag the frame's right edge, bottom edge or corner to resize it. Fit is the
324
+ default: it scales the frame to fit the stage in both directions, and never past 100%.
325
+ - **Comment**, where the pin board runs — see [Pinning from the lab](#pinning-from-the-lab).
326
+ - **Parameters**, only for a variant that registers some — see [Parameters panel](#parameters-panel).
327
+ - **⋯** — everything that leaves the lab (Open in VS Code, open the bare variant full screen, view
328
+ on staging, copy a link to this exact size), then two sets of switches: **Pins** (Show pins, on;
329
+ Show resolved, off) and **Preview › Dark mode**, which appears only when the framed site has a
330
+ dark mode of its own and switches the preview, not the lab.
331
+
332
+ The navbar is one row when it is 1040px wide or more, and two rows below that, the viewport row
333
+ last. The viewport is in the URL (`?w=390&h=844&z=fit`), so a link names the exact frame, and it
334
+ rides along when you step between variants. `[` `]` step the widths, `r` rotates, `f` fits, `0` is
335
+ 100%.
336
+
337
+ **`<subpath>/assets`** — every image under `src/` and `public/`, its size and dimensions, and which
338
+ files reference it, so "what does nothing use?" is a filter.
339
+
340
+ **`<subpath>/tasks`**, **`<subpath>/tasks/all`** — the pin board, dev only. See below.
249
341
 
250
342
  **`<subpath>/index.json`** — the whole index as a static file: every story with its id, URLs,
251
343
  files, tier/section/version, live slot, used-by list, tags and summary. Scripts and agent briefs
252
344
  read this instead of globbing.
253
345
 
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.
346
+ **`<subpath>/viewport/<story>`** the old viewport configurator's address, now a redirect to the
347
+ component page, kept so old links still land.
348
+
349
+ **Keys.** `⌘B` closes and opens the panel, `⌘⇧B` shrinks the menu to icons, `⌘.` does both,
350
+ `⌘K` searches. With the panel closed, point at the canvas's left edge and the panel slides out
351
+ under the navbar, over the canvas, without resizing the preview.
352
+
353
+ **Astro's dev toolbar is hidden inside the lab's frames**, so a lab page shows one toolbar: its
354
+ own. The site's pages, and a bare variant opened in its own tab, keep theirs.
355
+
356
+ ### Pinning from the lab
357
+
358
+ Where the pin board runs, **Comment** pins an element of the framed preview: click it, click the
359
+ element, and the pin app on Astro's dev toolbar opens its composer. The pick is confined to the
360
+ canvas, so it can never land on the lab's own chrome; Escape stops it. The button stays disabled
361
+ until that toolbar app has started, because it is the one picker and composer the lab uses.
362
+
363
+ Pins already left on the framed page are drawn over the frame at their elements, blue while open and
364
+ grey once resolved; a pin whose selector finds nothing on the page, or more than one thing, is not
365
+ drawn. Clicking a pin, or the pin row in the Properties panel, opens All Tasks. ⋯ › Pins decides
366
+ what is drawn — Show pins (on) and Show resolved (off) — and the choice is remembered in this
367
+ browser. On the site's own pages, outside the lab, pinning goes through the dev toolbar as before.
259
368
 
260
369
  **An unconditional `noindex`** on every lab page. Lab pages do not render through the site's
261
370
  layout, so they never inherit its staging `noindex` — this one is not gated on the environment, on
@@ -265,47 +374,26 @@ purpose. Keep your own build gate as well: two mechanisms, because the rule is a
265
374
 
266
375
  A component is live when a page under `src/pages/` imports it **and** mounts it as a tag. Every
267
376
  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.
377
+ document order. Nothing is configured, so the slot badge cannot go stale.
269
378
 
270
379
  ## Marks
271
380
 
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.
381
+ One JSON file under `directory`, **`responsive.json`**, written by a **dev-only** API
382
+ (`PUT /__lab/responsive`) and meant to be committed. It holds `{ done, approved }` per section
383
+ version, ticked in the component's **Properties** panel (the eye beside its name). Responsive work
384
+ is approval-gated and neither fact is readable from the code, so both are ticked by hand.
283
385
 
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.
386
+ The API refuses anything outside the responsive tier and anything that is not a stories file on
387
+ disk — with the offender and the reason. In a build there is no dev server, so there is no write
388
+ path at all: the boxes stay disabled and show the state the page was built with.
303
389
 
304
390
  ## Parameters panel
305
391
 
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.
392
+ A component with something to tune declares it and renders no UI of its own. The panel is a drawer
393
+ over the right edge of the component page's canvas, opened from the navbar's **Parameters** button,
394
+ which exists only for a variant that registered something — the bare variant page shows nothing.
395
+ The drawer never resizes the canvas, because that would change the preview's width and trip its
396
+ breakpoints; whether it is open, and which groups are collapsed, is remembered in this browser.
309
397
 
310
398
  ```ts
311
399
  import { registerLabParams } from "@orbytes/astrolab/params";
@@ -315,6 +403,7 @@ const panel = registerLabParams({
315
403
  title: "hero shader",
316
404
  controls: [
317
405
  { kind: "range", id: "speed", label: "Speed", min: 0, max: 4, step: 0.05, value: 1, unit: "×" },
406
+ { kind: "number", id: "headlineSize", label: "Headline size", value: 72, unit: "px" },
318
407
  { kind: "toggle", id: "grain", label: "Grain", value: true },
319
408
  ],
320
409
  apply(values) { /* push them into whatever they configure */ },
@@ -324,47 +413,102 @@ const values = panel.values; // LIVE — mutated in place, safe to hold
324
413
  panel.status("WebGL2 · 0.8 ms/frame");
325
414
  ```
326
415
 
416
+ The control kinds are `range` (a slider), `number` (below), `toggle`, `select` and `color`.
417
+
327
418
  `apply` is called once at registration with the restored values, so it must tolerate running before
328
419
  the thing it configures exists. Values persist per group in `localStorage`. Reset, Copy settings,
329
420
  the readouts and the collapse are the panel's job, not the component's.
330
421
 
331
- Exported types: `LabParamControl`, `LabParamGroup`, `LabParamHandle`, `LabParamValue`,
332
- `LabParamValues`, `LabParamEntry`.
422
+ ### Sizes are never sliders
333
423
 
334
- ## The pin board at `/pin`
424
+ **Decided 2026-09-24.** A parameter that is a size — a font size, a tracking, a line height, a width,
425
+ an offset, a radius — is `kind: "number"`: a field to type the value into, joined to a select of the
426
+ units it can be expressed in. Sliders stay for unitless tuning values (speed, grain, scale,
427
+ opacity).
335
428
 
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.
429
+ ```ts
430
+ { kind: "number", id, label, value, unit, units?, step?, min?, max?, note? }
431
+ ```
432
+
433
+ - `value` is the bare default number (`72`) and `unit` its default unit (`"px"`, or `""` for none).
434
+ - `units` is what the select offers. It defaults to `LAB_UNITS.size` — px · rem · em · vw · vh · %.
435
+ `LAB_UNITS.tracking` (em · px) and `LAB_UNITS.lineHeight` (none · px) are there too, or pass a
436
+ list of your own; the default `unit` is added if the list lacks it.
437
+ - `step` is what ↑ / ↓ add (1 by default); `min` and `max` bound typed and restored values alike.
438
+
439
+ `apply` receives **one CSS-ready string** per number control, the number and the unit joined —
440
+ `"72px"`, `"12.5rem"`, or `"1.05"` when the unit is `""` — so it drops straight into a style:
441
+
442
+ ```ts
443
+ el.style.setProperty("--headline-size", String(values.headlineSize)); // "72px"
444
+ ```
445
+
446
+ `parseLabLength("72px")` gives `{ value: 72, unit: "px" }` back when the number itself is wanted,
447
+ and `null` for anything that is not one number and an optional unit. Changing the unit keeps the
448
+ number and changes what it means: the panel never converts between units, because px → rem
449
+ depends on a root size it cannot know. A size that used to be a `range` can become a `number`
450
+ without losing its saved tuning — a stored bare number is read in the default unit.
451
+
452
+ Exported from `@orbytes/astrolab/params`: `registerLabParams`, `LAB_UNITS`, `parseLabLength`,
453
+ `formatLabLength` (its inverse), `labUnitsOf`, and the types `LabParamControl`, `LabParamGroup`,
454
+ `LabParamHandle`, `LabParamValue`, `LabParamValues`, `LabParamEntry`.
455
+
456
+ ## Tasks — the pin board at `/lab/tasks`
457
+
458
+ Dev only and on by default. Inside the lab it is two views in the lab's own chrome — **Kanban
459
+ Board** at `<subpath>/tasks` and **All Tasks**, a sortable table, at `<subpath>/tasks/all` — with no
460
+ panel beside them: the board's own chips filter by status. `/pin`, its old address, redirects there.
461
+ Pinning from inside the lab is the navbar's Comment button ([Pinning from the
462
+ lab](#pinning-from-the-lab)). Everything the board does is in [docs/PIN.md](./docs/PIN.md); what
463
+ matters at the integration boundary is here.
464
+
465
+ **It needs the site to be inside a git repository.** Every ticket path is stored relative to the
466
+ repository root: the Astro project's folder, or the nearest one above it, that holds `.git`.
467
+ With none, the board switches itself off for that run and says so only as a warning in the dev
468
+ server's log; the lab is unaffected. Nothing on screen says why: `<subpath>/tasks`,
469
+ `<subpath>/tasks/all` and `/pin` answer 404, the Tasks group and the Comment button are absent,
470
+ there is no pin app on the dev toolbar and no source stamp. Run `git init` (then restart the dev
471
+ server) to turn it on, or pass `pin: false` to silence the warning.
339
472
 
340
473
  ```js
341
474
  orbytesLab({
342
475
  css: ["./src/styles/global.css"],
343
476
  pin: {
344
- stamp: ["src/lab/sections", "src/components"], // site-relative
477
+ // site-relative; this adds the lab's components tier to the default
478
+ stamp: ["src/lab/sections", "src/lab/components", "src/components"],
345
479
  project: "acme-website", // the screenshot archive folder
346
480
  },
347
481
  })
348
482
  ```
349
483
 
484
+ **Which files a pin can name.** A pin names the source file of the nearest element that carries a
485
+ `data-orbytes-src` attribute, and `stamp` decides which `.astro` files get one. The default is
486
+ `src/lab/sections` and `src/components` — **not** `src/lab/components` or `src/lab/explorations`,
487
+ where the layout above puts components — so a pin on one of those, in its own variant, records
488
+ its source as `unresolved` (its selector, screenshot and HTML are still in the ticket). Rendered
489
+ inside a stamped section, the same component's pin names the section's file. Add the directories you
490
+ want, as in the example: `stamp` replaces the default rather than adding to it, and it does not
491
+ follow `directory` if you moved the stories. Before widening it, read the last bullet below: a
492
+ template in a stamped folder with no element to stamp (its first top-level tag a component, say)
493
+ throws, and needs a `stampSkip` entry.
494
+
350
495
  | Option | Default | What it does |
351
496
  | --- | --- | --- |
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. |
497
+ | `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. Replaces the default whole; see above for `src/lab/components`. |
353
498
  | `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
499
  | `backlogDir` | `"backlog"` | Repo-relative board directory: tickets in `tasks/`, PNGs in `assets/`. |
355
500
  | `archiveDir` | `~/.orbytes/feedback-archive` | Canonical screenshot home; the repo copy is a hardlink into it. |
356
501
  | `project` | the checkout's folder name | The folder under `archiveDir` this project's screenshots live in. |
357
502
  | `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. |
503
+ | `route` | `"<subpath>/tasks"` | Where the board is served. On its own, without the lab, `"/pin"`. |
504
+ | `links` | the lab's resolved subpath | The standalone board's corner links. `[]` draws none. |
360
505
  | `appId` / `appName` / `icon` | `"orbytes-pin"` / `"Pin"` / `"bug"` | The dev-toolbar app's identity. |
361
506
 
362
507
  Three things about it are worth knowing before they surprise you:
363
508
 
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" }`.
509
+ - **`/pin` still answers, in dev, with a redirect.** It is middleware installed ahead of Astro's own
510
+ request handler, so a site with its own `src/pages/pin.astro` gets the redirect instead of its
511
+ page — in dev only. Build output is unaffected.
368
512
  - **`project` defaults to the checkout's directory name**, which two clones of *different* repos
369
513
  under the same folder name silently share. One's `pin-007.png` overwrites the other's and both
370
514
  boards keep rendering. Set it explicitly where that is possible.
@@ -375,8 +519,8 @@ Three things about it are worth knowing before they surprise you:
375
519
 
376
520
  ### `orbytes-pin-gallery`
377
521
 
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.
522
+ The board is live at `/lab/tasks` and read fresh on every request, so this binary is only for the
523
+ file: a snapshot that opens with no dev server running.
380
524
 
381
525
  ```sh
382
526
  orbytes-pin-gallery # → backlog/gallery.html
@@ -385,8 +529,8 @@ orbytes-pin-gallery --backlog-dir feedback # when the site moved its board
385
529
  ```
386
530
 
387
531
  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:
532
+ listening behind it. It runs in the current working directory, so wire it as a script in the
533
+ consumer:
390
534
 
391
535
  ```json
392
536
  "scripts": { "pin:gallery": "orbytes-pin-gallery" }
@@ -397,7 +541,7 @@ as a script in the consumer:
397
541
  - **`decorators/`** — a decorator wraps a story in the page shell it will really live in. That
398
542
  shell is different in every build, so the package ships none: write your own against your own
399
543
  layout.
400
- - **`cull.json` / `responsive.json`** — data about your components, committed with them.
544
+ - **`responsive.json`** — data about your components, committed with them.
401
545
  - **The build gate** — whether the lab is in this build is the consumer's decision, in the
402
546
  consumer's `astro.config.mjs`, and worth asserting with a check of your own.
403
547
  - **The head component** — fonts and anything else the site's layout registers.
@@ -405,6 +549,7 @@ as a script in the consumer:
405
549
  ## Notes
406
550
 
407
551
  - 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.
552
+ from the stories file's text, not imported, so write them as string literals: a tag built at
553
+ runtime is not seen.
409
554
  - Editing the integration or its `.mjs` helpers is not picked up by the dev server's config-change
410
555
  restart — they stay in Node's ESM cache. Stop and start the server.