@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.
- package/README.md +261 -116
- package/bin/pin-gallery.mjs +53 -19
- package/defaults.mjs +72 -20
- package/dist/core/virtual-module/virtual-routes.js +12 -1
- package/docs/PIN-CONTRACT.md +84 -10
- package/docs/PIN.md +117 -37
- package/index.d.ts +41 -23
- package/index.mjs +46 -91
- package/package.json +7 -3
- package/src/Home.astro +166 -264
- package/src/LabHead.astro +37 -1047
- package/src/chrome/ActionsMenu.astro +97 -0
- package/src/chrome/ComponentCard.astro +76 -0
- package/src/chrome/Icon.astro +21 -0
- package/src/chrome/LICENSE-icons +43 -0
- package/src/chrome/Nav.astro +131 -0
- package/src/chrome/Panel.astro +117 -0
- package/src/chrome/Properties.astro +104 -0
- package/src/chrome/SectionsTree.astro +128 -0
- package/src/chrome/Shell.astro +120 -0
- package/src/chrome/Sprite.astro +23 -0
- package/src/chrome/StoryView.astro +192 -0
- package/src/chrome/Tree.astro +86 -0
- package/src/chrome/ViewportControls.astro +173 -0
- package/src/chrome/ViewportStage.astro +55 -0
- package/src/chrome/fonts/OFL.txt +93 -0
- package/src/chrome/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/src/chrome/icons.ts +68 -0
- package/src/chrome/marks-client.ts +75 -0
- package/src/chrome/model.ts +156 -0
- package/src/chrome/navbar-client.ts +324 -0
- package/src/chrome/params-client.ts +434 -0
- package/src/chrome/pins-data.ts +63 -0
- package/src/chrome/shell-client.ts +468 -0
- package/src/chrome/site-data.ts +230 -0
- package/src/chrome/trees.ts +257 -0
- package/src/chrome/viewport-client.ts +405 -0
- package/src/chrome/views/Assets.astro +125 -0
- package/src/chrome/views/Pages.astro +214 -0
- package/src/chrome/views/Placeholder.astro +37 -0
- package/src/chrome/views/Tasks.astro +79 -0
- package/src/core/LICENSE-astrobook +21 -0
- package/src/core/lib/components/home.astro +4 -2
- package/src/core/lib/pages/story.astro +12 -10
- package/src/core/utils/kebab-case.ts +2 -2
- package/src/core/virtual-module/virtual-routes.ts +20 -4
- package/src/pin/board.mjs +414 -190
- package/src/pin/index.mjs +67 -22
- package/src/pin/tickets.mjs +6 -5
- package/src/pin/toolbar.js +82 -4
- package/src/shell/Browse.astro +131 -353
- package/src/shell/Viewport.astro +22 -1315
- package/src/shell/lab-index.ts +28 -18
- 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/app.astro +5 -7
- package/src/ui/components/preview-layout.astro +17 -0
- package/src/ui/components/theme-script.astro +17 -5
- package/src/ui/lab.css +3754 -371
- package/virtual.d.ts +13 -4
- package/bin/lab-cull.mjs +0 -401
- package/src/shell/CardGrid.astro +0 -297
- package/src/ui/components/build-path.ts +0 -13
- package/src/ui/components/build-tree.ts +0 -108
- package/src/ui/components/collapse-duration.ts +0 -28
- package/src/ui/components/compress-terms.ts +0 -10
- package/src/ui/components/dashboard-layout.astro +0 -39
- package/src/ui/components/home.astro +0 -65
- package/src/ui/components/layout.astro +0 -110
- package/src/ui/components/sidebar-button-fullscreen.astro +0 -38
- package/src/ui/components/sidebar-button-search.astro +0 -23
- package/src/ui/components/sidebar-button-theme.astro +0 -9
- package/src/ui/components/sidebar-button.astro +0 -24
- package/src/ui/components/sidebar-resize-handle.astro +0 -74
- package/src/ui/components/sidebar-search-panel.astro +0 -41
- package/src/ui/components/sidebar-search-script.ts +0 -103
- package/src/ui/components/sidebar-title.astro +0 -17
- package/src/ui/components/sidebar-tree-node.astro +0 -143
- package/src/ui/components/sidebar-tree.astro +0 -84
- package/src/ui/components/sidebar.astro +0 -29
- package/src/ui/components/theme-toggle.astro +0 -63
package/bin/pin-gallery.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// pin-gallery — render every pin ticket as ONE standalone kanban on disk.
|
|
3
3
|
//
|
|
4
|
-
// The board itself lives at
|
|
4
|
+
// The board itself lives at `<subpath>/tasks` on `astro dev` now (`/lab/tasks` by default, or the
|
|
5
|
+
// pin half's `route` when it runs without the lab), generated from `backlog/tasks/` on every
|
|
5
6
|
// request, so looking at it is a refresh and this script is no longer part of that loop. What it
|
|
6
7
|
// is still for is the file: a page that opens with no dev server running — on a plane, on another
|
|
7
8
|
// machine, attached to a message — and a snapshot of the board at a moment in time.
|
|
@@ -15,15 +16,16 @@
|
|
|
15
16
|
// npx orbytes-pin-gallery # → backlog/gallery.html
|
|
16
17
|
// npx orbytes-pin-gallery --out x.html
|
|
17
18
|
// npx orbytes-pin-gallery --backlog-dir feedback # a site that moved its board
|
|
19
|
+
// npx orbytes-pin-gallery --help # the usage; writes nothing
|
|
18
20
|
//
|
|
19
|
-
// IT RUNS IN THE CONSUMER'S REPO, NOT ITS OWN
|
|
20
|
-
//
|
|
21
|
-
// the
|
|
22
|
-
//
|
|
21
|
+
// IT RUNS IN THE CONSUMER'S REPO, NOT ITS OWN. The root is walked up from `process.cwd()`, never
|
|
22
|
+
// from this file's location, which since the merge into @orbytes/astrolab (2026-09-22) is somewhere
|
|
23
|
+
// under the consumer's node_modules and, when the package is workspace-linked, resolves to THIS
|
|
24
|
+
// repo instead of theirs. A gallery of the
|
|
23
25
|
// lab's own (empty) backlog, written into the lab's own checkout, would exit 0 and look fine.
|
|
24
26
|
//
|
|
25
27
|
// EVERY line of the board — the parser, the cards, the columns, the CSS — comes from
|
|
26
|
-
// `../src/pin/board.mjs`, which the
|
|
28
|
+
// `../src/pin/board.mjs`, which the live board renders with as well. Nothing about a ticket is read
|
|
27
29
|
// or drawn here. That is the whole shape of this file, and it is not a preference: this script
|
|
28
30
|
// carried its own copy of the parsing and rendering until 2026-09-21 and that copy produced three
|
|
29
31
|
// separate bugs, each of them a drift from the package's own reader — a frontmatter parser that
|
|
@@ -32,19 +34,17 @@
|
|
|
32
34
|
// A second implementation of this page would be the fourth. Add nothing here; add it to board.mjs.
|
|
33
35
|
//
|
|
34
36
|
// The page opens straight from file:// — image sources stay relative to the output file, so the
|
|
35
|
-
// HTML stays a few KB and the
|
|
37
|
+
// HTML stays a few KB and the PNGs are never inlined.
|
|
36
38
|
|
|
37
39
|
import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
38
40
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
39
41
|
|
|
40
|
-
// `../src/pin/board.mjs` and not the package root
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// no bundler to run.
|
|
42
|
+
// `../src/pin/board.mjs` and not the package root. The root does load in plain Node now — it
|
|
43
|
+
// imports the compiled core in ../dist/core — but it brings the lab integration and that core
|
|
44
|
+
// with it for a job that needs neither. The board module imports only node:fs and node:path, and it
|
|
45
|
+
// does not reach shot.mjs — so a static HTML renderer needs no browser and no bundler to run.
|
|
44
46
|
import { collectTickets, renderBoard, findRepoRoot } from "../src/pin/board.mjs";
|
|
45
47
|
|
|
46
|
-
const REPO = findRepoRoot(process.cwd());
|
|
47
|
-
|
|
48
48
|
/* -------------------------------------------------------------------- main */
|
|
49
49
|
|
|
50
50
|
const args = process.argv.slice(2);
|
|
@@ -53,6 +53,41 @@ const flag = (name) => {
|
|
|
53
53
|
return at !== -1 && args[at + 1] ? args[at + 1] : null;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
const USAGE = `Usage: orbytes-pin-gallery [--out <file>] [--backlog-dir <dir>]
|
|
57
|
+
|
|
58
|
+
Writes every pin ticket in <repo>/<backlog-dir>/tasks/, cancelled ones included, as one read-only
|
|
59
|
+
kanban page that opens with no dev server running. <repo> is the current directory or the nearest
|
|
60
|
+
one above it that holds .git, so run it from inside the site's repo.
|
|
61
|
+
|
|
62
|
+
--out <file> where to write the page, relative to the current directory
|
|
63
|
+
(default: <repo>/<backlog-dir>/gallery.html)
|
|
64
|
+
--backlog-dir <dir> the board directory, relative to <repo> (default: backlog)
|
|
65
|
+
-h, --help print this and exit, writing nothing
|
|
66
|
+
|
|
67
|
+
If any ticket cannot be read it writes nothing and exits 1.
|
|
68
|
+
The live board is <subpath>/tasks on astro dev (/lab/tasks by default).`;
|
|
69
|
+
|
|
70
|
+
// Every argument is accounted for, or the run is refused. Until 2026-09-24 nothing checked them:
|
|
71
|
+
// a typo — `--output`, `--out=x.html`, `--help` itself — was ignored and the default gallery was
|
|
72
|
+
// written anyway, exit 0, which reads exactly like the command having done what was asked.
|
|
73
|
+
const VALUE_FLAGS = ["--out", "--backlog-dir"];
|
|
74
|
+
for (let i = 0; i < args.length; i++) {
|
|
75
|
+
const arg = args[i];
|
|
76
|
+
if (arg === "--help" || arg === "-h") {
|
|
77
|
+
console.log(USAGE);
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
if (VALUE_FLAGS.includes(arg)) {
|
|
81
|
+
i++; // its value, read below by `flag()`
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
console.error(`pin-gallery: unknown argument ${JSON.stringify(arg)} — nothing was written.\n`);
|
|
85
|
+
console.error(USAGE);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const REPO = findRepoRoot(process.cwd());
|
|
90
|
+
|
|
56
91
|
// The board directory, and the reason this flag exists at all.
|
|
57
92
|
//
|
|
58
93
|
// A binary cannot read the integration's options — it runs in a different process, from a shell,
|
|
@@ -67,7 +102,7 @@ const outPath = flag("--out")
|
|
|
67
102
|
? resolve(process.cwd(), flag("--out"))
|
|
68
103
|
: join(REPO, backlogDir, "gallery.html");
|
|
69
104
|
|
|
70
|
-
// `includeCancelled`: this file is the offline twin of
|
|
105
|
+
// `includeCancelled`: this file is the offline twin of the live board, so it shows what that shows —
|
|
71
106
|
// archive rail included. The default is the other way round precisely because every OTHER reader
|
|
72
107
|
// is agent-facing, and a cancelled ticket must not reach an agent's context (ruled 2026-09-22).
|
|
73
108
|
const { tickets, broken, paths } = collectTickets(REPO, { backlogDir, includeCancelled: true });
|
|
@@ -84,7 +119,7 @@ const { tickets, broken, paths } = collectTickets(REPO, { backlogDir, includeCan
|
|
|
84
119
|
// on disk is left exactly as it was — stale, but the message below says so in as many words, which
|
|
85
120
|
// is the difference between a stale file and a lie.
|
|
86
121
|
//
|
|
87
|
-
// The
|
|
122
|
+
// The live board does the opposite and shows the panel instead, because a live page that refuses is a
|
|
88
123
|
// blank screen with nothing to act on. A file that refuses is simply the file you already had.
|
|
89
124
|
if (broken.length) {
|
|
90
125
|
console.error(`pin-gallery: refusing to write — ${broken.length} ticket(s) could not be read:`);
|
|
@@ -92,7 +127,7 @@ if (broken.length) {
|
|
|
92
127
|
console.error(` ${tickets.length} readable ticket(s) were NOT written anywhere.`);
|
|
93
128
|
console.error(` An agent reading the board hits the same wall: listTickets() throws on the first`);
|
|
94
129
|
console.error(` of these and returns no tickets at all. Fix the ticket(s) and run again.`);
|
|
95
|
-
console.error(` The
|
|
130
|
+
console.error(` The live board (<subpath>/tasks, /lab/tasks by default) lists them by name on the dev server, if it is running.`);
|
|
96
131
|
if (existsSync(outPath)) {
|
|
97
132
|
const stamp = statSync(outPath).mtime.toLocaleString("en-AU", { dateStyle: "medium", timeStyle: "short" });
|
|
98
133
|
console.error(` ${relative(REPO, outPath) || outPath} was left untouched (written ${stamp}) and is now out of date.`);
|
|
@@ -108,8 +143,7 @@ const assetHref = (t) => (t.shotAbs ? relative(outDir, t.shotAbs).split(sep).joi
|
|
|
108
143
|
// The directory may not exist yet: on a fresh install `backlog/` is only created when the first
|
|
109
144
|
// ticket is written, and this command can run before that — or be pointed somewhere else entirely
|
|
110
145
|
// with `--out`. Measured 2026-09-22 on a clean project: without this the command died with a raw
|
|
111
|
-
// `ENOENT ... open 'backlog/gallery.html'` stack trace.
|
|
112
|
-
// handles the same case.
|
|
146
|
+
// `ENOENT ... open 'backlog/gallery.html'` stack trace.
|
|
113
147
|
mkdirSync(outDir, { recursive: true });
|
|
114
148
|
writeFileSync(outPath, renderBoard(tickets, broken, { assetHref }), "utf8");
|
|
115
149
|
|
|
@@ -117,5 +151,5 @@ const missing = tickets.filter((t) => !assetHref(t)).length;
|
|
|
117
151
|
console.log(`pin-gallery: ${tickets.length} ticket(s), ${missing} without a screenshot`);
|
|
118
152
|
console.log(` → ${relative(REPO, outPath) || outPath}`);
|
|
119
153
|
console.log(` read from ${relative(REPO, paths.tasks)}`);
|
|
120
|
-
console.log(` the live board is
|
|
154
|
+
console.log(` the live board is <subpath>/tasks on the dev server (/lab/tasks by default); this file is the read-only standalone copy`);
|
|
121
155
|
if (!existsSync(paths.assets)) console.log(` note: ${relative(REPO, paths.assets)} does not exist yet`);
|
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,74 @@ 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
|
-
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The screen sizes the viewport offers — a per-site setting (decided 2026-09-24), defaulting to the
|
|
39
|
+
* orbytes scale.
|
|
40
|
+
*
|
|
41
|
+
* - `design` — the widths the design is drawn at: the navbar's device switch, in this order. The
|
|
42
|
+
* `icon` names one of the chrome's icons (monitor, tablet, phone-landscape, phone).
|
|
43
|
+
* - `breakpoints` — the lower edge of each CSS band, widest first; the last is 0. The readout
|
|
44
|
+
* names the band the frame is in, and the presets offer both sides of every edge (992 and 991).
|
|
45
|
+
* - `devices` — named sizes that set width AND height.
|
|
46
|
+
*
|
|
47
|
+
* @typedef {{ label: string; width: number; icon: string }} DesignWidth
|
|
48
|
+
* @typedef {{ name: string; min: number }} Breakpoint
|
|
49
|
+
* @typedef {{ label: string; width: number; height: number }} Device
|
|
50
|
+
* @typedef {{ design: DesignWidth[]; breakpoints: Breakpoint[]; devices: Device[] }} Viewports
|
|
51
|
+
*/
|
|
52
|
+
/** @type {Viewports} */
|
|
53
|
+
export const DEFAULT_VIEWPORTS = {
|
|
54
|
+
design: [
|
|
55
|
+
{ label: "Desktop", width: 1440, icon: "monitor" },
|
|
56
|
+
{ label: "Tablet", width: 834, icon: "tablet" },
|
|
57
|
+
{ label: "Phone landscape", width: 550, icon: "phone-landscape" },
|
|
58
|
+
{ label: "Phone", width: 390, icon: "phone" },
|
|
59
|
+
],
|
|
60
|
+
breakpoints: [
|
|
61
|
+
{ name: "desktop", min: 992 },
|
|
62
|
+
{ name: "tablet", min: 768 },
|
|
63
|
+
{ name: "phone landscape", min: 480 },
|
|
64
|
+
{ name: "phone", min: 0 },
|
|
65
|
+
],
|
|
66
|
+
devices: [
|
|
67
|
+
{ label: "iPhone 15", width: 390, height: 844 },
|
|
68
|
+
{ label: "iPhone 15 Pro Max", width: 430, height: 932 },
|
|
69
|
+
{ label: "iPad", width: 834, height: 1194 },
|
|
70
|
+
{ label: "iPad landscape", width: 1194, height: 834 },
|
|
71
|
+
{ label: "MacBook", width: 1440, height: 900 },
|
|
72
|
+
{ label: "Wide", width: 1920, height: 1080 },
|
|
73
|
+
{ label: "Ultra-wide", width: 2560, height: 1080 },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** @param {unknown} value @returns {Viewports} */
|
|
78
|
+
const resolveViewports = (value) => {
|
|
79
|
+
const given = value && typeof value === "object" ? /** @type {Partial<Viewports>} */ (value) : {};
|
|
80
|
+
const list = (v, fallback) => (Array.isArray(v) && v.length ? v : fallback);
|
|
81
|
+
return {
|
|
82
|
+
design: list(given.design, DEFAULT_VIEWPORTS.design).map((d) => ({
|
|
83
|
+
label: String(d.label ?? `${d.width}`),
|
|
84
|
+
width: Number(d.width),
|
|
85
|
+
icon: String(d.icon ?? "monitor"),
|
|
86
|
+
})),
|
|
87
|
+
breakpoints: list(given.breakpoints, DEFAULT_VIEWPORTS.breakpoints)
|
|
88
|
+
.map((b) => ({ name: String(b.name), min: Number(b.min) || 0 }))
|
|
89
|
+
.sort((a, b) => b.min - a.min),
|
|
90
|
+
devices: list(given.devices, DEFAULT_VIEWPORTS.devices).map((d) => ({
|
|
91
|
+
label: String(d.label),
|
|
92
|
+
width: Number(d.width),
|
|
93
|
+
height: Number(d.height),
|
|
94
|
+
})),
|
|
95
|
+
};
|
|
39
96
|
};
|
|
40
97
|
|
|
41
98
|
/** `./src/lab/` and `src/lab` both mean `src/lab`. @param {string} value */
|
|
@@ -57,19 +114,17 @@ export const normaliseSubpath = (value) => {
|
|
|
57
114
|
* @property {string} id the first path segment under `directory`
|
|
58
115
|
* @property {string} label shown on the tier card and the folder pages
|
|
59
116
|
* @property {boolean} responsive section versions: responsive marks, page-width thumbnails
|
|
60
|
-
* @property {boolean} cullable may be marked for deletion
|
|
61
117
|
*/
|
|
62
118
|
|
|
63
119
|
/** @param {string | Partial<LabTier>} tier @returns {LabTier} */
|
|
64
120
|
const resolveTier = (tier) => {
|
|
65
121
|
const raw = typeof tier === "string" ? { id: tier } : { ...tier };
|
|
66
122
|
const id = String(raw.id ?? "");
|
|
67
|
-
const role = /** @type {Record<string, {responsive?: boolean
|
|
123
|
+
const role = /** @type {Record<string, {responsive?: boolean}>} */ (TIER_ROLES)[id] ?? {};
|
|
68
124
|
return {
|
|
69
125
|
id,
|
|
70
126
|
label: raw.label ?? (id ? id.charAt(0).toUpperCase() + id.slice(1) : "Root"),
|
|
71
127
|
responsive: raw.responsive ?? role.responsive ?? false,
|
|
72
|
-
cullable: raw.cullable ?? role.cullable ?? false,
|
|
73
128
|
};
|
|
74
129
|
};
|
|
75
130
|
|
|
@@ -84,11 +139,10 @@ const resolveTier = (tier) => {
|
|
|
84
139
|
* @property {string | null} feedbucketKey
|
|
85
140
|
* @property {LabTier[]} tiers
|
|
86
141
|
* @property {string | null} sectionsTier the id of the tier holding the responsive role
|
|
87
|
-
* @property {string | null} cullTier the id of the tier holding the cull role
|
|
88
142
|
* @property {string | null} responsiveDir `<directory>/<sectionsTier>/`, or null
|
|
89
|
-
* @property {string | null} cullDir `<directory>/<cullTier>/`, or null
|
|
90
143
|
* @property {string} responsiveFile `<directory>/responsive.json`
|
|
91
|
-
* @property {
|
|
144
|
+
* @property {Viewports} viewports the viewport's screen sizes (DEFAULT_VIEWPORTS)
|
|
145
|
+
* @property {null} tasks filled in by the integration when the pin board runs
|
|
92
146
|
*/
|
|
93
147
|
|
|
94
148
|
/**
|
|
@@ -100,7 +154,6 @@ export function resolveLabOptions(options = {}) {
|
|
|
100
154
|
const directory = normaliseDirectory(options.directory ?? DEFAULT_DIRECTORY);
|
|
101
155
|
const tiers = (Array.isArray(options.tiers) ? options.tiers : DEFAULT_TIERS).map(resolveTier);
|
|
102
156
|
const sections = tiers.find((tier) => tier.responsive) ?? null;
|
|
103
|
-
const cull = tiers.find((tier) => tier.cullable) ?? null;
|
|
104
157
|
return {
|
|
105
158
|
directory,
|
|
106
159
|
subpath: normaliseSubpath(options.subpath ?? DEFAULT_SUBPATH),
|
|
@@ -111,10 +164,9 @@ export function resolveLabOptions(options = {}) {
|
|
|
111
164
|
feedbucketKey: options.feedbucketKey ? String(options.feedbucketKey) : null,
|
|
112
165
|
tiers,
|
|
113
166
|
sectionsTier: sections ? sections.id : null,
|
|
114
|
-
cullTier: cull ? cull.id : null,
|
|
115
167
|
responsiveDir: sections ? `${directory}/${sections.id}/` : null,
|
|
116
|
-
cullDir: cull ? `${directory}/${cull.id}/` : null,
|
|
117
168
|
responsiveFile: `${directory}/responsive.json`,
|
|
118
|
-
|
|
169
|
+
viewports: resolveViewports(options.viewports),
|
|
170
|
+
tasks: null,
|
|
119
171
|
};
|
|
120
172
|
}
|
|
@@ -51,7 +51,18 @@ async function getVirtualRoutes(rootDir, codegenDir, logger, dashboardSubpath, p
|
|
|
51
51
|
}
|
|
52
52
|
function createVirtualRouteComponent(route) {
|
|
53
53
|
const modName = `astrobook_module_${route.storyModule.id}`.replaceAll(/\W+/g, "_").replaceAll(/_+/g, "_").replace(/_+$/, "");
|
|
54
|
-
|
|
54
|
+
if (route.props.hasSidebar) {
|
|
55
|
+
return `
|
|
56
|
+
---
|
|
57
|
+
// Automatically generated by Astrobook
|
|
58
|
+
|
|
59
|
+
import StoryPage from '${STORY_PAGE}';
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
<StoryPage story={'${route.props.story}'} hasSidebar={true} />
|
|
63
|
+
`.trim();
|
|
64
|
+
}
|
|
65
|
+
const page = PREVIEW_PAGE;
|
|
55
66
|
return `
|
|
56
67
|
---
|
|
57
68
|
// Automatically generated by Astrobook
|
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", …)`
|
|
@@ -182,6 +204,48 @@ site setting `pin: { route: "/__pin" }` got a panel that fetched a path the midd
|
|
|
182
204
|
serve and showed an empty list — fixed 2026-09-22. One function normalises the trailing
|
|
183
205
|
slash, `boardRoute`, and both the middleware and the toolbar read it, so they cannot disagree.
|
|
184
206
|
|
|
207
|
+
**The board page moved into the lab on 2026-09-24.** Through `orbytesLab()` the route is
|
|
208
|
+
`<subpath>/tasks` and the page there is an Astro page in the lab's chrome
|
|
209
|
+
(`../src/chrome/views/Tasks.astro`), not this middleware's standalone document. The panel's contract
|
|
210
|
+
with it is unchanged, because the page renders the SAME markup from the same function
|
|
211
|
+
(`boardParts`): it still reads `[data-board]`, its `data-api`, every `[data-col][data-status]`, the
|
|
212
|
+
`script.card-data` payloads and `.broken li code` off the page at `route`. A change to those
|
|
213
|
+
selectors in `board.mjs` is a change to this contract.
|
|
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
|
+
|
|
185
249
|
## The status line — one line, not a log
|
|
186
250
|
|
|
187
251
|
Decided 2026-09-22, replacing a running four-entry list that narrated every step. The log was
|
|
@@ -215,7 +279,7 @@ binding Astro 7 uses contains zero. So this package stamps its own `data-orbytes
|
|
|
215
279
|
via a dev-only Vite `load` hook. **Expect it to break again** — Astro dropped these once before,
|
|
216
280
|
silently, with no deprecation.
|
|
217
281
|
|
|
218
|
-
A file
|
|
282
|
+
A file under a `stamp` directory with no stampable element is a hard throw naming the file. That
|
|
219
283
|
is the guard, not an inconvenience: a warned-past file silently produces tickets with no source.
|
|
220
284
|
|
|
221
285
|
Selectors are built up to the **shortest form resolving to exactly one node**, verified against the
|
|
@@ -247,15 +311,25 @@ four times independently, and verified on real production builds — normal and
|
|
|
247
311
|
the inner `<button>` in their shadow root.
|
|
248
312
|
- **Markers belong in the toolbar canvas, never `document.body`.** The screenshotter hides only
|
|
249
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.
|
|
250
321
|
|
|
251
322
|
## Chrome
|
|
252
323
|
|
|
253
324
|
Neutral tool chrome, never the host site's styling — decided for this board and, separately, for
|
|
254
325
|
the component lab. A standard board that reads the same in every project it is installed in.
|
|
255
326
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
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.
|
|
259
333
|
|
|
260
334
|
**The `--pin-*` namespace survived the merge and is still the board's own** (2026-09-22). Repointing
|
|
261
335
|
it at the lab's `--lab-*` tokens is the shared-chrome design — lab phase 2 — and was deliberately
|