@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/src/pin/index.mjs
CHANGED
|
@@ -6,18 +6,19 @@
|
|
|
6
6
|
// ONE integration and gets both:
|
|
7
7
|
//
|
|
8
8
|
// import orbytesLab from "@orbytes/astrolab";
|
|
9
|
-
// integrations: [ includeLab ? orbytesLab({ css: [...] }) : null ] // /lab and /
|
|
9
|
+
// integrations: [ includeLab ? orbytesLab({ css: [...] }) : null ] // /lab and /lab/tasks
|
|
10
10
|
// integrations: [ includeLab ? orbytesLab({ pin: false }) : null ] // /lab alone
|
|
11
11
|
//
|
|
12
12
|
// `orbytesLab()` returns an array Astro flattens, and this integration is one of its entries — see
|
|
13
13
|
// ../../index.mjs, which also hands it the lab's RESOLVED subpath so the board's "Lab" link points
|
|
14
|
-
// at wherever the lab actually is. Calling this module directly still works
|
|
15
|
-
//
|
|
14
|
+
// at wherever the lab actually is. Calling this module directly still works — it is exported as
|
|
15
|
+
// `orbytesPin` from the package root and at `@orbytes/astrolab/pin` — and nothing about it assumes
|
|
16
|
+
// the lab is present.
|
|
16
17
|
//
|
|
17
18
|
// No cloud, no API key, no MCP, no second process — ../../docs/PIN-CONTRACT.md. It replaces the
|
|
18
19
|
// hosted-widget → issue-tracker feedback chain for a solo build pass, and only for that.
|
|
19
20
|
//
|
|
20
|
-
//
|
|
21
|
+
// Four jobs:
|
|
21
22
|
// 1. stamp `data-orbytes-src="<repo-relative path>"` on the first top-level element of every
|
|
22
23
|
// `.astro` file under the configured directories, so the picker can name the file an element
|
|
23
24
|
// came from (./source-stamp.mjs — Astro 7 emits no source attribute of its own).
|
|
@@ -28,19 +29,28 @@
|
|
|
28
29
|
// 4. serve the KANBAN at `<route>` — every ticket in `backlog/tasks`, read fresh on each
|
|
29
30
|
// request — its screenshots at `<route>/assets/*`, and the one write endpoint at
|
|
30
31
|
// `<route>/api/ticket`, all from the dev server you are already on (./board.mjs, which
|
|
31
|
-
// `orbytes-pin-gallery` renders the standalone file with too).
|
|
32
|
+
// `orbytes-pin-gallery` renders the standalone file with too). Inside the lab the kanban
|
|
33
|
+
// itself is not this middleware's: it is ../chrome/views/Tasks.astro, injected at
|
|
34
|
+
// `<route>` and `<route>/all`, and the middleware keeps the screenshots and the endpoint.
|
|
32
35
|
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
36
|
+
// WHERE `<route>` IS, AND WHAT IT SHADOWS. On its own this integration defaults `route` to `/pin`
|
|
37
|
+
// and draws the board there as middleware. Through `orbytesLab()` it is `<subpath>/tasks`
|
|
38
|
+
// (`/lab/tasks` by default), and the middleware answers `/pin` — the board's address until
|
|
39
|
+
// 2026-09-24 — with a 302 to it, unless `route` is itself `/pin`. The middleware is installed
|
|
40
|
+
// before Astro's own request handler, so a site with its own `src/pages/pin.astro` gets the board
|
|
41
|
+
// (alone) or the redirect (in the lab) instead of its page, in dev only, with nothing on screen to
|
|
42
|
+
// say why. On its own the collision is a one-liner to avoid: `pin: { route: "/__pin" }`. Inside
|
|
43
|
+
// the lab that is not enough: the redirect fires for every `route` except `/pin`, and a `route` of
|
|
44
|
+
// `/pin` injects the Tasks views there instead, so only `pin: false` frees the path. Said here, in
|
|
45
|
+
// ../../docs/PIN.md and in ../../README.md, because a silent shadow is only findable if somebody
|
|
46
|
+
// wrote it down.
|
|
39
47
|
//
|
|
40
|
-
//
|
|
48
|
+
// On its own the board is MIDDLEWARE, not an injected route, and the choice is deliberate. (Inside
|
|
49
|
+
// the lab it is an injected route after all — the Tasks views, injected in `astro:config:setup`
|
|
50
|
+
// past the dev-only assertion, so they are no more able to reach a build.) The lab injects its
|
|
41
51
|
// pages because they are prerendered; this one is generated per request from files outside the
|
|
42
52
|
// site, so it belongs with the lab's other half — the mark APIs it serves from `astro:server:setup`.
|
|
43
|
-
// That hook does not exist in a build, so
|
|
53
|
+
// That hook does not exist in a build, so `<route>` cannot leak into `dist/` even by accident, and
|
|
44
54
|
// nothing has to be marked `prerender: false` in a project with `output: "static"` and no adapter.
|
|
45
55
|
// ── backlog.md is retired (2026-09-22) ──────────────────────────────────────────────────────────
|
|
46
56
|
// Until today `<route>` was read-only and pointed at backlog.md's own web UI — a second server on
|
|
@@ -188,7 +198,8 @@ const DEFAULTS = {
|
|
|
188
198
|
/** Site-relative directories whose `.astro` files get the source stamp. */
|
|
189
199
|
stamp: ["src/lab/sections", "src/components"],
|
|
190
200
|
/**
|
|
191
|
-
*
|
|
201
|
+
* Site-relative `.astro` paths exempt from the stamp's hard failure (a repo-relative path is
|
|
202
|
+
* accepted too — ./source-stamp.mjs resolves each entry both ways). A file whose template has
|
|
192
203
|
* no element to stamp throws by design; this is the only way past it, and it is a deliberate,
|
|
193
204
|
* named decision per file rather than a warning that lets every such file through silently.
|
|
194
205
|
*/
|
|
@@ -216,10 +227,12 @@ const DEFAULTS = {
|
|
|
216
227
|
shots: true,
|
|
217
228
|
/**
|
|
218
229
|
* Where the board is served, dev only. `<route>/assets/*` serves its screenshots and
|
|
219
|
-
* `<route>/api/ticket` is the one write endpoint.
|
|
230
|
+
* `<route>/api/ticket` is the one write endpoint. `orbytesLab()` sets it to `<subpath>/tasks`;
|
|
231
|
+
* `/pin` is the default only when this integration is used on its own.
|
|
220
232
|
*
|
|
221
233
|
* It is MIDDLEWARE and it shadows a host page at the same path (see the note at the top of this
|
|
222
|
-
* file).
|
|
234
|
+
* file). Used on its own, a site with its own `/pin` page sets something else here; inside the
|
|
235
|
+
* lab no value frees `/pin` (see the note at the top of this file).
|
|
223
236
|
*/
|
|
224
237
|
route: "/pin",
|
|
225
238
|
/**
|
|
@@ -232,6 +245,14 @@ const DEFAULTS = {
|
|
|
232
245
|
appId: "orbytes-pin",
|
|
233
246
|
appName: "Pin",
|
|
234
247
|
icon: "bug",
|
|
248
|
+
/**
|
|
249
|
+
* Set by `orbytesLab()` (../../index.mjs), never by a consumer: `{ shared }`, the object the two
|
|
250
|
+
* halves talk through. When present, the board is drawn IN THE LAB'S CHROME at `<route>` and
|
|
251
|
+
* `<route>/all` (../chrome/views/Tasks.astro, injected here, dev only) instead of as this
|
|
252
|
+
* middleware's standalone page, and `/pin` — the board's address until 2026-09-24 — redirects
|
|
253
|
+
* there. Absent, the pin half behaves exactly as it did on its own.
|
|
254
|
+
*/
|
|
255
|
+
lab: null,
|
|
235
256
|
};
|
|
236
257
|
|
|
237
258
|
/** The dev server's own origin, read from Vite. Never hardcode 4321 — Astro picks a free port. */
|
|
@@ -257,7 +278,7 @@ export default function orbytesPin(options = {}) {
|
|
|
257
278
|
return {
|
|
258
279
|
name: "orbytes-pin",
|
|
259
280
|
hooks: {
|
|
260
|
-
"astro:config:setup": ({ config: astroConfig, command, updateConfig, addDevToolbarApp, logger }) => {
|
|
281
|
+
"astro:config:setup": ({ config: astroConfig, command, updateConfig, addDevToolbarApp, injectRoute, logger }) => {
|
|
261
282
|
// Assertion one. In `build`, `preview` or `sync` this integration registers nothing at
|
|
262
283
|
// all — no plugin, no toolbar app, no stamp.
|
|
263
284
|
if (command !== "dev") {
|
|
@@ -288,6 +309,21 @@ export default function orbytesPin(options = {}) {
|
|
|
288
309
|
}
|
|
289
310
|
project = String(config.project ?? projectName(repoRoot));
|
|
290
311
|
|
|
312
|
+
// Inside the lab: the Tasks views are Astro pages in the lab's chrome, injected HERE — past
|
|
313
|
+
// assertion one, so they exist in `astro dev` and in no build — and the lab is told where
|
|
314
|
+
// they are through the shared object, which is how its Tasks group knows to appear.
|
|
315
|
+
if (config.lab && config.lab.shared) {
|
|
316
|
+
const route = boardRoute(config.route);
|
|
317
|
+
injectRoute({ pattern: `${route}/[...view]`, entrypoint: file("../chrome/views/Tasks.astro"), prerender: true });
|
|
318
|
+
config.lab.shared.tasks = {
|
|
319
|
+
base: route,
|
|
320
|
+
api: `${route}/api/ticket`,
|
|
321
|
+
assets: `${route}/assets/`,
|
|
322
|
+
repoRoot,
|
|
323
|
+
backlogDir: config.backlogDir,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
291
327
|
addDevToolbarApp({
|
|
292
328
|
id: config.appId,
|
|
293
329
|
name: config.appName,
|
|
@@ -402,7 +438,7 @@ export default function orbytesPin(options = {}) {
|
|
|
402
438
|
// The wall, and its screenshots. Registered here rather than as an injected route (see the
|
|
403
439
|
// note at the top of this file), which also puts it ahead of Astro's own request handler:
|
|
404
440
|
// Astro installs that in a Vite post hook, after every integration's middleware is in
|
|
405
|
-
// place, so
|
|
441
|
+
// place, so every path this answers reaches it and never reaches Astro's 404.
|
|
406
442
|
server.middlewares.use((req, res, next) => {
|
|
407
443
|
let url;
|
|
408
444
|
try {
|
|
@@ -411,7 +447,15 @@ export default function orbytesPin(options = {}) {
|
|
|
411
447
|
return next();
|
|
412
448
|
}
|
|
413
449
|
const path = url.pathname;
|
|
414
|
-
|
|
450
|
+
// The board's old address, kept as a redirect for bookmarks and muscle memory.
|
|
451
|
+
if (config.lab && route !== "/pin" && (path === "/pin" || path === "/pin/")) {
|
|
452
|
+
res.statusCode = 302;
|
|
453
|
+
res.setHeader("Location", route + url.search);
|
|
454
|
+
return res.end();
|
|
455
|
+
}
|
|
456
|
+
// Inside the lab the board itself is an Astro page (injected above), so the wall is not
|
|
457
|
+
// this middleware's to draw — only its screenshots and its one write endpoint are.
|
|
458
|
+
const wall = !config.lab && (path === route || path === `${route}/`);
|
|
415
459
|
const api = path === apiPath;
|
|
416
460
|
if (!wall && !api && !path.startsWith(assetPrefix)) return next();
|
|
417
461
|
|
|
@@ -513,9 +557,10 @@ export default function orbytesPin(options = {}) {
|
|
|
513
557
|
// Read from disk on every request — that is the whole point of the route, and why
|
|
514
558
|
// `orbytes-pin-gallery` is no longer part of looking at the board.
|
|
515
559
|
try {
|
|
516
|
-
// `includeCancelled`:
|
|
517
|
-
//
|
|
518
|
-
//
|
|
560
|
+
// `includeCancelled`: the human-facing surfaces — this wall, the lab's Tasks views and
|
|
561
|
+
// the gallery file — are where the archive is visible. Every agent-facing read drops
|
|
562
|
+
// cancelled tickets by default — a cancelled ticket must not take up context space for
|
|
563
|
+
// an agent — and an archive nobody can open is a delete.
|
|
519
564
|
const { tickets, broken } = collectTickets(repoRoot, { backlogDir: config.backlogDir, includeCancelled: true });
|
|
520
565
|
const html = renderBoard(tickets, broken, {
|
|
521
566
|
// The presence of an endpoint is what makes the page writable. The standalone
|
package/src/pin/tickets.mjs
CHANGED
|
@@ -120,8 +120,9 @@ export function isOpen(status) {
|
|
|
120
120
|
* Is this ticket archived — cancelled, and therefore invisible to every agent-facing read?
|
|
121
121
|
*
|
|
122
122
|
* Ruled 2026-09-22: items in here must not override anything or take up context space for an
|
|
123
|
-
* agent. `listTickets` drops these unless explicitly asked for them
|
|
124
|
-
*
|
|
123
|
+
* agent. `listTickets` drops these unless explicitly asked for them. The human-facing surfaces
|
|
124
|
+
* still show them — the lab's Tasks views, the pin half's standalone board and
|
|
125
|
+
* `orbytes-pin-gallery` — because an archive nobody can open is a delete.
|
|
125
126
|
*
|
|
126
127
|
* @param {unknown} status
|
|
127
128
|
*/
|
|
@@ -672,9 +673,9 @@ export function parseTicket(text, file = "<unknown file>") {
|
|
|
672
673
|
* that never heard of the rule still cannot be handed a dead ticket, and one that wants the
|
|
673
674
|
* archive has to say so in the call.
|
|
674
675
|
*
|
|
675
|
-
* `includeCancelled: true` returns them
|
|
676
|
-
*
|
|
677
|
-
*
|
|
676
|
+
* `includeCancelled: true` returns them. Nothing in this package passes it here: the surfaces that
|
|
677
|
+
* draw the archive and `updateTicket`, which has to be able to find a cancelled ticket in order to
|
|
678
|
+
* move it back out, pass it to `collectTickets` (./board.mjs) instead.
|
|
678
679
|
*
|
|
679
680
|
* Statuses come back RAW (› `parseTicket`), so the filter normalises before it compares: a ticket
|
|
680
681
|
* whose file still says `Done` is `Resolved`, and is live.
|
package/src/pin/toolbar.js
CHANGED
|
@@ -698,7 +698,7 @@ const BOARD_ROUTE = (pinConfig && pinConfig.route) || '/pin';
|
|
|
698
698
|
* The panel has exactly TWO tabs, which is fixed and not a preference, so the six map onto them:
|
|
699
699
|
* **Open is everything that is not `Resolved` and not `Cancelled`**, `Resolved` is its own tab, and
|
|
700
700
|
* `Cancelled` appears in neither and draws no marker — drawing an archived ticket on the page is
|
|
701
|
-
* precisely the "taking up space" the status exists to stop. It stays reachable on
|
|
701
|
+
* precisely the "taking up space" the status exists to stop. It stays reachable on the board.
|
|
702
702
|
*
|
|
703
703
|
* These names are for CLASSIFYING and LABELLING only. Every write sends the status string the
|
|
704
704
|
* board itself reported (› `setStatus`), never a string from this list, because `expect` is
|
|
@@ -860,6 +860,10 @@ export default defineToolbarApp({
|
|
|
860
860
|
}
|
|
861
861
|
|
|
862
862
|
let mode = 'idle'; // idle | picking | selected
|
|
863
|
+
/** Whether Astro has this app toggled on — the lab's Comment button shows it as pressed. */
|
|
864
|
+
let appActive = false;
|
|
865
|
+
/** Set by the lab's Comment: the element whose box confines the pick (› the lab channel). */
|
|
866
|
+
let scope = null;
|
|
863
867
|
let hoverEl = null;
|
|
864
868
|
let selectedEl = null;
|
|
865
869
|
/**
|
|
@@ -1633,6 +1637,7 @@ export default defineToolbarApp({
|
|
|
1633
1637
|
syncMarkers();
|
|
1634
1638
|
// The count on the toolbar button is read from the same list, so it moves with it.
|
|
1635
1639
|
updateTicketsButton();
|
|
1640
|
+
publish();
|
|
1636
1641
|
}
|
|
1637
1642
|
}
|
|
1638
1643
|
|
|
@@ -2280,6 +2285,7 @@ export default defineToolbarApp({
|
|
|
2280
2285
|
|
|
2281
2286
|
if (next === 'picking') {
|
|
2282
2287
|
if (!surface.isConnected) canvas.append(surface);
|
|
2288
|
+
fitSurface();
|
|
2283
2289
|
hideChrome(hoverHighlight);
|
|
2284
2290
|
// The picker owns clicks while it is armed — a marker must not swallow one that
|
|
2285
2291
|
// was meant to pin the element underneath it.
|
|
@@ -2300,6 +2306,18 @@ export default defineToolbarApp({
|
|
|
2300
2306
|
clearCrumbs();
|
|
2301
2307
|
hideChrome(selectedHighlight);
|
|
2302
2308
|
}
|
|
2309
|
+
publish();
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
/** The surface covers the page — or only the scope's box, when the lab's Comment set one. */
|
|
2313
|
+
function fitSurface() {
|
|
2314
|
+
const r = scope && scope.isConnected ? scope.getBoundingClientRect() : null;
|
|
2315
|
+
Object.assign(
|
|
2316
|
+
surface.style,
|
|
2317
|
+
r
|
|
2318
|
+
? { left: `${r.left}px`, top: `${r.top}px`, width: `${r.width}px`, height: `${r.height}px`, right: 'auto', bottom: 'auto' }
|
|
2319
|
+
: { left: '0', top: '0', right: '0', bottom: '0', width: '', height: '' },
|
|
2320
|
+
);
|
|
2303
2321
|
}
|
|
2304
2322
|
|
|
2305
2323
|
function hideChrome(el) {
|
|
@@ -2362,6 +2380,7 @@ export default defineToolbarApp({
|
|
|
2362
2380
|
}
|
|
2363
2381
|
|
|
2364
2382
|
function reposition() {
|
|
2383
|
+
if (mode === 'picking') fitSurface();
|
|
2365
2384
|
if (mode === 'picking' && hoverEl) {
|
|
2366
2385
|
place(hoverHighlight, hoverEl, hoverFrames);
|
|
2367
2386
|
const { source } = resolveSource(hoverEl);
|
|
@@ -2424,6 +2443,10 @@ export default defineToolbarApp({
|
|
|
2424
2443
|
* @returns {{ el: Element|null, frames: Element[], note: string|null }}
|
|
2425
2444
|
*/
|
|
2426
2445
|
function hitTest(x, y) {
|
|
2446
|
+
if (scope) {
|
|
2447
|
+
const r = scope.getBoundingClientRect();
|
|
2448
|
+
if (x < r.left || x > r.right || y < r.top || y > r.bottom) return { el: null, frames: [], note: null };
|
|
2449
|
+
}
|
|
2427
2450
|
let doc = document;
|
|
2428
2451
|
let px = x;
|
|
2429
2452
|
let py = y;
|
|
@@ -2439,7 +2462,8 @@ export default defineToolbarApp({
|
|
|
2439
2462
|
}
|
|
2440
2463
|
|
|
2441
2464
|
const frame = frameUnder(el, px, py);
|
|
2442
|
-
|
|
2465
|
+
// A scoped pick (the lab's Comment) pins what is IN the frame, never the lab around it.
|
|
2466
|
+
if (!frame) return scope && !frames.length ? { el: null, frames: [], note: null } : { el, frames: frames.slice(), note: null };
|
|
2443
2467
|
|
|
2444
2468
|
const inner = frameDocument(frame);
|
|
2445
2469
|
if (!inner) {
|
|
@@ -2881,6 +2905,19 @@ export default defineToolbarApp({
|
|
|
2881
2905
|
// box constantly — and a dictated paragraph cannot be retyped from memory. So Escape
|
|
2882
2906
|
// never discards text: with anything in the box it holds the selection, the words and
|
|
2883
2907
|
// the caret, and points at Cancel, which is the one control that discards on purpose.
|
|
2908
|
+
//
|
|
2909
|
+
// Inside the lab, Escape ends Comment mode rather than stopping at idle. Decided
|
|
2910
|
+
// 2026-09-24: the lab's Comment is one mode — pick, write, send — so wherever Escape would
|
|
2911
|
+
// leave this app on and idle ("Nothing selected", with Comment still pressed), it turns the
|
|
2912
|
+
// app off instead, exactly as pressing Comment again does. Only a pick the lab started has a
|
|
2913
|
+
// `scope`; the toolbar's own button keeps the steps above. The branch that holds a comment
|
|
2914
|
+
// still runs first, and turning the app off never empties the box, so a word typed here
|
|
2915
|
+
// survives every Escape.
|
|
2916
|
+
function leaveOrIdle() {
|
|
2917
|
+
if (scope) app.toggleState({ state: false });
|
|
2918
|
+
else setMode('idle');
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2884
2921
|
function onKeydown(event) {
|
|
2885
2922
|
// The climb. Capture phase and preventDefault, because ⌥↑ in a textarea is
|
|
2886
2923
|
// "move the caret to the start of the paragraph" on macOS and would otherwise
|
|
@@ -2904,7 +2941,8 @@ export default defineToolbarApp({
|
|
|
2904
2941
|
swallowEscapeKeyup = true;
|
|
2905
2942
|
// The badge and the hint line both change on setMode — saying it again in words
|
|
2906
2943
|
// was the log talking to itself.
|
|
2907
|
-
|
|
2944
|
+
if (selectedEl) setMode('selected');
|
|
2945
|
+
else leaveOrIdle();
|
|
2908
2946
|
} else if (mode === 'selected') {
|
|
2909
2947
|
event.preventDefault();
|
|
2910
2948
|
event.stopPropagation();
|
|
@@ -2915,7 +2953,7 @@ export default defineToolbarApp({
|
|
|
2915
2953
|
return;
|
|
2916
2954
|
}
|
|
2917
2955
|
clearSelection();
|
|
2918
|
-
|
|
2956
|
+
leaveOrIdle();
|
|
2919
2957
|
} else if (panelOpen) {
|
|
2920
2958
|
// Idle, with the panel open. Escape peels one layer at a time — the expanded entry,
|
|
2921
2959
|
// then the panel — and only once both are shut does it reach Astro's own
|
|
@@ -2936,6 +2974,13 @@ export default defineToolbarApp({
|
|
|
2936
2974
|
} else {
|
|
2937
2975
|
closePanel();
|
|
2938
2976
|
}
|
|
2977
|
+
} else if (scope && appActive) {
|
|
2978
|
+
// Idle in the lab, with nothing left to peel: close the app here rather than hope
|
|
2979
|
+
// Astro's own Escape handler is armed, which it is only after a real hover.
|
|
2980
|
+
event.preventDefault();
|
|
2981
|
+
event.stopPropagation();
|
|
2982
|
+
swallowEscapeKeyup = true;
|
|
2983
|
+
leaveOrIdle();
|
|
2939
2984
|
}
|
|
2940
2985
|
}
|
|
2941
2986
|
|
|
@@ -2951,6 +2996,8 @@ export default defineToolbarApp({
|
|
|
2951
2996
|
/* ---------- lifecycle ---------- */
|
|
2952
2997
|
|
|
2953
2998
|
app.onToggled(({ state }) => {
|
|
2999
|
+
appActive = Boolean(state);
|
|
3000
|
+
if (!state) scope = null;
|
|
2954
3001
|
if (state) {
|
|
2955
3002
|
setMode(selectedEl ? 'selected' : 'picking');
|
|
2956
3003
|
// Astro hides an app's whole canvas when the app is toggled off, so the markers go
|
|
@@ -3129,6 +3176,37 @@ export default defineToolbarApp({
|
|
|
3129
3176
|
}
|
|
3130
3177
|
});
|
|
3131
3178
|
|
|
3179
|
+
/* ---------- the lab's channel ---------- */
|
|
3180
|
+
|
|
3181
|
+
/**
|
|
3182
|
+
* The component lab drives this app from its own navbar — the Comment button, and the pins
|
|
3183
|
+
* it draws over its framed preview — and imports nothing from this file, so the door in and
|
|
3184
|
+
* the window out are plain window events:
|
|
3185
|
+
*
|
|
3186
|
+
* in `orbytes-pin:command` { action: 'pick', within?: Element } | { action: 'stop' }
|
|
3187
|
+
* out `orbytes-pin:state` { active, mode, tickets } after every mode change, toggle and
|
|
3188
|
+
* board read; the same snapshot sits on `window.__orbytesPin` for a late listener.
|
|
3189
|
+
*
|
|
3190
|
+
* `within` confines the pick to that element's box, and to what lies inside a frame there,
|
|
3191
|
+
* so Comment pins the previewed page and never the lab's chrome around it. The toolbar's own
|
|
3192
|
+
* button still opens the app unscoped, exactly as before.
|
|
3193
|
+
*/
|
|
3194
|
+
function publish() {
|
|
3195
|
+
const detail = { active: appActive, mode, tickets: panelTickets };
|
|
3196
|
+
window.__orbytesPin = detail;
|
|
3197
|
+
window.dispatchEvent(new CustomEvent('orbytes-pin:state', { detail }));
|
|
3198
|
+
}
|
|
3199
|
+
window.addEventListener('orbytes-pin:command', (event) => {
|
|
3200
|
+
const { action, within } = (event && event.detail) || {};
|
|
3201
|
+
if (action === 'pick') {
|
|
3202
|
+
scope = within instanceof Element ? within : null;
|
|
3203
|
+
if (appActive) setMode(selectedEl ? 'selected' : 'picking');
|
|
3204
|
+
else app.toggleState({ state: true });
|
|
3205
|
+
} else if (action === 'stop') {
|
|
3206
|
+
app.toggleState({ state: false });
|
|
3207
|
+
}
|
|
3208
|
+
});
|
|
3209
|
+
|
|
3132
3210
|
setMode('idle');
|
|
3133
3211
|
restoreAfterReload();
|
|
3134
3212
|
// Read the board once at start-up so the markers are on the page the moment the app is
|