@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/docs/PIN.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # The pin board
2
2
 
3
3
  Click a rendered element on `astro dev`, leave a comment, and get a markdown ticket plus a PNG on
4
- local disk for a Claude Code agent to pick up. Then work the tickets on a kanban at `/pin`, on the
5
- same dev server. No cloud, no API key, no MCP, no second process.
4
+ local disk for a Claude Code agent to pick up — from the lab's navbar (**Comment**, on the page it is
5
+ previewing) or from Astro's dev toolbar on the site's own pages. Then work the tickets on a kanban
6
+ at `/lab/tasks`, in the lab, on the same dev server. No cloud, no API key, no MCP, no second
7
+ process. It needs the site to be inside a git repository (› *Wiring*).
6
8
 
7
9
  It is for a **solo pass** — your own review of a site in dev, before anyone else sees it. It does
8
10
  not sync with whatever tool you use for review rounds with other people, and is not meant to
@@ -24,7 +26,7 @@ own port (`backlog browser`, 6420). That is the whole reason it is gone — the
24
26
 
25
27
  | Stop | Do instead |
26
28
  |---|---|
27
- | `backlog browser` | `npm run dev`, then `/pin` |
29
+ | `backlog browser` | `npm run dev`, then `/lab/tasks` |
28
30
  | `backlog task edit <n> …` on a pin ticket | drag the card, or use the status menu in its detail panel |
29
31
 
30
32
  `backlog task edit` is the original defect, not an alternative. Any write through backlog.md
@@ -33,7 +35,7 @@ pin ticket that erases `dispatch`, `source`, `selector`, `rect`, `shot` and `out
33
35
  why the pin fields live in a fenced block in the body, and why `/pin` was read-only until
34
36
  2026-09-22:
35
37
  a second writer of these files would have rebuilt the bug. With backlog.md out of the loop there
36
- is no second writer, so `/pin` writes them correctly by construction (› *The write path*).
38
+ is no second writer, so the board writes them correctly by construction (› *The write path*).
37
39
 
38
40
  If backlog.md is still installed globally, this does not uninstall it.
39
41
  A `backlog/config.yml` left behind in a repo that used backlog.md is a record and nothing reads it
@@ -78,20 +80,34 @@ isCancelled(t.status); // the archive test
78
80
 
79
81
  `isOpen()` is *not `Resolved` and not `Cancelled`* — one definition, shared by this board's chips
80
82
  and the in-page panel's Open tab, so the two cannot disagree by one ticket. `Cancelled` appears in
81
- neither tab: it is reachable on `/pin` only.
82
-
83
- ## The board at `/pin`
83
+ neither tab: it is reachable on the board only.
84
+
85
+ ## The board at `/lab/tasks`
86
+
87
+ **In the lab since 2026-09-24** (decided that day: one URL, everything under `/lab`). The board is
88
+ two views in the lab's own chrome — **Kanban Board** at `/lab/tasks` and **All Tasks**, a sortable
89
+ table of every ticket, newest first, at `/lab/tasks/all` — with no second-level panel beside them
90
+ (decided 2026-09-24: the board's own status chips already filter, so a status list beside it only
91
+ took width from the columns); `/lab/tasks?status=Ready%20for%20review` opens the board filtered to
92
+ one column.
93
+ `/pin`, the board's address until then, redirects there. Both views are Astro pages the pin half
94
+ injects in `astro dev` only, rendered from the same markup, stylesheet and script as the standalone
95
+ file (`boardParts` in [`src/pin/board.mjs`](../src/pin/board.mjs)); the stylesheet is scoped with
96
+ `@scope (.pin-board)` so it cannot restyle the chrome around it, and it takes the lab's palette and
97
+ dark mode inside the lab.
84
98
 
85
99
  Six columns in the order above, read fresh from `backlog/tasks/` on every request. No build step,
86
- no regeneration; a refresh is the reload.
100
+ no regeneration; a refresh is the reload. In the lab the columns keep a fixed width and the row
101
+ scrolls sideways, rather than wrapping into a stack beside two sidebars.
87
102
 
88
103
  - **Ready for review is drawn to be seen**, because it is the human review queue and the whole
89
104
  reason the model changed — violet rule, violet dot, *waiting on you* under the name, a tinted panel
90
105
  with a filled count **only when it actually holds something**. An empty queue stays quiet; one
91
106
  that shouts at nothing teaches you to stop looking at it. Its count also leads the top bar.
92
107
  - **Cancelled is drawn to be ignored**: a narrow dashed rail rather than a sixth of the wall, its
93
- cards desaturated until hovered. Still a real drop target, and still the only place a cancelled
94
- ticket can be seen at all — an archive nobody can open is a delete.
108
+ cards desaturated until hovered. Still a real drop target. This rail, the All Tasks table (which
109
+ lists every ticket) and the gallery file are the only places a cancelled ticket can be seen at
110
+ all — an archive nobody can open is a delete.
95
111
  - Every column carries a one-line gloss under its name (*agents start here*, *waiting on you*),
96
112
  because six nouns with no explanation is a vocabulary you have to be told once and remember.
97
113
  - **A card** carries its screenshot, id, title, dispatch mode, priority and source file, plus the
@@ -110,27 +126,51 @@ printed `0 ticket(s)` until 2026-09-22). It renders from the same module and is
110
126
  board is writable exactly when `renderBoard` is given an endpoint, and a `file://` page has nothing
111
127
  listening behind it.
112
128
 
113
- **The board's corner links** are an option (`links`), not the hardcoded `/` and `/lab` they were
114
- until 2026-09-22. Through `orbytesLab()` they are filled in from the lab's own resolved `subpath`,
115
- so the "Lab" link is derived from the value the routes are injected at rather than agreeing with it
116
- by luck.
129
+ **The standalone board's corner links** are an option (`links`), not the hardcoded `/` and `/lab`
130
+ they were until 2026-09-22. Inside the lab there are none — the lab's own menu is right there.
131
+
132
+ ## Pinning from inside the lab
133
+
134
+ Every preview in the lab is an `<iframe>`, and Astro's dev toolbar is hidden inside the lab's
135
+ frames, so a lab page has one toolbar — its own — and one pin app. The lab drives that app rather
136
+ than carrying a second picker:
137
+
138
+ - **Comment**, in the navbar of a component page and of a page view, starts a pick confined to the
139
+ canvas: only the framed page can be picked, never the lab's chrome around it. Escape stops the
140
+ pick and turns Comment off; a comment already typed into the composer is kept, never discarded.
141
+ The composer that follows is the pin app's own window on the dev toolbar. The button is disabled
142
+ until the pin app has started, which it does when the dev toolbar is idle.
143
+ - **The pins** already left on the framed page are drawn over the frame at their elements: blue
144
+ while open, grey once resolved, numbered in the order they were made, cancelled ones never. A
145
+ ticket whose selector finds nothing on that page, or more than one thing, draws no pin rather
146
+ than a guessed one. A pin, and the pin row in the component's Properties panel, open All Tasks.
147
+ - **⋯ › Pins** holds two switches, Show pins (on) and Show resolved (off), remembered per browser
148
+ in `localStorage` (`lab-pins-show`, `lab-pins-resolved`).
149
+
150
+ The list of pins is the pin app's own, re-read after every write, so a new pin appears as soon as
151
+ its ticket lands. How the lab and the app talk is in [the contract](./PIN-CONTRACT.md) › *The lab's
152
+ channel*. On the site's own pages, outside `/lab`, the dev toolbar's pin app works exactly as it
153
+ always did.
117
154
 
118
155
  ## The write path
119
156
 
120
157
  One endpoint, `POST <route>/api/ticket`, registered in `astro:server:setup` — which does not exist
121
158
  in a build, so it cannot reach `dist/`.
122
159
 
123
- **`route` shadows a host page at the same path.** The board and this endpoint are middleware,
124
- installed ahead of Astro's own request handler, so a site with its own `src/pages/pin.astro` gets
125
- the board instead of its page — in dev only, and with nothing on screen to explain it. The default
126
- stays `/pin`: it is the address every document here names. A site that owns
127
- that path sets `pin: { route: "/__pin" }`.
160
+ **The endpoint, the screenshots and the `/pin` redirect are middleware**, installed ahead of
161
+ Astro's own request handler, so they shadow a host page at the same path — in dev only. Through
162
+ `orbytesLab()` the route is `<subpath>/tasks`, which no site page is likely to be; the one exposed
163
+ address is `/pin`, which redirects. Setting `route` does not free it: inside the lab `/pin`
164
+ redirects to `route` whatever that is, and a `route` of `/pin` puts the Tasks views there instead,
165
+ so no `route` leaves a site's own `/pin` page alone in dev — `pin: false` does. On its own,
166
+ without the lab, the pin half still serves its standalone board at `route`, default `/pin`, and
167
+ there `pin: { route: "/__pin" }` moves it off a site page.
128
168
 
129
169
  ```json
130
170
  { "id": "PIN-004", "status": "Ready for review", "expect": { "status": "In Progress" } }
131
171
  ```
132
172
 
133
- `updateTicket` (`src/board.mjs`) rewrites **only** the frontmatter lines it is changing, plus
173
+ `updateTicket` (`src/pin/board.mjs`) rewrites **only** the frontmatter lines it is changing, plus
134
174
  `updated_date`. It never serialises: the file is split by index, and the slice from the closing
135
175
  `---` onward — the comment, the image and the fenced ```yaml pin block — is the same substring on
136
176
  the way out as on the way in. That identity is asserted on every write, and the write is a temp
@@ -172,7 +212,7 @@ Nothing is wired separately: the board comes with the lab, through the one integ
172
212
  already configures.
173
213
 
174
214
  ```js
175
- import orbytesLab from "./packages/astro-lab/index.mjs";
215
+ import orbytesLab from "@orbytes/astrolab";
176
216
 
177
217
  const includeLab =
178
218
  process.argv.includes("dev") || process.env.PUBLIC_DEPLOY_ENV === "staging";
@@ -188,6 +228,15 @@ export default defineConfig({
188
228
  gates itself on `command === "dev"`, so including the lab in a staging build registers it and it
189
229
  does nothing there.
190
230
 
231
+ **The site must be inside a git repository.** Every path in a ticket is repo-relative, and the root
232
+ is the Astro project's folder or the nearest one above it that holds `.git`. With none, the board
233
+ stands down for that run instead of taking the dev server with it (measured 2026-09-22: a freshly
234
+ scaffolded Astro project, which has no git yet, could not run `astro dev` at all before the board
235
+ learned to stand down). The only sign is a warning in the dev server's log. `<route>`,
236
+ `<route>/all` and `/pin` answer 404, the lab shows no Tasks group and no Comment button, and there
237
+ is no pin app on the dev toolbar and no source stamp. `git init` and a restart turn it on;
238
+ `pin: false` silences the warning.
239
+
191
240
  `playwright` is an **optional** peer and is loaded by dynamic import inside the screenshotter, not
192
241
  at module scope — a site without it loads its config, runs the lab and writes tickets without
193
242
  pictures. Install it (`npm i -D playwright && npx playwright install chromium`) for the screenshots,
@@ -199,14 +248,14 @@ Pass these as `orbytesLab({ pin: { … } })`.
199
248
 
200
249
  | Option | Default | What it does |
201
250
  |---|---|---|
202
- | `stamp` | `["src/lab/sections", "src/components"]` | **Site-relative** directories whose `.astro` files get `data-orbytes-src`. |
251
+ | `stamp` | `["src/lab/sections", "src/components"]` | **Site-relative** directories whose `.astro` files get `data-orbytes-src`. Replaces the default whole, and does not follow the lab's `directory`. `src/lab/components` and `src/lab/explorations` are not in it — see *The source attribute*. |
203
252
  | `stampSkip` | `[]` | **Site-relative** `.astro` paths exempt from the stamp's hard failure. See below. A repo-relative path is accepted too — until 2026-09-22 that was the only spelling that worked, while `stamp` beside it was site-relative, so the exemption silently lapsed wherever the Astro project was not the repo root. |
204
253
  | `backlogDir` | `"backlog"` | Repo-relative board directory: tickets in `tasks/`, PNGs in `assets/`. |
205
254
  | `archiveDir` | `~/.orbytes/feedback-archive` | Canonical screenshot home; the repo copy is a hardlink into it. |
206
255
  | `project` | the checkout's folder name | The folder under `archiveDir` this project's screenshots live in. Two clones of **different** repos under one folder name share an archive silently, one overwriting the other's `pin-007.png` — set it where that is possible. |
207
256
  | `shots` | `true` | `false` writes tickets and takes no screenshots. |
208
- | `route` | `"/pin"` | Where the board is served. `<route>/assets/*` serves its screenshots, `<route>/api/ticket` is the write endpoint. **It shadows a host page at that path** — see *The write path*. Trailing slashes are stripped. The toolbar app reads the resolved value too (it fetches the ticket list from it and its **Board ↗** button opens it), through the virtual module below — until 2026-09-22 it carried a literal `/pin`, so setting this left the panel fetching a path nothing serves. |
209
- | `links` | the lab's resolved subpath | The board's corner links, `[{ href, label }]`. `[]` draws none. |
257
+ | `route` | `"<subpath>/tasks"` (`"/pin"` without the lab) | Where the board is served. `<route>/assets/*` serves its screenshots, `<route>/api/ticket` is the write endpoint; in the lab, `<route>` and `<route>/all` are the two views. Trailing slashes are stripped. The toolbar app reads the resolved value too (it fetches the ticket list from it and its **Board ↗** button opens it), through the virtual module below — until 2026-09-22 it carried a literal `/pin`, so setting this left the panel fetching a path nothing serves. |
258
+ | `links` | the lab's resolved subpath | The standalone board's corner links, `[{ href, label }]`. `[]` draws none. |
210
259
  | `appId` / `appName` / `icon` | `"orbytes-pin"` / `"Pin"` / `"bug"` | The dev-toolbar app's identity. |
211
260
 
212
261
  ## What a ticket looks like
@@ -306,26 +355,33 @@ Two rules now keep a selector off that ground, both in
306
355
  - **A class naming a runtime condition never enters a selector** (`isTransientClass`,
307
356
  `extendsSiblingClass`) — `swiper-slide-active` is on slide 1 now and slide 2 after a scroll.
308
357
 
309
- Neither is complete, and the field is what makes the remainder visible. `src/shot.mjs` already
358
+ Neither is complete, and the field is what makes the remainder visible. `src/pin/shot.mjs` already
310
359
  loads the page **fresh in a real browser** to take the screenshot; before the shutter it asks that
311
360
  page how many nodes the ticket's selector finds, and writes the answer back into the pin block:
312
361
 
313
362
  | Value | Means |
314
363
  |---|---|
315
364
  | `1` | healthy — the selector names exactly this element on a fresh load |
316
- | `0` | dead — work the ticket from `source`, the screenshot and `outer_html` |
365
+ | `0` | dead — flagged *needs a human* on the board; a person works it from `source`, the screenshot and `outer_html` |
317
366
  | `2`+ | ambiguous — the selector names several elements |
318
367
  | `pending` | never tested — shots are off, or the screenshot failed |
319
368
 
320
369
  Anything but `1` is said in the dev log and in the toolbar panel. A ticket whose selector died is
321
370
  **still a good ticket** — it is labelled, never discarded. The defect was the silence.
322
371
 
372
+ On the board, `0` and `2`+ put a banner on the card, count towards *needing a human* in the
373
+ summary, and match the *Needs a human* filter. The banner's note for `0` says an agent cannot
374
+ locate the element: its selector cannot, and the rest of the ticket is intact for a person to work
375
+ from. **The flag is a label, not a gate.** It changes no status, and neither `isAgentReady()` nor
376
+ `listTickets()` reads `selector_matches` — a flagged ticket left in `Ready for agent` is still
377
+ handed to an agent. Only its status keeps an agent off it.
378
+
323
379
  ## Screenshots
324
380
 
325
381
  One chromium for the whole dev session, launched lazily on the first pin and kept warm, closed
326
- when the dev server closes. Measured on an Apple-silicon laptop, 2026-09-21: the launch itself is
327
- ~98 ms, and a full capture — navigate, freeze motion, settle every image, hold the scroll position,
328
- clip — is ~1.34 s warm against ~1.42 s cold.
382
+ when the dev server closes. What a shot costs has been measured three times, each timing a
383
+ different span; the figures, their dates and what each one timed are in
384
+ [the contract](./PIN-CONTRACT.md) › *Screenshots*.
329
385
 
330
386
  You never wait for any of it. The ticket is written and `orbytes-pin:created` replied on the
331
387
  same synchronous tick; the screenshot follows and sends `orbytes-pin:shot` when it lands. A ticket
@@ -349,6 +405,22 @@ file under `stamp`, and the picker walks up from the clicked node to the nearest
349
405
  it. No ancestor found → `source` is `unresolved`, and the CSS selector plus `outer_html` still
350
406
  make the ticket actionable.
351
407
 
408
+ **The default does not cover every tier.** It stamps `src/lab/sections` and `src/components`, so a
409
+ component kept in the lab's own `src/lab/components/` (or `src/lab/explorations/`) and pinned on its
410
+ own variant has no stamped ancestor, and its tickets say `unresolved`. Name the folder to include it
411
+ — `stamp` replaces the default, so restate what you keep:
412
+
413
+ ```js
414
+ orbytesLab({
415
+ pin: {
416
+ stamp: ["src/lab/sections", "src/lab/components", "src/components"],
417
+ },
418
+ })
419
+ ```
420
+
421
+ Widening `stamp` widens the hard failure below with it, so check the folder for templates with no
422
+ element to stamp first.
423
+
352
424
  The attribute is inserted immediately after the tag name (`<section` → `<section
353
425
  data-orbytes-src="…"`), so no attribute value, expression, spread or self-closing slash can be
354
426
  mangled and no line number moves.
@@ -374,14 +446,17 @@ orbytesLab({
374
446
 
375
447
  ## The browser ↔ server channel
376
448
 
377
- Astro's dev toolbar, over the existing Vite HMR socket. No HTTP endpoint, no port, no CORS.
449
+ Creating a ticket goes over Astro's dev toolbar, on the existing Vite HMR socket — no HTTP endpoint,
450
+ no port, no CORS. Changing a ticket's status is the one HTTP write, from the board and from the
451
+ panel's own menus alike: `POST <route>/api/ticket`, same-origin, on the dev server already
452
+ running.
378
453
 
379
454
  | Direction | Event | Payload |
380
455
  |---|---|---|
381
456
  | browser → server | `orbytes-pin:create` | `{ comment, dispatch, source, selector, url, viewport, scroll, rect, outerHTML }` |
382
457
  | server → browser | `orbytes-pin:created` | `{ id, file, shot, error }` — all four keys, always |
383
458
  | server → browser | `orbytes-pin:shot` | `{ id, shot, selectorMatches, error }` |
384
- | board → server | `POST <route>/api/ticket` | `{ id, status?, priority?, expect? }` → `{ ok, id, status, priority, file, changed, card }` |
459
+ | board or panel → server | `POST <route>/api/ticket` | `{ id, status?, priority?, expect? }` → `{ ok, id, status, priority, file, changed, card }` |
385
460
 
386
461
  On failure the reply is `{ id: null, file: null, shot: null, error: "why, in one plain sentence" }`.
387
462
 
@@ -410,10 +485,12 @@ window, so clicking it again focuses that tab instead of opening another.
410
485
 
411
486
  1. `astro:config:setup` returns early unless the Astro command is `dev` — no plugin, no toolbar
412
487
  app, no stamp.
413
- 2. Both Vite plugins carry `apply: "serve"`, so they cannot load in a build even if they were
488
+ 2. All three Vite plugins — the source stamp, the toolbar's config module and the one that closes
489
+ the browser — carry `apply: "serve"`, so they cannot load in a build even if they were
414
490
  registered.
415
- 3. `astro:server:setup`, where every write lives — the ticket writer, the board and the
416
- `/pin/api/ticket` endpoint — does not run in a build at all.
491
+ 3. `astro:server:setup`, where every write lives — the ticket writer, the screenshots and the
492
+ `<route>/api/ticket` endpoint — does not run in a build at all. The lab's two Tasks views are
493
+ injected past assertion 1, so they exist in `astro dev` and in no build.
417
494
  4. That hook returns early anyway when `astro:config:setup` stood down, so a host that runs the
418
495
  hooks in an order this package did not choose still gets nothing.
419
496
 
@@ -422,8 +499,11 @@ The first one matters more since the merge than it did before: the lab is delibe
422
499
  in a build for the first time, and `command === "dev"` is what makes that a no-op.
423
500
 
424
501
  Verified by running a real `npm run build` and grepping `dist/` for `orbytes-pin`, `data-orbytes-src`
425
- and `/pin/api`: no hit.
502
+ and `/pin/api`: no hit. Re-checked 2026-09-24 on a staging build of the testbed, the case where the
503
+ pin half is registered: no hit for those three or for `/tasks/api`, and no `lab/tasks/` page.
426
504
 
427
505
  No path this package emits into served HTML or into a ticket is absolute. The repo root is found
428
- by walking up from the Astro config root to the first directory containing `.git`, and it throws
429
- rather than guess.
506
+ by walking up from the Astro config root to the first directory containing `.git`, and
507
+ `findRepoRoot` throws rather than guess. The integration catches that throw, warns, and stands down
508
+ for the run (› *Wiring*) — a library that guesses writes files where nobody asked, and an
509
+ integration that rethrows takes the dev server down.
package/index.d.ts CHANGED
@@ -11,11 +11,6 @@ export interface LabTierOption {
11
11
  * thumbnails and in the viewport configurator. Defaults to true for a tier named "sections".
12
12
  */
13
13
  responsive?: boolean;
14
- /**
15
- * This tier's stories may be marked for deletion in the sidebar and removed by
16
- * `orbytes-lab-cull`. Defaults to true for a tier named "explorations".
17
- */
18
- cullable?: boolean;
19
14
  }
20
15
 
21
16
  export interface OrbytesLabOptions {
@@ -23,14 +18,17 @@ export interface OrbytesLabOptions {
23
18
  directory?: string;
24
19
  /** Where the lab is served. Default `"/lab"`. */
25
20
  subpath?: string;
26
- /** The consumer's token/stylesheet files, as Astrobook takes them. Default `[]`. */
21
+ /**
22
+ * The consumer's token/stylesheet files. They reach the PREVIEW only — the bare story each
23
+ * component page shows in its canvas — never the lab's own chrome. Default `[]`.
24
+ */
27
25
  css?: string[];
28
26
  /**
29
- * A path to the consumer's own head component, rendered inside the lab's head — how fonts and
30
- * anything else site-specific reach lab pages. Relative to the project root.
27
+ * A path to the consumer's own head component, rendered inside every story preview's head — how
28
+ * fonts and anything else site-specific reach the previews. Relative to the project root.
31
29
  */
32
30
  head?: string;
33
- /** The lab's name, shown in the dashboard. Default `"Component lab"`. */
31
+ /** The site's name as the lab shows it (the tab title, the home page). Default `"Component lab"`. */
34
32
  title?: string;
35
33
  /**
36
34
  * The deployed lab's base URL — the real minified build, which a dev server is not. Every card
@@ -49,17 +47,35 @@ export interface OrbytesLabOptions {
49
47
  feedbucketKey?: string;
50
48
  /**
51
49
  * The tiers, in reading order. Default `["sections", "components", "explorations"]`; a tier
52
- * named "sections" takes the responsive role and one named "explorations" the cull role unless
53
- * the entry says otherwise.
50
+ * named "sections" takes the responsive role unless the entry says otherwise.
54
51
  */
55
52
  tiers?: (string | LabTierOption)[];
53
+ /**
54
+ * The screen sizes the viewport offers. Every key is optional; an omitted one keeps the default
55
+ * (the orbytes scale).
56
+ */
57
+ viewports?: {
58
+ /**
59
+ * The widths the design is drawn at — the navbar's device switch, in order. `icon` is one of
60
+ * `monitor`, `tablet`, `phone-landscape`, `phone`.
61
+ * @default 1440 desktop · 834 tablet · 550 phone landscape · 390 phone
62
+ */
63
+ design?: { label: string; width: number; icon?: string }[];
64
+ /**
65
+ * The lower edge of each CSS band; the last is 0. The readout names the band the frame is in,
66
+ * and the presets offer both sides of every edge.
67
+ * @default desktop 992 · tablet 768 · phone landscape 480 · phone 0
68
+ */
69
+ breakpoints?: { name: string; min: number }[];
70
+ /** Named sizes that set width AND height. @default iPhone 15, iPad, MacBook, … */
71
+ devices?: { label: string; width: number; height: number }[];
72
+ };
56
73
  /**
57
74
  * The pin board — the dev-only click-to-ticket feedback half of this package (`docs/PIN.md`).
58
75
  *
59
- * `true` or omitted ships it at `/pin` with its defaults; `false` omits it entirely, for a site
60
- * that wants the component lab alone. An object is the pin integration's own options, and the
61
- * lab fills in `links` from its RESOLVED `subpath` so the board's "Lab" link is derived rather
62
- * than guessed — pass your own `links` to override that.
76
+ * `true` or omitted ships it at `<subpath>/tasks` (the lab's Kanban Board and All Tasks views),
77
+ * with `/pin` redirecting there; `false` omits it entirely, for a site that wants the lab alone.
78
+ * An object is the pin integration's own options.
63
79
  *
64
80
  * @default true
65
81
  */
@@ -95,11 +111,12 @@ export interface OrbytesPinOptions {
95
111
  shots?: boolean;
96
112
  /**
97
113
  * Where the board is served, dev only. `<route>/assets/*` serves its screenshots and
98
- * `<route>/api/ticket` is the one write endpoint.
114
+ * `<route>/api/ticket` is the one write endpoint; through `orbytesLab()`, `<route>` and
115
+ * `<route>/all` are the lab's Kanban Board and All Tasks views.
99
116
  *
100
- * It is MIDDLEWARE, installed ahead of Astro's own request handler, so it SHADOWS a host page at
101
- * the same path — in dev, silently. A site with its own `/pin` page sets something else here.
102
- * @default "/pin"
117
+ * Its asset and API paths are MIDDLEWARE, installed ahead of Astro's own request handler, so they
118
+ * shadow a host page at the same path — in dev, silently.
119
+ * @default "<subpath>/tasks" through orbytesLab(), "/pin" on its own
103
120
  */
104
121
  route?: string;
105
122
  /**
@@ -117,9 +134,10 @@ export interface OrbytesPinOptions {
117
134
  }
118
135
 
119
136
  /**
120
- * The orbytes component lab AND the pin board: the Astrobook core vendored into this package
121
- * (`src/core/`, 0.13.3), the lab shell around it, and the dev-only feedback board at `/pin`. Astro
122
- * flattens the returned array, so one entry in `integrations` configures all three.
137
+ * The orbytes lab AND the pin board: the Astrobook core vendored into this package
138
+ * (`src/core/`, 0.13.3), the lab's chrome around it, and the dev-only feedback board at
139
+ * `<subpath>/tasks`. Astro flattens the returned array, so one entry in `integrations` configures
140
+ * all three.
123
141
  *
124
142
  * `pin: false` returns the lab alone.
125
143
  */
@@ -206,7 +224,7 @@ export const OPEN_STATUSES: readonly PinStatus[];
206
224
  export function normaliseStatus(status: unknown): string;
207
225
  /** Open — neither accepted nor cancelled. `isOpen("Done")` is `false`. */
208
226
  export function isOpen(status: unknown): boolean;
209
- /** Archived. Every agent-facing read drops these; `/pin` is the one place they are visible. */
227
+ /** Archived. Every agent-facing read drops these; the board is the one place they are visible. */
210
228
  export function isCancelled(status: unknown): boolean;
211
229
  /** May an agent START work on this ticket? Exactly one status says yes. */
212
230
  export function isAgentReady(status: unknown): boolean;