@orbytes/astrolab 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +37 -0
  2. package/README.md +410 -0
  3. package/bin/lab-cull.mjs +401 -0
  4. package/bin/pin-gallery.mjs +121 -0
  5. package/defaults.mjs +120 -0
  6. package/dist/core/astro-integration.js +130 -0
  7. package/dist/core/index.js +6 -0
  8. package/dist/core/lib-paths.js +29 -0
  9. package/dist/core/options.js +173 -0
  10. package/dist/core/utils/get-exports.js +52 -0
  11. package/dist/core/utils/invariant.js +12 -0
  12. package/dist/core/utils/kebab-case.js +15 -0
  13. package/dist/core/utils/path-builder.js +22 -0
  14. package/dist/core/utils/path.js +53 -0
  15. package/dist/core/virtual-module/get-story-modules.js +60 -0
  16. package/dist/core/virtual-module/story-modules.js +8 -0
  17. package/dist/core/virtual-module/virtual-module-ids.js +22 -0
  18. package/dist/core/virtual-module/virtual-routes.js +83 -0
  19. package/dist/core/virtual-module/vite-plugin.js +98 -0
  20. package/docs/PIN-CONTRACT.md +263 -0
  21. package/docs/PIN.md +429 -0
  22. package/index.d.ts +279 -0
  23. package/index.mjs +347 -0
  24. package/package.json +93 -0
  25. package/src/Empty.astro +4 -0
  26. package/src/Home.astro +298 -0
  27. package/src/LabHead.astro +1102 -0
  28. package/src/core/LICENSE-astrobook +166 -0
  29. package/src/core/astro-integration.ts +166 -0
  30. package/src/core/client.ts +89 -0
  31. package/src/core/index.ts +7 -0
  32. package/src/core/lib/components/empty.astro +1 -0
  33. package/src/core/lib/components/head.astro +1 -0
  34. package/src/core/lib/components/home.astro +8 -0
  35. package/src/core/lib/components/with-decorators.astro +22 -0
  36. package/src/core/lib/pages/app.astro +19 -0
  37. package/src/core/lib/pages/preview.astro +17 -0
  38. package/src/core/lib/pages/story.astro +16 -0
  39. package/src/core/lib-paths.ts +72 -0
  40. package/src/core/options.ts +262 -0
  41. package/src/core/utils/get-exports.ts +59 -0
  42. package/src/core/utils/invariant.ts +13 -0
  43. package/src/core/utils/kebab-case.ts +30 -0
  44. package/src/core/utils/path-builder.ts +45 -0
  45. package/src/core/utils/path.ts +80 -0
  46. package/src/core/virtual-module/get-story-modules.ts +110 -0
  47. package/src/core/virtual-module/story-modules.ts +9 -0
  48. package/src/core/virtual-module/virtual-module-ids.ts +17 -0
  49. package/src/core/virtual-module/virtual-routes.ts +130 -0
  50. package/src/core/virtual-module/vite-plugin.ts +125 -0
  51. package/src/pin/board.mjs +1521 -0
  52. package/src/pin/index.mjs +666 -0
  53. package/src/pin/shot.mjs +427 -0
  54. package/src/pin/source-stamp.mjs +159 -0
  55. package/src/pin/tickets.mjs +697 -0
  56. package/src/pin/toolbar.js +3181 -0
  57. package/src/shell/Browse.astro +371 -0
  58. package/src/shell/CardGrid.astro +297 -0
  59. package/src/shell/Viewport.astro +1330 -0
  60. package/src/shell/index.json.ts +12 -0
  61. package/src/shell/lab-index.ts +344 -0
  62. package/src/shell/lab-params.ts +245 -0
  63. package/src/shell/live-files.mjs +164 -0
  64. package/src/shell/marks.mjs +136 -0
  65. package/src/types/index.ts +6 -0
  66. package/src/types/types.ts +239 -0
  67. package/src/types/virtual.d.ts +29 -0
  68. package/src/ui/components/app.astro +13 -0
  69. package/src/ui/components/build-path.ts +13 -0
  70. package/src/ui/components/build-tree.ts +108 -0
  71. package/src/ui/components/collapse-duration.ts +28 -0
  72. package/src/ui/components/compress-terms.ts +10 -0
  73. package/src/ui/components/dashboard-layout.astro +39 -0
  74. package/src/ui/components/home.astro +65 -0
  75. package/src/ui/components/layout.astro +110 -0
  76. package/src/ui/components/preview-layout.astro +109 -0
  77. package/src/ui/components/sidebar-button-fullscreen.astro +38 -0
  78. package/src/ui/components/sidebar-button-search.astro +23 -0
  79. package/src/ui/components/sidebar-button-theme.astro +9 -0
  80. package/src/ui/components/sidebar-button.astro +24 -0
  81. package/src/ui/components/sidebar-resize-handle.astro +74 -0
  82. package/src/ui/components/sidebar-search-panel.astro +41 -0
  83. package/src/ui/components/sidebar-search-script.ts +103 -0
  84. package/src/ui/components/sidebar-title.astro +17 -0
  85. package/src/ui/components/sidebar-tree-node.astro +143 -0
  86. package/src/ui/components/sidebar-tree.astro +84 -0
  87. package/src/ui/components/sidebar.astro +29 -0
  88. package/src/ui/components/theme-message.ts +26 -0
  89. package/src/ui/components/theme-script.astro +71 -0
  90. package/src/ui/components/theme-toggle.astro +63 -0
  91. package/src/ui/components/theme.ts +32 -0
  92. package/src/ui/index.ts +4 -0
  93. package/src/ui/lab.css +549 -0
  94. package/virtual.d.ts +42 -0
@@ -0,0 +1,98 @@
1
+ import { resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { loadStoryModules } from "./story-modules.js";
4
+ import {
5
+ COMPONENT_HEAD_ID,
6
+ COMPONENT_HEAD_RESOLVED_ID,
7
+ COMPONENT_HOME_ID,
8
+ COMPONENT_HOME_RESOLVED_ID,
9
+ STORY_MODULES_ID,
10
+ STORY_MODULES_RESOLVED_ID,
11
+ GLOBAL_CONFIG_ID,
12
+ GLOBAL_CONFIG_RESOLVED_ID,
13
+ USER_CSS_RESOLVED_ID,
14
+ USER_CSS_ID
15
+ } from "./virtual-module-ids.js";
16
+ function createVirtualFilesPlugin(rootDir, config, astroConfig, logger) {
17
+ const root = astroConfig.root;
18
+ function resolveId(id, base = root) {
19
+ return JSON.stringify(
20
+ id.startsWith(".") ? resolve(fileURLToPath(base), id) : id
21
+ );
22
+ }
23
+ let storyModules;
24
+ let needToReload = false;
25
+ let watcherInitialized = false;
26
+ async function getStoryModules() {
27
+ const newStoryModules = await loadStoryModules(rootDir);
28
+ if (storyModules && newStoryModules !== storyModules) {
29
+ needToReload = true;
30
+ }
31
+ storyModules = newStoryModules;
32
+ return storyModules;
33
+ }
34
+ async function restartServerIfNeeded(server) {
35
+ await getStoryModules();
36
+ if (needToReload) {
37
+ needToReload = false;
38
+ logger.info(`Restarting`);
39
+ await server.restart();
40
+ }
41
+ }
42
+ function initializeNewFileWatcher(server) {
43
+ if (watcherInitialized || !server.watcher) return;
44
+ watcherInitialized = true;
45
+ server.watcher.on("add", async (filePath) => {
46
+ if (filePath.includes(".stories.")) {
47
+ logger.debug(`Potential story file added: ${filePath}`);
48
+ await restartServerIfNeeded(server);
49
+ }
50
+ });
51
+ server.watcher.on("unlink", async (filePath) => {
52
+ if (filePath.includes(".stories.")) {
53
+ logger.debug(`Potential story file deleted: ${filePath}`);
54
+ await restartServerIfNeeded(server);
55
+ }
56
+ });
57
+ logger.debug(`Initialized file watcher for story files`);
58
+ }
59
+ return {
60
+ name: "astrobook/virtual-files",
61
+ resolveId(id) {
62
+ switch (id) {
63
+ case STORY_MODULES_ID:
64
+ return STORY_MODULES_RESOLVED_ID;
65
+ case GLOBAL_CONFIG_ID:
66
+ return GLOBAL_CONFIG_RESOLVED_ID;
67
+ case COMPONENT_HEAD_ID:
68
+ return COMPONENT_HEAD_RESOLVED_ID;
69
+ case COMPONENT_HOME_ID:
70
+ return COMPONENT_HOME_RESOLVED_ID;
71
+ case USER_CSS_ID:
72
+ return USER_CSS_RESOLVED_ID;
73
+ }
74
+ },
75
+ load(id) {
76
+ switch (id) {
77
+ case STORY_MODULES_RESOLVED_ID:
78
+ return getStoryModules();
79
+ case GLOBAL_CONFIG_RESOLVED_ID:
80
+ return `const config = ${JSON.stringify(config)}; export default config;`;
81
+ case COMPONENT_HEAD_RESOLVED_ID:
82
+ return `export { default } from ${resolveId(config.head)};`;
83
+ case COMPONENT_HOME_RESOLVED_ID:
84
+ return `export { default } from ${resolveId(config.home)};`;
85
+ case USER_CSS_RESOLVED_ID:
86
+ return config.css.map((id2) => `import ${resolveId(id2)};`).join("");
87
+ }
88
+ },
89
+ async handleHotUpdate(ctx) {
90
+ const { server } = ctx;
91
+ initializeNewFileWatcher(server);
92
+ await restartServerIfNeeded(server);
93
+ }
94
+ };
95
+ }
96
+ export {
97
+ createVirtualFilesPlugin
98
+ };
@@ -0,0 +1,263 @@
1
+ # orbytes-pin — the contract
2
+
3
+ The interfaces the parts of the pin board agree on. Four source files under
4
+ [`../src/pin/`](../src/pin/) cite this document by name, and it lives inside the package so those
5
+ citations resolve wherever the package is copied. The board moved into the lab package — today
6
+ `@orbytes/astrolab`, and named `orbytes-astro-lab` until the 2026-09-22 rename — on 2026-09-22,
7
+ and the citations were repointed with it (`../../docs/PIN-CONTRACT.md`); if you are
8
+ moving them again, `grep -rn PIN-CONTRACT src/pin` is the list.
9
+
10
+ **This describes what is, not what was planned.** Where a decision had a reason that is not obvious
11
+ from the code, the reason is here — usually because it was learned by something breaking.
12
+
13
+ ## What this package is
14
+
15
+ A dev-only Astro integration. You click a rendered element on `astro dev`, type a comment, and a
16
+ markdown ticket plus a screenshot land on local disk for an agent to pick up. No cloud, no API key,
17
+ no MCP, no second process.
18
+
19
+ It serves a **solo pass** — your own review of a site in dev, before anyone else sees it. Whatever
20
+ tool you use for review rounds with other people is untouched by it, and nothing syncs between the
21
+ two (decided 2026-09-21). Two systems, and moving anything between them is a human's job.
22
+
23
+ ## Statuses
24
+
25
+ Six, in board order, adopted 2026-09-22:
26
+
27
+ ```
28
+ Backlog | Ready for agent | In Progress | Ready for review | Resolved | Cancelled
29
+ ```
30
+
31
+ - **`Backlog`** — raised, but not ready for an agent to pick up.
32
+ - **`Ready for agent`** — queued work; it replaces the usual *To Do*. **The only status an agent may
33
+ start work from.**
34
+ - **`In Progress`** — an agent is on it.
35
+ - **`Ready for review`** — an agent has finished; a human decides whether it is done. The human
36
+ review queue.
37
+ - **`Resolved`** — accepted by a human, and only a human sets it. The rule binds agents, not the
38
+ wire: the write endpoint deliberately does **not** refuse `Resolved`, because a person's own drag
39
+ arrives through that same path and the endpoint cannot tell a person from a script.
40
+ - **`Cancelled`** — an archive for abandoned work, which must not take up an agent's context. So it
41
+ is **absent, not dimmed**: `listTickets()` and `collectTickets()` drop cancelled tickets unless
42
+ explicitly asked with `includeCancelled: true`.
43
+
44
+ `To Do` reads as `Ready for agent` and `Done` as `Resolved`, so anything written before this
45
+ vocabulary still parses. Neither is ever emitted, and a ticket carrying one is rewritten on its next
46
+ write, whatever that write was for.
47
+
48
+ The constants and the predicates (`isOpen`, `isCancelled`, `isAgentReady`, `OPEN_STATUSES`) live in
49
+ [`../src/pin/tickets.mjs`](../src/pin/tickets.mjs) and are re-exported from the package root.
50
+ Nothing should hardcode a status string.
51
+
52
+ ## Ticket format — `<repo>/backlog/tasks/pin-<NNN>-<slug>.md`
53
+
54
+ ```markdown
55
+ ---
56
+ id: PIN-014
57
+ title: Hero headline sits too low against the image
58
+ status: Ready for agent
59
+ assignee: []
60
+ created_date: '2026-09-21'
61
+ labels: [pin, hero]
62
+ dependencies: []
63
+ priority: medium
64
+ ---
65
+
66
+ ## Description
67
+
68
+ The comment, verbatim, unedited.
69
+
70
+ ![](/assets/pin-014.png)
71
+
72
+ ```yaml pin
73
+ dispatch: queue
74
+ source: site/src/lab/sections/Section01Hero/V1/HeroSection.astro
75
+ selector: 'section.hero > div > h1'
76
+ url: /
77
+ viewport: { width: 1440, height: 960, dpr: 2 }
78
+ scroll: { x: 0, y: 2840 }
79
+ rect: { x: 120, y: 340, width: 800, height: 96 }
80
+ shot: backlog/assets/pin-014.png
81
+ outer_html: '<h1 class="hero__title">Clarity, not more noise</h1>'
82
+ selector_matches: 1
83
+ ```
84
+ ```
85
+
86
+ **Why the pin data is in the body and not in frontmatter.** This format was shaped around
87
+ backlog.md, which served as the board for one day. Any board that re-serialises frontmatter drops
88
+ keys it does not recognise — measured twice, on real commands: a single `backlog task edit -s Done`
89
+ deleted `dispatch`, `source`, `selector`, `rect`, `shot` and `outer_html` in one go, while the body
90
+ survived untouched. backlog.md is retired here, but the constraint is worth keeping: the body is the
91
+ half that survives a careless writer.
92
+
93
+ **Why `## Description` and not `## Comment`.** Measured: under `## Comment`, backlog.md's detail
94
+ view rendered neither the comment nor the screenshot.
95
+
96
+ **Why titles cap at 70 characters.** js-yaml folds a scalar when its length exceeds the line width,
97
+ which is 78 for a top-level key — measured at exactly 79 by generating 41 tickets across lengths
98
+ 55–95 and running a real status change on each. At the old 80-character cap every full-length title
99
+ folded on its first status change and read back as `>-`.
100
+
101
+ Match the fenced block on its info string, ```` ```yaml pin ````, never on position in the file. A
102
+ ticket whose block is missing or unparseable is a hard error naming the file — never a partial
103
+ ticket with empty fields.
104
+
105
+ ## Writing a ticket
106
+
107
+ `updateTicket` ([`../src/pin/board.mjs`](../src/pin/board.mjs)) **never serialises.** It splits the
108
+ file by index, so the slice from the closing
109
+ `---` onward is the *same substring* going out as coming in, and a mangled body is not a thing the
110
+ code can produce. It is asserted anyway, and the assertion has a negative control: a `js-yaml`
111
+ load/dump round-trip over the same ticket turns it red — 685 → 686 bytes, different hash, and
112
+ `labels: [pin, sample, hero]` exploded into a block sequence.
113
+
114
+ Do not refactor that into a round-trip to make it tidier. Temp-file-plus-rename, compare-and-swap
115
+ on `expect` before the rename, and refusals — never warnings — for a status off the allowlist, an id
116
+ naming no readable ticket, or an `expect` that disagrees with disk.
117
+
118
+ ## Screenshots
119
+
120
+ Canonical: `~/.orbytes/feedback-archive/<project>/pin-<NNN>.png`, **hardlinked** into
121
+ `<repo>/backlog/assets/` so the board can serve it — the same bytes under two names, zero extra
122
+ storage. Copy is the fallback across filesystems, and the log says which happened.
123
+ `backlog/assets/` is gitignored; the ticket markdown is committed.
124
+
125
+ Taken by a warm Playwright singleton against a freshly loaded page, motion frozen two ways, toolbar
126
+ hidden, clipped to the ticket's rect with `fullPage: true` — **`clip` is viewport-relative without
127
+ it**, so a pin below the fold would throw *"Clipped area is either empty or outside the resulting
128
+ image"*. Measured cost: ~2.0–2.4s per shot, warm or cold, dominated by `waitForLoadState`, not by
129
+ browser launch. You never wait on it: the ticket is written and the panel confirms in under 30ms,
130
+ on the same tick, and the shot lands afterwards on its own event.
131
+
132
+ **`playwright` is an optional peer and it has to be installed** — `npm i -D playwright && npx
133
+ playwright install chromium`. Nothing else gates screenshots: the dynamic import inside
134
+ `getBrowser()` is correct, `shots` defaults to `true`, and the one-sentence error the toolbar shows
135
+ when the import fails names the command. The board was built in a repo where playwright was already
136
+ a devDependency, and **the dependency did not travel with the code** when it moved here on
137
+ 2026-09-22 — so every ticket written in this repo before that day carries
138
+ `selector_matches: pending` and no image. The answer to *"why are there no screenshots?"* is an
139
+ uninstalled peer, not a missing feature.
140
+
141
+ **The ordering above was finally exercised end to end on 2026-09-22**, which until then it never had
142
+ been in this repo — the shot path had always failed fast, so a real two-second delay had never sat
143
+ between the two replies. Driving the shipped toolbar with a real mouse on the testbed's home page:
144
+ the status line showed *"PIN-004 written to … — screenshot pending"* **17.2ms** after the Create
145
+ ticket click (stamped in the page, on the button's own click handler, so no automation latency is
146
+ in the figure) and *"PIN-004 screenshot landed at …"* at **+1934ms**. A pin at document y=5108,
147
+ well below the fold, produced a 1358×53 PNG — its rect to the pixel — showing the pinned heading
148
+ and not the top of the page, and the repo copy and the archive copy shared one inode. The ordering
149
+ holds.
150
+
151
+ **`selector_matches` is counted on that fresh page**, immediately before the shutter, so the number
152
+ and the picture describe one DOM. `1` healthy · `0` dead · `2+` ambiguous · `pending` never tested.
153
+ A dead-selector ticket is **labelled, never discarded** — the screenshot, source file and
154
+ `outer_html` still make it workable by hand. The defect this guards against is silence, not
155
+ instability.
156
+
157
+ ## The browser ↔ server channel
158
+
159
+ Over the Vite HMR socket. No HTTP endpoint for writes from the toolbar, no port, no CORS.
160
+
161
+ Client, inside `init(canvas, app, server)` ([`../src/pin/toolbar.js`](../src/pin/toolbar.js)):
162
+ `server.send("orbytes-pin:create", payload)` · `server.on("orbytes-pin:created" | "orbytes-pin:shot", …)`
163
+
164
+ Server, inside `astro:server:setup({ server, toolbar })`:
165
+ `toolbar.on("orbytes-pin:create", …)` · `toolbar.send(…)`
166
+
167
+ **Ordering matters.** Write the ticket and reply on the same synchronous tick; take the screenshot
168
+ after, and send a second event when it lands. A ticket with no screenshot yet is valid; a ticket
169
+ someone waited two seconds for is not.
170
+
171
+ The in-page panel does **not** parse tickets. It reads the JSON the board already emits per card and
172
+ the write endpoint from the board's own markup. A second parser is how three earlier bugs here
173
+ happened.
174
+
175
+ **The toolbar app's options arrive as a virtual module**, `virtual:orbytes-pin/config.mjs`, written
176
+ by a `apply: "serve"` Vite plugin the integration registers (`pinConfigPlugin`, `../src/pin/index.mjs`).
177
+ Astro instantiates a dev-toolbar app entrypoint with `(canvas, app, server)` and nothing else, so
178
+ there is no other channel; the lab solves the identical problem the identical way with
179
+ `virtual:orbytes-lab/config.mjs`, and a second mechanism here would be a second thing to learn.
180
+ Today it carries one key, `route`, which was a literal `'/pin'` in `toolbar.js` until 2026-09-22 — a
181
+ site setting `pin: { route: "/__pin" }` got a panel that fetched a path the middleware does not
182
+ serve and showed an empty list — fixed 2026-09-22. One function normalises the trailing
183
+ slash, `boardRoute`, and both the middleware and the toolbar read it, so they cannot disagree.
184
+
185
+ ## The status line — one line, not a log
186
+
187
+ Decided 2026-09-22, replacing a running four-entry list that narrated every step. The log was
188
+ redundant: a button that opens the board is the one thing it added that nothing else on the panel
189
+ could say.
190
+
191
+ - **One line replaces it**, `#pin-status`. A new message replaces the one before it. That is what
192
+ makes it a status line and not a shorter log.
193
+ - **A failure has no timer.** `error` and `warn` show and stay — a refused ticket, a screenshot that
194
+ did not happen, a selector that dies on reload are the only things on this panel that nothing else
195
+ on screen can say. Click the line to dismiss it.
196
+ - **An acknowledgement clears itself** after `ACK_MS`. The ticket-written confirmation is kept
197
+ because the contract makes a point of it landing on the same synchronous tick, and a confirmation
198
+ nobody can see is not a confirmation.
199
+ - **Commentary was deleted at its call sites, not hidden here** — *"Sending to the dev server…"*,
200
+ *"Pick mode cancelled."*, *"Selection dropped."*, *"Scrolled back to the last pinned element."*
201
+ and the crumb's *"… comes from …"* are gone. Each described something the badge, the hint line,
202
+ the target box or the page itself had already shown. Do not add one back: if a new line is worth
203
+ saying, ask first whether the panel already shows it.
204
+
205
+ **`Tickets` and `Board ↗` are two different controls.** `Tickets` toggles the in-page rail over the
206
+ current page; `Board ↗` opens the full kanban at the resolved route in its own tab, under a named
207
+ window so a second click focuses the tab already open rather than stacking another.
208
+
209
+ ## Source attribution
210
+
211
+ Astro 7 does **not** emit `data-astro-source-file`, despite `compile.js` still passing
212
+ `annotateSourceFile` and the dev toolbar's audit app still reading the attributes. Verified three
213
+ ways, with a negative control: the legacy Go compiler's wasm on disk contains both strings; the Rust
214
+ binding Astro 7 uses contains zero. So this package stamps its own `data-orbytes-src`, repo-relative,
215
+ via a dev-only Vite `load` hook. **Expect it to break again** — Astro dropped these once before,
216
+ silently, with no deprecation.
217
+
218
+ A file matched by the `stamp` glob with no stampable element is a hard throw naming the file. That
219
+ is the guard, not an inconvenience: a warned-past file silently produces tickets with no source.
220
+
221
+ Selectors are built up to the **shortest form resolving to exactly one node**, verified against the
222
+ live document at each step. Ids anchor a selector only when they look authored — an allowlist, not a
223
+ blocklist, because a blocklist has to predict the next library's generated garbage. Transient state
224
+ classes are dropped by shape, not by name.
225
+
226
+ ## Dev only
227
+
228
+ `astro:server:setup` does not exist in a build, which is what makes the write path safe. Asserted
229
+ four times independently, and verified on real production builds — normal and staging — by grepping
230
+ `dist/` for every marker this package emits.
231
+
232
+ ## Gotchas that cost real time
233
+
234
+ - **`astro dev --ignore-lock` is refused in an agent environment** and the error reads as unrelated
235
+ to the flag. One background server per project; `astro dev status` finds it.
236
+ - **Astro 7's dev server binds IPv6 only.** `curl 127.0.0.1:<port>` returns http_code 000 and looks
237
+ exactly like a crash. Use `localhost`.
238
+ - **Restarting the dev server leaves the toolbar app 504ing until one hard page reload.** A Vite
239
+ re-optimize hazard, unfixed. A config touch restarts the server but Node's module cache keeps the
240
+ old integration — only a genuine stop and start reloads this package.
241
+ - **A symlinked workspace package defeats Vite's `node_modules` optimizer skip**, because
242
+ `preserveSymlinks` is false. This 504'd the entire dev toolbar, Astro's own five apps included,
243
+ until `optimizeDeps.exclude` named the two offending specifiers.
244
+ - **Testing the toolbar needs a real mouse hover to arm it.** Astro gates Escape-to-close on
245
+ `data-hidden` on `#dev-toolbar-root` *inside* the shadow root. A synthetic click leaves it
246
+ disarmed and the test cannot fail. `astro-dev-toolbar-button` elements can measure 0×0 — measure
247
+ the inner `<button>` in their shadow root.
248
+ - **Markers belong in the toolbar canvas, never `document.body`.** The screenshotter hides only
249
+ `astro-dev-toolbar`, so a page-parented marker is photographed into every later ticket.
250
+
251
+ ## Chrome
252
+
253
+ Neutral tool chrome, never the host site's styling — decided for this board and, separately, for
254
+ the component lab. A standard board that reads the same in every project it is installed in.
255
+
256
+ Every value is a custom property in one namespaced block (`--pin-*`, on `.pin-chrome`). No literal
257
+ colours outside it, and **no bare element selectors** anywhere — those inherit down into the
258
+ previewed page.
259
+
260
+ **The `--pin-*` namespace survived the merge and is still the board's own** (2026-09-22). Repointing
261
+ it at the lab's `--lab-*` tokens is the shared-chrome design — lab phase 2 — and was deliberately
262
+ NOT done while moving the code: a restyle and a move in one change means a visual difference nobody
263
+ can attribute. The two namespaces cannot collide, so nothing is urgent about it.