@orbytes/astrolab 0.4.0-next.1 → 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.
- package/README.md +184 -84
- package/bin/pin-gallery.mjs +53 -19
- package/defaults.mjs +7 -20
- package/docs/PIN-CONTRACT.md +76 -10
- package/docs/PIN.md +93 -23
- package/index.d.ts +1 -7
- package/index.mjs +14 -81
- package/package.json +2 -2
- package/src/Home.astro +7 -8
- package/src/LabHead.astro +1 -1
- package/src/chrome/ActionsMenu.astro +97 -0
- package/src/chrome/ComponentCard.astro +9 -2
- package/src/chrome/Nav.astro +36 -10
- package/src/chrome/Panel.astro +17 -4
- package/src/chrome/Properties.astro +104 -0
- package/src/chrome/SectionsTree.astro +128 -0
- package/src/chrome/Shell.astro +20 -6
- package/src/chrome/StoryView.astro +103 -162
- package/src/chrome/Tree.astro +56 -53
- package/src/chrome/ViewportControls.astro +136 -61
- package/src/chrome/ViewportStage.astro +26 -3
- package/src/chrome/icons.ts +9 -0
- package/src/chrome/marks-client.ts +26 -53
- package/src/chrome/model.ts +14 -0
- package/src/chrome/navbar-client.ts +324 -0
- package/src/chrome/params-client.ts +434 -0
- package/src/chrome/pins-data.ts +42 -9
- package/src/chrome/shell-client.ts +99 -3
- package/src/chrome/trees.ts +112 -7
- package/src/chrome/viewport-client.ts +68 -242
- package/src/chrome/views/Assets.astro +21 -6
- package/src/chrome/views/Pages.astro +90 -54
- package/src/chrome/views/Placeholder.astro +3 -3
- package/src/chrome/views/Tasks.astro +12 -40
- package/src/core/LICENSE-astrobook +5 -0
- package/src/core/utils/kebab-case.ts +2 -2
- package/src/pin/board.mjs +25 -15
- package/src/pin/index.mjs +34 -20
- package/src/pin/tickets.mjs +6 -5
- package/src/pin/toolbar.js +81 -3
- package/src/shell/Browse.astro +35 -10
- package/src/shell/lab-index.ts +5 -4
- package/src/shell/lab-params.ts +113 -6
- package/src/shell/live-files.mjs +212 -10
- package/src/shell/marks.mjs +17 -41
- package/src/ui/components/preview-layout.astro +17 -0
- package/src/ui/components/theme-script.astro +4 -3
- package/src/ui/lab.css +2167 -566
- package/virtual.d.ts +0 -4
- package/bin/lab-cull.mjs +0 -401
package/defaults.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// The lab's defaults and the one option resolver — imported by the integration (./index.mjs)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// The lab's defaults and the one option resolver — imported by the integration (./index.mjs), so
|
|
2
|
+
// every path the lab reads and writes is derived in one place. Dependency-free: it loads in plain
|
|
3
|
+
// Node, from the consumer's astro.config.mjs, outside Vite.
|
|
4
4
|
//
|
|
5
5
|
// Everything `resolveLabOptions` returns is JSON-serialisable on purpose. It is handed to the
|
|
6
6
|
// package's own .astro pages through the `virtual:orbytes-lab/config.mjs` module the integration
|
|
@@ -25,17 +25,13 @@ export const DEFAULT_TIERS = ["sections", "components", "explorations"];
|
|
|
25
25
|
* What a tier NAMED like one of these does, unless the consumer says otherwise on the tier itself:
|
|
26
26
|
*
|
|
27
27
|
* - `responsive` — its stories are section versions: they carry the responsive mark (ticked by
|
|
28
|
-
* hand in the
|
|
29
|
-
* in thumbnails and in the viewport configurator.
|
|
30
|
-
* - `cullable` — its stories may be marked for deletion (`<directory>/cull.json`, drained by
|
|
31
|
-
* `orbytes-lab-cull`). Nothing else in the lab can be marked: version history and shared chrome
|
|
32
|
-
* are never culled from a browser.
|
|
28
|
+
* hand on the component's page in the lab, stored in `<directory>/responsive.json`) and they
|
|
29
|
+
* render at page width in thumbnails and in the viewport configurator.
|
|
33
30
|
*
|
|
34
|
-
* At most one tier holds
|
|
31
|
+
* At most one tier holds the role; the first one that claims it wins.
|
|
35
32
|
*/
|
|
36
33
|
export const TIER_ROLES = {
|
|
37
34
|
sections: { responsive: true },
|
|
38
|
-
explorations: { cullable: true },
|
|
39
35
|
};
|
|
40
36
|
|
|
41
37
|
/**
|
|
@@ -118,19 +114,17 @@ export const normaliseSubpath = (value) => {
|
|
|
118
114
|
* @property {string} id the first path segment under `directory`
|
|
119
115
|
* @property {string} label shown on the tier card and the folder pages
|
|
120
116
|
* @property {boolean} responsive section versions: responsive marks, page-width thumbnails
|
|
121
|
-
* @property {boolean} cullable may be marked for deletion
|
|
122
117
|
*/
|
|
123
118
|
|
|
124
119
|
/** @param {string | Partial<LabTier>} tier @returns {LabTier} */
|
|
125
120
|
const resolveTier = (tier) => {
|
|
126
121
|
const raw = typeof tier === "string" ? { id: tier } : { ...tier };
|
|
127
122
|
const id = String(raw.id ?? "");
|
|
128
|
-
const role = /** @type {Record<string, {responsive?: boolean
|
|
123
|
+
const role = /** @type {Record<string, {responsive?: boolean}>} */ (TIER_ROLES)[id] ?? {};
|
|
129
124
|
return {
|
|
130
125
|
id,
|
|
131
126
|
label: raw.label ?? (id ? id.charAt(0).toUpperCase() + id.slice(1) : "Root"),
|
|
132
127
|
responsive: raw.responsive ?? role.responsive ?? false,
|
|
133
|
-
cullable: raw.cullable ?? role.cullable ?? false,
|
|
134
128
|
};
|
|
135
129
|
};
|
|
136
130
|
|
|
@@ -145,11 +139,8 @@ const resolveTier = (tier) => {
|
|
|
145
139
|
* @property {string | null} feedbucketKey
|
|
146
140
|
* @property {LabTier[]} tiers
|
|
147
141
|
* @property {string | null} sectionsTier the id of the tier holding the responsive role
|
|
148
|
-
* @property {string | null} cullTier the id of the tier holding the cull role
|
|
149
142
|
* @property {string | null} responsiveDir `<directory>/<sectionsTier>/`, or null
|
|
150
|
-
* @property {string | null} cullDir `<directory>/<cullTier>/`, or null
|
|
151
143
|
* @property {string} responsiveFile `<directory>/responsive.json`
|
|
152
|
-
* @property {string} cullFile `<directory>/cull.json`
|
|
153
144
|
* @property {Viewports} viewports the viewport's screen sizes (DEFAULT_VIEWPORTS)
|
|
154
145
|
* @property {null} tasks filled in by the integration when the pin board runs
|
|
155
146
|
*/
|
|
@@ -163,7 +154,6 @@ export function resolveLabOptions(options = {}) {
|
|
|
163
154
|
const directory = normaliseDirectory(options.directory ?? DEFAULT_DIRECTORY);
|
|
164
155
|
const tiers = (Array.isArray(options.tiers) ? options.tiers : DEFAULT_TIERS).map(resolveTier);
|
|
165
156
|
const sections = tiers.find((tier) => tier.responsive) ?? null;
|
|
166
|
-
const cull = tiers.find((tier) => tier.cullable) ?? null;
|
|
167
157
|
return {
|
|
168
158
|
directory,
|
|
169
159
|
subpath: normaliseSubpath(options.subpath ?? DEFAULT_SUBPATH),
|
|
@@ -174,11 +164,8 @@ export function resolveLabOptions(options = {}) {
|
|
|
174
164
|
feedbucketKey: options.feedbucketKey ? String(options.feedbucketKey) : null,
|
|
175
165
|
tiers,
|
|
176
166
|
sectionsTier: sections ? sections.id : null,
|
|
177
|
-
cullTier: cull ? cull.id : null,
|
|
178
167
|
responsiveDir: sections ? `${directory}/${sections.id}/` : null,
|
|
179
|
-
cullDir: cull ? `${directory}/${cull.id}/` : null,
|
|
180
168
|
responsiveFile: `${directory}/responsive.json`,
|
|
181
|
-
cullFile: `${directory}/cull.json`,
|
|
182
169
|
viewports: resolveViewports(options.viewports),
|
|
183
170
|
tasks: null,
|
|
184
171
|
};
|
package/docs/PIN-CONTRACT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# orbytes-pin — the contract
|
|
2
2
|
|
|
3
|
-
The interfaces the parts of the pin board agree on.
|
|
3
|
+
The interfaces the parts of the pin board agree on. Five source files under
|
|
4
4
|
[`../src/pin/`](../src/pin/) cite this document by name, and it lives inside the package so those
|
|
5
5
|
citations resolve wherever the package is copied. The board moved into the lab package — today
|
|
6
6
|
`@orbytes/astrolab`, and named `orbytes-astro-lab` until the 2026-09-22 rename — on 2026-09-22,
|
|
@@ -120,14 +120,27 @@ naming no readable ticket, or an `expect` that disagrees with disk.
|
|
|
120
120
|
Canonical: `~/.orbytes/feedback-archive/<project>/pin-<NNN>.png`, **hardlinked** into
|
|
121
121
|
`<repo>/backlog/assets/` so the board can serve it — the same bytes under two names, zero extra
|
|
122
122
|
storage. Copy is the fallback across filesystems, and the log says which happened.
|
|
123
|
-
|
|
123
|
+
|
|
124
|
+
**The package ignores nothing in git.** It writes no `.gitignore` and adds no rule anywhere, so in
|
|
125
|
+
a site's repo `<backlogDir>/assets/` is committed like any other folder unless that repo ignores
|
|
126
|
+
it. This repo's own `.gitignore` carries `backlog/assets/` and `backlog/gallery.html`, so here the
|
|
127
|
+
ticket markdown is committed and the pictures and the gallery snapshot are not; a site that wants
|
|
128
|
+
the same adds those two lines (with its own `backlogDir`) to its `.gitignore`.
|
|
124
129
|
|
|
125
130
|
Taken by a warm Playwright singleton against a freshly loaded page, motion frozen two ways, toolbar
|
|
126
131
|
hidden, clipped to the ticket's rect with `fullPage: true` — **`clip` is viewport-relative without
|
|
127
132
|
it**, so a pin below the fold would throw *"Clipped area is either empty or outside the resulting
|
|
128
|
-
image"*.
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
image"*. You never wait on a shot: the ticket is written and the panel confirms in under 30ms, on
|
|
134
|
+
the same tick, and the shot lands afterwards on its own event.
|
|
135
|
+
|
|
136
|
+
**What a shot costs was measured three times, and each figure times a different span**, so they
|
|
137
|
+
are not competing readings of one number:
|
|
138
|
+
|
|
139
|
+
| Measured | Where | Span timed | Figure |
|
|
140
|
+
|---|---|---|---|
|
|
141
|
+
| 2026-09-21 | the client site the board was first built in, an Apple-silicon laptop | the capture: navigate, freeze motion, settle every image, hold the scroll position, clip | ~1.34 s warm, ~1.42 s cold; launching chromium ~98 ms |
|
|
142
|
+
| 2026-09-21 | the same site, cold cache, real input | the toolbar's submit to the screenshot | 2.0–2.4 s, warm or cold, dominated by the `networkidle` wait in `page.goto`, not by browser launch |
|
|
143
|
+
| 2026-09-22 | this repo's testbed home page, a real mouse | the *Create ticket* click to the *screenshot landed* status line | 1934 ms (below) |
|
|
131
144
|
|
|
132
145
|
**`playwright` is an optional peer and it has to be installed** — `npm i -D playwright && npx
|
|
133
146
|
playwright install chromium`. Nothing else gates screenshots: the dynamic import inside
|
|
@@ -154,9 +167,18 @@ A dead-selector ticket is **labelled, never discarded** — the screenshot, sour
|
|
|
154
167
|
`outer_html` still make it workable by hand. The defect this guards against is silence, not
|
|
155
168
|
instability.
|
|
156
169
|
|
|
170
|
+
The board's label is `selectorFlag` in [`../src/pin/board.mjs`](../src/pin/board.mjs): `0` and
|
|
171
|
+
`2+` put a *needs a human* banner on the card, and for `0` its note says an agent cannot locate the
|
|
172
|
+
element — by selector, which is the one thing that died. **It is a label, not a gate.** It changes
|
|
173
|
+
no status, and `isAgentReady()` and `listTickets()` do not read `selector_matches`, so a flagged
|
|
174
|
+
ticket left in `Ready for agent` still reaches an agent. Only its status keeps an agent off it.
|
|
175
|
+
|
|
157
176
|
## The browser ↔ server channel
|
|
158
177
|
|
|
159
|
-
|
|
178
|
+
Creating a ticket goes over the Vite HMR socket: no HTTP endpoint, no port, no CORS. A status
|
|
179
|
+
change is the one HTTP write, from the board and from the panel's own menus alike —
|
|
180
|
+
`POST <route>/api/ticket`, same-origin, on the dev server already running, with the address read
|
|
181
|
+
from the board's `data-api` (› *Writing a ticket*).
|
|
160
182
|
|
|
161
183
|
Client, inside `init(canvas, app, server)` ([`../src/pin/toolbar.js`](../src/pin/toolbar.js)):
|
|
162
184
|
`server.send("orbytes-pin:create", payload)` · `server.on("orbytes-pin:created" | "orbytes-pin:shot", …)`
|
|
@@ -190,6 +212,40 @@ with it is unchanged, because the page renders the SAME markup from the same fun
|
|
|
190
212
|
`script.card-data` payloads and `.broken li code` off the page at `route`. A change to those
|
|
191
213
|
selectors in `board.mjs` is a change to this contract.
|
|
192
214
|
|
|
215
|
+
## The lab's channel — `orbytes-pin:command` and `orbytes-pin:state`
|
|
216
|
+
|
|
217
|
+
Added 2026-09-24, when the lab's navbar took over pinning inside `/lab`. The lab imports nothing
|
|
218
|
+
from [`../src/pin/toolbar.js`](../src/pin/toolbar.js); it drives the one pin app on the lab page's
|
|
219
|
+
dev toolbar through two window events, so there is one picker and one composer, never a second
|
|
220
|
+
copy.
|
|
221
|
+
|
|
222
|
+
| Direction | Event | Detail |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| lab → app | `orbytes-pin:command` | `{ action: "pick", within?: Element }` or `{ action: "stop" }` |
|
|
225
|
+
| app → lab | `orbytes-pin:state` | `{ active, mode, tickets }` |
|
|
226
|
+
|
|
227
|
+
- **`pick`** turns the app on in pick mode; if it is already on, it goes back to picking unless an
|
|
228
|
+
element is already selected. **`within`** confines the pick to that element's box, and within
|
|
229
|
+
it to what lies inside a frame: the lab passes its canvas, so Comment pins the previewed page and
|
|
230
|
+
never the lab's chrome. The scope clears when the app turns off, and the toolbar's own button
|
|
231
|
+
still opens the app unscoped.
|
|
232
|
+
- **`stop`** turns the app off.
|
|
233
|
+
- **`orbytes-pin:state`** is published after every mode change and every board read. `active` is
|
|
234
|
+
whether the app is on; `mode` is `idle`, `picking` or `selected`; `tickets` is the list the app
|
|
235
|
+
read off the board — the same `script.card-data` payloads described above, so the lab parses no
|
|
236
|
+
ticket either.
|
|
237
|
+
- **`window.__orbytesPin`** holds the same snapshot, for a listener that arrives after the last
|
|
238
|
+
event. The lab's Comment button is disabled until it exists.
|
|
239
|
+
|
|
240
|
+
**Anything the lab draws over a framed page carries `data-orbytes-pin-ui`**, the attribute the
|
|
241
|
+
picker skips in whichever document it finds it (`isChrome`). The picker takes the topmost element
|
|
242
|
+
under the pointer, so an unmarked overlay stands in front of the frame: a scoped pick finds nothing
|
|
243
|
+
there, and an unscoped one pins the overlay. **And it is drawn in the chrome,
|
|
244
|
+
outside the frame** — the lab's markers and hint live in the lab page's own document, over the
|
|
245
|
+
`<iframe>`, never in the framed one. The framed document is what the picker measures, builds a selector against and
|
|
246
|
+
copies into `outer_html`, and what `shot.mjs` re-loads fresh for the screenshot; an element the lab
|
|
247
|
+
added to it would be in the first and absent from the second.
|
|
248
|
+
|
|
193
249
|
## The status line — one line, not a log
|
|
194
250
|
|
|
195
251
|
Decided 2026-09-22, replacing a running four-entry list that narrated every step. The log was
|
|
@@ -223,7 +279,7 @@ binding Astro 7 uses contains zero. So this package stamps its own `data-orbytes
|
|
|
223
279
|
via a dev-only Vite `load` hook. **Expect it to break again** — Astro dropped these once before,
|
|
224
280
|
silently, with no deprecation.
|
|
225
281
|
|
|
226
|
-
A file
|
|
282
|
+
A file under a `stamp` directory with no stampable element is a hard throw naming the file. That
|
|
227
283
|
is the guard, not an inconvenience: a warned-past file silently produces tickets with no source.
|
|
228
284
|
|
|
229
285
|
Selectors are built up to the **shortest form resolving to exactly one node**, verified against the
|
|
@@ -255,15 +311,25 @@ four times independently, and verified on real production builds — normal and
|
|
|
255
311
|
the inner `<button>` in their shadow root.
|
|
256
312
|
- **Markers belong in the toolbar canvas, never `document.body`.** The screenshotter hides only
|
|
257
313
|
`astro-dev-toolbar`, so a page-parented marker is photographed into every later ticket.
|
|
314
|
+
- **An overlay the lab draws over a frame needs `data-orbytes-pin-ui`, and belongs in the chrome.**
|
|
315
|
+
The picker takes the topmost element under the pointer and skips only what carries the
|
|
316
|
+
attribute, so an unmarked overlay blocks or becomes the pick; and an overlay put inside the
|
|
317
|
+
framed document changes the DOM a selector is built against (› *The lab's channel*).
|
|
318
|
+
- **Inside the lab, Astro's dev toolbar is hidden in every frame** (2026-09-24), so the lab page's
|
|
319
|
+
own toolbar is the only one. A test that looks for the pin app inside a framed story will not
|
|
320
|
+
find it; drive the lab page's app instead.
|
|
258
321
|
|
|
259
322
|
## Chrome
|
|
260
323
|
|
|
261
324
|
Neutral tool chrome, never the host site's styling — decided for this board and, separately, for
|
|
262
325
|
the component lab. A standard board that reads the same in every project it is installed in.
|
|
263
326
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
previewed
|
|
327
|
+
In the in-page panel and its markers ([`../src/pin/toolbar.js`](../src/pin/toolbar.js)) every value
|
|
328
|
+
is a custom property in one namespaced block (`--pin-*`, on `.pin-chrome`). No literal colours
|
|
329
|
+
outside it, and **no bare element selectors** anywhere — those inherit down into the previewed
|
|
330
|
+
page. The kanban's stylesheet (`BOARD_CSS`, [`../src/pin/board.mjs`](../src/pin/board.mjs)) is
|
|
331
|
+
fenced differently: its tokens are unprefixed, on its own root inside `@scope (.pin-board)`, and
|
|
332
|
+
inside the lab `.pin-board--lab` maps them onto the lab's `--lab-*` tokens.
|
|
267
333
|
|
|
268
334
|
**The `--pin-*` namespace survived the merge and is still the board's own** (2026-09-22). Repointing
|
|
269
335
|
it at the lab's `--lab-*` tokens is the shared-chrome design — lab phase 2 — and was deliberately
|
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
|
|
5
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
@@ -84,8 +86,10 @@ neither tab: it is reachable on the board only.
|
|
|
84
86
|
|
|
85
87
|
**In the lab since 2026-09-24** (decided that day: one URL, everything under `/lab`). The board is
|
|
86
88
|
two views in the lab's own chrome — **Kanban Board** at `/lab/tasks` and **All Tasks**, a sortable
|
|
87
|
-
table of every ticket, newest first, at `/lab/tasks/all` — with
|
|
88
|
-
the
|
|
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.
|
|
89
93
|
`/pin`, the board's address until then, redirects there. Both views are Astro pages the pin half
|
|
90
94
|
injects in `astro dev` only, rendered from the same markup, stylesheet and script as the standalone
|
|
91
95
|
file (`boardParts` in [`src/pin/board.mjs`](../src/pin/board.mjs)); the stylesheet is scoped with
|
|
@@ -101,8 +105,9 @@ scrolls sideways, rather than wrapping into a stack beside two sidebars.
|
|
|
101
105
|
with a filled count **only when it actually holds something**. An empty queue stays quiet; one
|
|
102
106
|
that shouts at nothing teaches you to stop looking at it. Its count also leads the top bar.
|
|
103
107
|
- **Cancelled is drawn to be ignored**: a narrow dashed rail rather than a sixth of the wall, its
|
|
104
|
-
cards desaturated until hovered. Still a real drop target
|
|
105
|
-
ticket
|
|
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.
|
|
106
111
|
- Every column carries a one-line gloss under its name (*agents start here*, *waiting on you*),
|
|
107
112
|
because six nouns with no explanation is a vocabulary you have to be told once and remember.
|
|
108
113
|
- **A card** carries its screenshot, id, title, dispatch mode, priority and source file, plus the
|
|
@@ -124,6 +129,29 @@ listening behind it.
|
|
|
124
129
|
**The standalone board's corner links** are an option (`links`), not the hardcoded `/` and `/lab`
|
|
125
130
|
they were until 2026-09-22. Inside the lab there are none — the lab's own menu is right there.
|
|
126
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.
|
|
154
|
+
|
|
127
155
|
## The write path
|
|
128
156
|
|
|
129
157
|
One endpoint, `POST <route>/api/ticket`, registered in `astro:server:setup` — which does not exist
|
|
@@ -132,14 +160,17 @@ in a build, so it cannot reach `dist/`.
|
|
|
132
160
|
**The endpoint, the screenshots and the `/pin` redirect are middleware**, installed ahead of
|
|
133
161
|
Astro's own request handler, so they shadow a host page at the same path — in dev only. Through
|
|
134
162
|
`orbytesLab()` the route is `<subpath>/tasks`, which no site page is likely to be; the one exposed
|
|
135
|
-
address is `/pin`, which redirects.
|
|
136
|
-
|
|
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.
|
|
137
168
|
|
|
138
169
|
```json
|
|
139
170
|
{ "id": "PIN-004", "status": "Ready for review", "expect": { "status": "In Progress" } }
|
|
140
171
|
```
|
|
141
172
|
|
|
142
|
-
`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
|
|
143
174
|
`updated_date`. It never serialises: the file is split by index, and the slice from the closing
|
|
144
175
|
`---` onward — the comment, the image and the fenced ```yaml pin block — is the same substring on
|
|
145
176
|
the way out as on the way in. That identity is asserted on every write, and the write is a temp
|
|
@@ -181,7 +212,7 @@ Nothing is wired separately: the board comes with the lab, through the one integ
|
|
|
181
212
|
already configures.
|
|
182
213
|
|
|
183
214
|
```js
|
|
184
|
-
import orbytesLab from "
|
|
215
|
+
import orbytesLab from "@orbytes/astrolab";
|
|
185
216
|
|
|
186
217
|
const includeLab =
|
|
187
218
|
process.argv.includes("dev") || process.env.PUBLIC_DEPLOY_ENV === "staging";
|
|
@@ -197,6 +228,15 @@ export default defineConfig({
|
|
|
197
228
|
gates itself on `command === "dev"`, so including the lab in a staging build registers it and it
|
|
198
229
|
does nothing there.
|
|
199
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
|
+
|
|
200
240
|
`playwright` is an **optional** peer and is loaded by dynamic import inside the screenshotter, not
|
|
201
241
|
at module scope — a site without it loads its config, runs the lab and writes tickets without
|
|
202
242
|
pictures. Install it (`npm i -D playwright && npx playwright install chromium`) for the screenshots,
|
|
@@ -208,7 +248,7 @@ Pass these as `orbytesLab({ pin: { … } })`.
|
|
|
208
248
|
|
|
209
249
|
| Option | Default | What it does |
|
|
210
250
|
|---|---|---|
|
|
211
|
-
| `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*. |
|
|
212
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. |
|
|
213
253
|
| `backlogDir` | `"backlog"` | Repo-relative board directory: tickets in `tasks/`, PNGs in `assets/`. |
|
|
214
254
|
| `archiveDir` | `~/.orbytes/feedback-archive` | Canonical screenshot home; the repo copy is a hardlink into it. |
|
|
@@ -315,26 +355,33 @@ Two rules now keep a selector off that ground, both in
|
|
|
315
355
|
- **A class naming a runtime condition never enters a selector** (`isTransientClass`,
|
|
316
356
|
`extendsSiblingClass`) — `swiper-slide-active` is on slide 1 now and slide 2 after a scroll.
|
|
317
357
|
|
|
318
|
-
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
|
|
319
359
|
loads the page **fresh in a real browser** to take the screenshot; before the shutter it asks that
|
|
320
360
|
page how many nodes the ticket's selector finds, and writes the answer back into the pin block:
|
|
321
361
|
|
|
322
362
|
| Value | Means |
|
|
323
363
|
|---|---|
|
|
324
364
|
| `1` | healthy — the selector names exactly this element on a fresh load |
|
|
325
|
-
| `0` | dead —
|
|
365
|
+
| `0` | dead — flagged *needs a human* on the board; a person works it from `source`, the screenshot and `outer_html` |
|
|
326
366
|
| `2`+ | ambiguous — the selector names several elements |
|
|
327
367
|
| `pending` | never tested — shots are off, or the screenshot failed |
|
|
328
368
|
|
|
329
369
|
Anything but `1` is said in the dev log and in the toolbar panel. A ticket whose selector died is
|
|
330
370
|
**still a good ticket** — it is labelled, never discarded. The defect was the silence.
|
|
331
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
|
+
|
|
332
379
|
## Screenshots
|
|
333
380
|
|
|
334
381
|
One chromium for the whole dev session, launched lazily on the first pin and kept warm, closed
|
|
335
|
-
when the dev server closes.
|
|
336
|
-
|
|
337
|
-
|
|
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*.
|
|
338
385
|
|
|
339
386
|
You never wait for any of it. The ticket is written and `orbytes-pin:created` replied on the
|
|
340
387
|
same synchronous tick; the screenshot follows and sends `orbytes-pin:shot` when it lands. A ticket
|
|
@@ -358,6 +405,22 @@ file under `stamp`, and the picker walks up from the clicked node to the nearest
|
|
|
358
405
|
it. No ancestor found → `source` is `unresolved`, and the CSS selector plus `outer_html` still
|
|
359
406
|
make the ticket actionable.
|
|
360
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
|
+
|
|
361
424
|
The attribute is inserted immediately after the tag name (`<section` → `<section
|
|
362
425
|
data-orbytes-src="…"`), so no attribute value, expression, spread or self-closing slash can be
|
|
363
426
|
mangled and no line number moves.
|
|
@@ -383,14 +446,17 @@ orbytesLab({
|
|
|
383
446
|
|
|
384
447
|
## The browser ↔ server channel
|
|
385
448
|
|
|
386
|
-
Astro's dev toolbar,
|
|
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.
|
|
387
453
|
|
|
388
454
|
| Direction | Event | Payload |
|
|
389
455
|
|---|---|---|
|
|
390
456
|
| browser → server | `orbytes-pin:create` | `{ comment, dispatch, source, selector, url, viewport, scroll, rect, outerHTML }` |
|
|
391
457
|
| server → browser | `orbytes-pin:created` | `{ id, file, shot, error }` — all four keys, always |
|
|
392
458
|
| server → browser | `orbytes-pin:shot` | `{ id, shot, selectorMatches, error }` |
|
|
393
|
-
| 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 }` |
|
|
394
460
|
|
|
395
461
|
On failure the reply is `{ id: null, file: null, shot: null, error: "why, in one plain sentence" }`.
|
|
396
462
|
|
|
@@ -419,7 +485,8 @@ window, so clicking it again focuses that tab instead of opening another.
|
|
|
419
485
|
|
|
420
486
|
1. `astro:config:setup` returns early unless the Astro command is `dev` — no plugin, no toolbar
|
|
421
487
|
app, no stamp.
|
|
422
|
-
2.
|
|
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
|
|
423
490
|
registered.
|
|
424
491
|
3. `astro:server:setup`, where every write lives — the ticket writer, the screenshots and the
|
|
425
492
|
`<route>/api/ticket` endpoint — does not run in a build at all. The lab's two Tasks views are
|
|
@@ -432,8 +499,11 @@ The first one matters more since the merge than it did before: the lab is delibe
|
|
|
432
499
|
in a build for the first time, and `command === "dev"` is what makes that a no-op.
|
|
433
500
|
|
|
434
501
|
Verified by running a real `npm run build` and grepping `dist/` for `orbytes-pin`, `data-orbytes-src`
|
|
435
|
-
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.
|
|
436
504
|
|
|
437
505
|
No path this package emits into served HTML or into a ticket is absolute. The repo root is found
|
|
438
|
-
by walking up from the Astro config root to the first directory containing `.git`, and
|
|
439
|
-
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 {
|
|
@@ -52,8 +47,7 @@ export interface OrbytesLabOptions {
|
|
|
52
47
|
feedbucketKey?: string;
|
|
53
48
|
/**
|
|
54
49
|
* The tiers, in reading order. Default `["sections", "components", "explorations"]`; a tier
|
|
55
|
-
* named "sections" takes the responsive role
|
|
56
|
-
* the entry says otherwise.
|
|
50
|
+
* named "sections" takes the responsive role unless the entry says otherwise.
|
|
57
51
|
*/
|
|
58
52
|
tiers?: (string | LabTierOption)[];
|
|
59
53
|
/**
|
package/index.mjs
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
//
|
|
12
12
|
// ── the pin board (./src/pin/, moved in 2026-09-22) ─────────────────────────────────────────────
|
|
13
13
|
// Decided 2026-09-22: the lab and the pin board are ONE app, bundled and working as one — not two
|
|
14
|
-
// packages a site installs separately. So
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// packages a site installs separately. So the lab (`<subpath>`) and its Tasks board
|
|
15
|
+
// (`<subpath>/tasks`) come from one call, one install, one dev server. `pin: false` omits it
|
|
16
|
+
// entirely — a site may want the lab alone — and `pin: {…}` passes options straight through
|
|
17
|
+
// (ops/standalone-plan.md › The pin board merges in; docs/PIN.md).
|
|
17
18
|
//
|
|
18
19
|
// The pin half is DEV ONLY and gates itself four times over; it is registered in staging builds
|
|
19
20
|
// along with the lab and does nothing there, because its first assertion is `command === "dev"`.
|
|
@@ -33,18 +34,13 @@
|
|
|
33
34
|
// (the task views, <subpath>/tasks and /tasks/all, are the pin half's, dev only)
|
|
34
35
|
// and define `virtual:orbytes-lab/*` so those pages can read the resolved options, the
|
|
35
36
|
// consumer's head component and the consumer's CSS without importing anything by path.
|
|
36
|
-
// 3. serve the
|
|
37
|
-
// build, so a deployed lab has no write path and the switches hide themselves):
|
|
38
|
-
// GET /__lab/cull { marked: string[], updated: string | null }
|
|
39
|
-
// PUT /__lab/cull body { marked: string[] } — every entry must be an existing
|
|
40
|
-
// stories file in the CULLABLE tier AND not live on any page;
|
|
41
|
-
// otherwise 400 with the offenders. Writes <directory>/cull.json.
|
|
37
|
+
// 3. serve the responsive marks API on the Vite dev server only (astro:server:setup never runs
|
|
38
|
+
// in a build, so a deployed lab has no write path and the switches hide themselves):
|
|
42
39
|
// GET /__lab/responsive { done: string[], approved: string[], updated: string | null }
|
|
43
40
|
// PUT /__lab/responsive body { done: string[], approved?: string[] } — every entry must
|
|
44
41
|
// be an existing stories file in the RESPONSIVE tier; otherwise 400
|
|
45
42
|
// with the offenders. Writes <directory>/responsive.json.
|
|
46
|
-
// Marks only. NOTHING HERE DELETES A FILE
|
|
47
|
-
// run by hand and with a dry run first.
|
|
43
|
+
// Marks only. NOTHING HERE DELETES A FILE.
|
|
48
44
|
//
|
|
49
45
|
// Gotcha (2026-09-06): Astro re-imports astro.config.mjs on a config change, but this module and
|
|
50
46
|
// its ./src/shell/*.mjs imports stay in Node's ESM cache for the life of the process — so an edit
|
|
@@ -59,15 +55,7 @@ import { fileURLToPath } from "node:url";
|
|
|
59
55
|
import { createAstrobookIntegration } from "./dist/core/index.js";
|
|
60
56
|
import { resolveLabOptions } from "./defaults.mjs";
|
|
61
57
|
import orbytesPin from "./src/pin/index.mjs";
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
64
|
-
cullOffence,
|
|
65
|
-
readCull,
|
|
66
|
-
readResponsive,
|
|
67
|
-
responsiveOffence,
|
|
68
|
-
writeCull,
|
|
69
|
-
writeResponsive,
|
|
70
|
-
} from "./src/shell/marks.mjs";
|
|
58
|
+
import { readResponsive, responsiveOffence, writeResponsive } from "./src/shell/marks.mjs";
|
|
71
59
|
|
|
72
60
|
const file = (relative) => fileURLToPath(new URL(relative, import.meta.url));
|
|
73
61
|
|
|
@@ -84,22 +72,6 @@ const RESOLVED = {
|
|
|
84
72
|
[VIRTUAL.css]: "__virtual_orbytes_lab_user_css__.mjs",
|
|
85
73
|
};
|
|
86
74
|
|
|
87
|
-
/**
|
|
88
|
-
* Why an entry may not be marked for deletion, or null when it may. `importedByLive` maps a
|
|
89
|
-
* component file to the live section that imports it, so an exploration a live section uses is
|
|
90
|
-
* refused here with the same reason the cull script would give later.
|
|
91
|
-
*/
|
|
92
|
-
const cullEntryOffence = (rootDir, config, entry, liveByFile, importedByLive) => {
|
|
93
|
-
const bad = cullOffence(rootDir, config, entry);
|
|
94
|
-
if (bad) return bad;
|
|
95
|
-
const component = storyComponentFile(rootDir, entry);
|
|
96
|
-
const mount = component ? liveByFile.get(component) : undefined;
|
|
97
|
-
if (mount) return `live on ${mount.page} (slot ${mount.slot}: ${component})`;
|
|
98
|
-
const user = component ? importedByLive.get(component) : undefined;
|
|
99
|
-
if (user) return `used by live section ${user}`;
|
|
100
|
-
return null;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
75
|
const readBody = (req) =>
|
|
104
76
|
new Promise((resolve, reject) => {
|
|
105
77
|
let body = "";
|
|
@@ -184,10 +156,10 @@ function labShell(options, shared = { tasks: null }) {
|
|
|
184
156
|
// consumer should need one line in its config and nothing more, so the exemption is
|
|
185
157
|
// declared here rather than there.
|
|
186
158
|
ssr: { noExternal: ["@orbytes/astrolab"] },
|
|
187
|
-
// Marking writes JSON
|
|
188
|
-
//
|
|
159
|
+
// Marking writes a JSON file under src/; keep Vite from treating it as a source
|
|
160
|
+
// change and reloading the lab mid-triage.
|
|
189
161
|
server: {
|
|
190
|
-
watch: { ignored: [`**/${config.
|
|
162
|
+
watch: { ignored: [`**/${config.responsiveFile}`] },
|
|
191
163
|
},
|
|
192
164
|
},
|
|
193
165
|
});
|
|
@@ -196,47 +168,8 @@ function labShell(options, shared = { tasks: null }) {
|
|
|
196
168
|
);
|
|
197
169
|
},
|
|
198
170
|
"astro:server:setup": ({ server, logger }) => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (url.pathname !== "/__lab/cull") return next();
|
|
202
|
-
try {
|
|
203
|
-
if (req.method === "GET") return send(res, 200, readCull(rootDir, config));
|
|
204
|
-
if (req.method === "PUT") {
|
|
205
|
-
const body = await readBody(req);
|
|
206
|
-
if (!Array.isArray(body.marked))
|
|
207
|
-
return send(res, 400, { error: "body must be { marked: string[] }" });
|
|
208
|
-
const live = liveComponentFiles(rootDir);
|
|
209
|
-
// First mount wins, so the refusal names the page a reader would look at first.
|
|
210
|
-
const liveByFile = new Map();
|
|
211
|
-
for (const l of live) if (!liveByFile.has(l.file)) liveByFile.set(l.file, l);
|
|
212
|
-
const importedByLive = new Map();
|
|
213
|
-
for (const l of live) {
|
|
214
|
-
for (const dep of defaultImports(rootDir, l.file)) {
|
|
215
|
-
if (!importedByLive.has(dep.file)) importedByLive.set(dep.file, l.file);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
const offenders = body.marked
|
|
219
|
-
.map((entry) => ({
|
|
220
|
-
path: entry,
|
|
221
|
-
reason: cullEntryOffence(rootDir, config, entry, liveByFile, importedByLive),
|
|
222
|
-
}))
|
|
223
|
-
.filter((o) => o.reason !== null);
|
|
224
|
-
if (offenders.length > 0) {
|
|
225
|
-
return send(res, 400, { error: "some entries cannot be marked", offenders });
|
|
226
|
-
}
|
|
227
|
-
const data = writeCull(rootDir, config, body.marked);
|
|
228
|
-
logger.info(`cull marks saved: ${data.marked.length} → ${config.cullFile}`);
|
|
229
|
-
return send(res, 200, { ok: true, file: config.cullFile, ...data });
|
|
230
|
-
}
|
|
231
|
-
return send(res, 405, { error: "GET or PUT" });
|
|
232
|
-
} catch (e) {
|
|
233
|
-
return send(res, 500, { error: String(e?.message ?? e) });
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// The responsive marks — same shape of API as the cull one above, and deliberately its
|
|
238
|
-
// sibling rather than a second mechanism: a section version is marked responsive (and,
|
|
239
|
-
// separately, approved for that work) by hand, because neither fact is in the code.
|
|
171
|
+
// The responsive marks: a section version is marked responsive (and, separately, approved
|
|
172
|
+
// for that work) by hand, because neither fact is in the code.
|
|
240
173
|
server.middlewares.use(async (req, res, next) => {
|
|
241
174
|
const url = new URL(req.url ?? "/", "http://x");
|
|
242
175
|
if (url.pathname !== "/__lab/responsive") return next();
|
|
@@ -269,7 +202,7 @@ function labShell(options, shared = { tasks: null }) {
|
|
|
269
202
|
}
|
|
270
203
|
});
|
|
271
204
|
|
|
272
|
-
logger.info("mark
|
|
205
|
+
logger.info("mark API at /__lab/responsive (dev only)");
|
|
273
206
|
},
|
|
274
207
|
},
|
|
275
208
|
};
|