@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbytes/astrolab",
|
|
3
|
-
"version": "0.4.0-next.
|
|
3
|
+
"version": "0.4.0-next.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The orbytes lab for Astro sites — every page, section and component rendered live, which of them the site actually mounts, a viewport canvas to size them on, and a dev-only click-to-ticket pin board — in one integration, all local.",
|
|
6
6
|
"keywords": [
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"preview": "npm --workspace testbed-orbytes-v2 run preview",
|
|
29
29
|
"build:staging": "npm --workspace testbed-orbytes-v2 run build:staging",
|
|
30
30
|
"check:isolation": "npm --workspace testbed-orbytes-v2 run check:isolation",
|
|
31
|
+
"check:lab-index": "npm --workspace testbed-orbytes-v2 run check:lab-index",
|
|
31
32
|
"verify": "npm --workspace testbed-orbytes-v2 run verify"
|
|
32
33
|
},
|
|
33
34
|
"bin": {
|
|
34
|
-
"orbytes-lab-cull": "bin/lab-cull.mjs",
|
|
35
35
|
"orbytes-pin-gallery": "bin/pin-gallery.mjs"
|
|
36
36
|
},
|
|
37
37
|
"main": "./index.mjs",
|
package/src/Home.astro
CHANGED
|
@@ -79,15 +79,14 @@ const previewAlias = (branch: string) =>
|
|
|
79
79
|
const unusedAssets = assets.filter((a) => a.usedBy.length === 0).length;
|
|
80
80
|
---
|
|
81
81
|
|
|
82
|
-
<Shell title="Home" active="home">
|
|
82
|
+
<Shell title="Home" active="home" navbarClass="lab-navbar--listing">
|
|
83
83
|
<Fragment slot="navbar">
|
|
84
|
-
<div class="lab-
|
|
85
|
-
<h1 class="lab-
|
|
86
|
-
|
|
87
|
-
<div class="lab-navbar__tools">
|
|
84
|
+
<div class="lab-listing-bar">
|
|
85
|
+
<h1 class="lab-listing-bar__title">Home</h1>
|
|
86
|
+
<p class="lab-listing-bar__counts">{labConfig.title}</p>
|
|
88
87
|
{
|
|
89
88
|
STAGING_URL && (
|
|
90
|
-
<a class="lab-btn lab-btn--sm" href={STAGING_URL} target="_blank" rel="noopener">
|
|
89
|
+
<a class="lab-btn lab-btn--sm lab-listing-bar__end" href={STAGING_URL} target="_blank" rel="noopener">
|
|
91
90
|
<Icon name="external" />
|
|
92
91
|
Staging
|
|
93
92
|
</a>
|
|
@@ -126,7 +125,7 @@ const unusedAssets = assets.filter((a) => a.usedBy.length === 0).length;
|
|
|
126
125
|
{tier.live > 0 && <span class="lab-pill lab-pill--live">{tier.live} live</span>}
|
|
127
126
|
{tier.used > 0 && <span class="lab-pill lab-pill--used">{tier.used} used</span>}
|
|
128
127
|
{tier.unused > 0 && <span class="lab-pill">{tier.unused} unused</span>}
|
|
129
|
-
<span class="lab-pill">{tier.stories} stories</span>
|
|
128
|
+
<span class="lab-pill">{tier.stories} variant{tier.stories === 1 ? "" : "s"}</span>
|
|
130
129
|
</span>
|
|
131
130
|
</a>
|
|
132
131
|
))
|
|
@@ -168,7 +167,7 @@ const unusedAssets = assets.filter((a) => a.usedBy.length === 0).length;
|
|
|
168
167
|
{component.version && <span class="lab-card__version">{component.version}</span>}
|
|
169
168
|
</a>
|
|
170
169
|
) : (
|
|
171
|
-
<span class="lab-slots__name lab-slots__name--bare" title="Not in the lab — no stories file for it">
|
|
170
|
+
<span class="lab-slots__name lab-slots__name--bare" title="Not in the lab — no *.stories.ts file for it">
|
|
172
171
|
{file.split("/").pop()}
|
|
173
172
|
</span>
|
|
174
173
|
)}
|
package/src/LabHead.astro
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// consumer configured a key.
|
|
6
6
|
//
|
|
7
7
|
// REWRITTEN 2026-09-24 for the redesign. Until then this file also decorated Astrobook's sidebar
|
|
8
|
-
// after the fact — pills,
|
|
8
|
+
// after the fact — pills, mark boxes, search widening, header buttons — with 800
|
|
9
9
|
// lines of script patching a DOM it did not render, plus a copy of the whole lab index as JSON on
|
|
10
10
|
// every page. The new chrome renders all of that on the server, so none of it is needed here.
|
|
11
11
|
//
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The navbar's ⋯ button and its menu — Figma `Astrolab` › Menu › Type=Actions. Everything that
|
|
3
|
+
// leaves the lab (VS Code, the bare variant full screen, staging, a link at this exact size), then
|
|
4
|
+
// two sections that are switches rather than places:
|
|
5
|
+
//
|
|
6
|
+
// Pins Show pins (on) and Show resolved (off) — they govern the markers drawn on the framed
|
|
7
|
+
// preview. They moved here from the navbar (decided 2026-09-24); the navbar keeps only
|
|
8
|
+
// Comment. Only while the pin board runs, which is `astro dev` with the board enabled.
|
|
9
|
+
// Preview Dark mode — the PREVIEW's own light or dark, separate from the chrome's. Hidden until
|
|
10
|
+
// the framed site turns out to have a dark mode (./viewport-client.ts looks for one).
|
|
11
|
+
//
|
|
12
|
+
// Checkable rows are `menuitemcheckbox` buttons, so the menu stays one list a keyboard can walk.
|
|
13
|
+
// Behaviour: ./navbar-client.ts (pins), ./viewport-client.ts (copy link, dark mode).
|
|
14
|
+
import Icon from "./Icon.astro";
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
editorUrl?: string | null;
|
|
18
|
+
/** The bare variant, alone in a tab. */
|
|
19
|
+
fullScreenUrl?: string | null;
|
|
20
|
+
stagingUrl?: string | null;
|
|
21
|
+
/** Show the Pins section — only where the pin board runs. */
|
|
22
|
+
pins: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { editorUrl, fullScreenUrl, stagingUrl, pins } = Astro.props;
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<button
|
|
29
|
+
class="lab-tool lab-tool--icon lab-nb__more"
|
|
30
|
+
type="button"
|
|
31
|
+
popovertarget="lab-actions-menu"
|
|
32
|
+
title="More actions"
|
|
33
|
+
>
|
|
34
|
+
<Icon name="more" size="sm" />
|
|
35
|
+
<span class="lab-visually-hidden">More actions</span>
|
|
36
|
+
</button>
|
|
37
|
+
<div class="lab-menu lab-menu--actions" id="lab-actions-menu" popover role="menu" data-lab-align="end">
|
|
38
|
+
{
|
|
39
|
+
editorUrl && (
|
|
40
|
+
<a class="lab-menu__item" role="menuitem" href={editorUrl}>
|
|
41
|
+
<span class="lab-menu__mark"><Icon name="code" size="sm" /></span>
|
|
42
|
+
<span class="lab-menu__text">Open in VS Code</span>
|
|
43
|
+
</a>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
{
|
|
47
|
+
fullScreenUrl && (
|
|
48
|
+
<a class="lab-menu__item" role="menuitem" href={fullScreenUrl} target="_blank" rel="noopener">
|
|
49
|
+
<span class="lab-menu__mark"><Icon name="expand" size="sm" /></span>
|
|
50
|
+
<span class="lab-menu__text">Open full screen</span>
|
|
51
|
+
<span class="lab-menu__hint">the bare variant</span>
|
|
52
|
+
</a>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
{
|
|
56
|
+
stagingUrl && (
|
|
57
|
+
<a class="lab-menu__item" role="menuitem" href={stagingUrl} target="_blank" rel="noopener">
|
|
58
|
+
<span class="lab-menu__mark"><Icon name="staging" size="sm" /></span>
|
|
59
|
+
<span class="lab-menu__text">View on staging</span>
|
|
60
|
+
</a>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
{(editorUrl || fullScreenUrl || stagingUrl) && <hr />}
|
|
64
|
+
<button class="lab-menu__item" type="button" role="menuitem" data-lab-copy-link>
|
|
65
|
+
<span class="lab-menu__mark"><Icon name="link" size="sm" /></span>
|
|
66
|
+
<span class="lab-menu__text" data-lab-copy-label>Copy link to this exact size</span>
|
|
67
|
+
<span class="lab-menu__hint" data-lab-copy-size></span>
|
|
68
|
+
</button>
|
|
69
|
+
{
|
|
70
|
+
pins && (
|
|
71
|
+
<>
|
|
72
|
+
<hr />
|
|
73
|
+
<p class="lab-menu__label" id="lab-actions-pins">Pins</p>
|
|
74
|
+
<div role="group" aria-labelledby="lab-actions-pins">
|
|
75
|
+
<button class="lab-menu__item" type="button" role="menuitemcheckbox" aria-checked="true" data-lab-pins-toggle="show">
|
|
76
|
+
<span class="lab-menu__mark"><span class="lab-menu__box" aria-hidden="true" /></span>
|
|
77
|
+
<span class="lab-menu__text">Show pins</span>
|
|
78
|
+
</button>
|
|
79
|
+
<button class="lab-menu__item" type="button" role="menuitemcheckbox" aria-checked="false" data-lab-pins-toggle="resolved">
|
|
80
|
+
<span class="lab-menu__mark"><span class="lab-menu__box" aria-hidden="true" /></span>
|
|
81
|
+
<span class="lab-menu__text">Show resolved</span>
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
<div data-lab-preview-group hidden>
|
|
88
|
+
<hr />
|
|
89
|
+
<p class="lab-menu__label" id="lab-actions-preview">Preview</p>
|
|
90
|
+
<div role="group" aria-labelledby="lab-actions-preview">
|
|
91
|
+
<button class="lab-menu__item" type="button" role="menuitemcheckbox" aria-checked="false" data-lab-preview-theme>
|
|
92
|
+
<span class="lab-menu__mark"><span class="lab-menu__box" aria-hidden="true" /></span>
|
|
93
|
+
<span class="lab-menu__text">Dark mode</span>
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// <iframe> sized to a virtual viewport and scaled to the card by ../chrome/shell-client.ts: page
|
|
5
5
|
// width for section-like tiers, a component stage otherwise. The whole card opens the component;
|
|
6
6
|
// the icon links under it sit above that link.
|
|
7
|
+
//
|
|
8
|
+
// A live component carries the Sections tree's slot badge, not a "live · slot N of M" pill: one sign
|
|
9
|
+
// for "live in slot N" across the lab (decided 2026-09-24, ASTROL-26). The title keeps the long form.
|
|
7
10
|
import Icon from "./Icon.astro";
|
|
8
11
|
import { componentState, type LabComponent } from "./trees";
|
|
9
12
|
import { isSectionLike, livePillText, liveTitle } from "../shell/lab-index";
|
|
@@ -30,10 +33,14 @@ const state = componentState(mod);
|
|
|
30
33
|
{mod.version && <span class="lab-card__version">{mod.version}</span>}
|
|
31
34
|
</span>
|
|
32
35
|
<span class="lab-card__sub">
|
|
33
|
-
{mod.stories.length}
|
|
36
|
+
{mod.stories.length} variant{mod.stories.length === 1 ? "" : "s"}{mod.summary ? ` · ${mod.summary}` : ""}
|
|
34
37
|
</span>
|
|
35
38
|
<span class="lab-card__pills">
|
|
36
|
-
{liveStory &&
|
|
39
|
+
{liveStory?.live && (
|
|
40
|
+
<span class="lab-slot" data-state="slotted" title={liveTitle(liveStory) ?? livePillText(liveStory) ?? undefined}>
|
|
41
|
+
<span class="lab-visually-hidden">Live in slot </span>{liveStory.live.slot}
|
|
42
|
+
</span>
|
|
43
|
+
)}
|
|
37
44
|
{!mod.live && mod.usedBy.length > 0 && (
|
|
38
45
|
<span class="lab-pill lab-pill--used" title={mod.usedBy.join("\n")}>used by {mod.usedBy.length}</span>
|
|
39
46
|
)}
|
package/src/chrome/Nav.astro
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
// Level 1 — the main nav (Figma `Sidebar navigation`, the white card). Home, the Site group (Pages,
|
|
3
3
|
// one entry per tier, Assets), the Tasks group when the pin board is running, and Support and
|
|
4
|
-
// Settings at the foot. Collapses to a
|
|
5
|
-
//
|
|
4
|
+
// Settings at the foot. Collapses to a 64px icon rail (⌘⇧B); in the rail a group's items open as
|
|
5
|
+
// a flyout on hover or focus, under the group's name, drawn by CSS alone (../ui/lab.css › level 1
|
|
6
|
+
// as a rail).
|
|
7
|
+
//
|
|
8
|
+
// The header carries the collapse button and the LAB's own light/dark toggle — a moon, beside the
|
|
9
|
+
// logo, under the logomark in the rail (decided 2026-09-24). It switches the chrome only; the
|
|
10
|
+
// preview's light/dark is a separate setting of its own.
|
|
6
11
|
//
|
|
7
12
|
// The logo and the Support / Settings pages are placeholders until they are designed.
|
|
8
13
|
import Icon from "./Icon.astro";
|
|
@@ -25,15 +30,35 @@ const isOpen = (group: { items: { key: NavKey }[] }) => group.items.some((item)
|
|
|
25
30
|
<span class="lab-brand__mark" aria-hidden="true"></span>
|
|
26
31
|
<span class="lab-brand__name">Astrolab</span>
|
|
27
32
|
</a>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
{/* The collapse button's name and tooltip follow the state: "Collapse menu" when level 1
|
|
34
|
+
is open, "Expand menu" in the rail (./shell-client.ts keeps aria-label in step). */}
|
|
35
|
+
<button
|
|
36
|
+
class="lab-icon-btn lab-l1__btn lab-tipped"
|
|
37
|
+
type="button"
|
|
38
|
+
data-lab-toggle="l1"
|
|
39
|
+
data-lab-l1-btn
|
|
40
|
+
aria-label="Collapse menu"
|
|
41
|
+
aria-describedby="lab-tip-l1"
|
|
42
|
+
>
|
|
43
|
+
<Icon name="chevrons-left" size="sm" class="lab-l1-btn__collapse" />
|
|
44
|
+
<Icon name="chevrons-right" size="sm" class="lab-l1-btn__expand" />
|
|
45
|
+
<span class="lab-tip lab-tip--l1" id="lab-tip-l1" role="tooltip">
|
|
46
|
+
<span class="lab-tip__title">
|
|
47
|
+
<span class="lab-l1-btn__collapse">Collapse menu</span><span class="lab-l1-btn__expand">Expand menu</span> · ⌘⇧B
|
|
48
|
+
</span>
|
|
49
|
+
<span class="lab-tip__text">⌘B shows or hides the panel</span>
|
|
50
|
+
<span class="lab-tip__text">⌘. hides both (focus mode)</span>
|
|
51
|
+
</span>
|
|
32
52
|
</button>
|
|
33
|
-
<button
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
<button
|
|
54
|
+
class="lab-icon-btn lab-l1__btn lab-theme-btn"
|
|
55
|
+
type="button"
|
|
56
|
+
data-lab-theme-toggle
|
|
57
|
+
aria-pressed="false"
|
|
58
|
+
aria-label="Dark mode for the lab"
|
|
59
|
+
title="Dark mode for the lab"
|
|
60
|
+
>
|
|
61
|
+
<Icon name="moon" size="sm" />
|
|
37
62
|
</button>
|
|
38
63
|
</div>
|
|
39
64
|
|
|
@@ -66,6 +91,7 @@ const isOpen = (group: { items: { key: NavKey }[] }) => group.items.some((item)
|
|
|
66
91
|
<Icon name="chevron-down" size="sm" class="lab-nav-group__chevron" />
|
|
67
92
|
</button>
|
|
68
93
|
<div class="lab-nav-group__items">
|
|
94
|
+
<p class="lab-nav-group__flyout-label" aria-hidden="true">{group.label}</p>
|
|
69
95
|
{group.items.map((item) => (
|
|
70
96
|
<a
|
|
71
97
|
class="lab-nav-item"
|
package/src/chrome/Panel.astro
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
// Level 2 — the panel (Figma `Sidebar navigation`, the grey column): breadcrumb, search with its
|
|
3
3
|
// filter menu, the tree in the default slot, and the coffee footer. Closes with ⌘B or its own
|
|
4
|
-
// button; while closed it peeks out over the stage
|
|
4
|
+
// button; while closed it peeks out over the stage — below the navbar, never over it — when the
|
|
5
|
+
// pointer reaches the stage's left edge.
|
|
6
|
+
//
|
|
7
|
+
// Only the Site views have one. Kanban Board and All Tasks have no level 2 (decided 2026-09-24).
|
|
5
8
|
//
|
|
6
9
|
// The coffee link is a placeholder until it has a destination.
|
|
7
10
|
import Icon from "./Icon.astro";
|
|
@@ -37,9 +40,19 @@ const { crumbs, label, search = true, searchPlaceholder = "Search", filters, fil
|
|
|
37
40
|
}
|
|
38
41
|
</ol>
|
|
39
42
|
</nav>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
{/* One button, two actions: docked, it hides the panel; while the panel only peeks, it keeps
|
|
44
|
+
it open. Its name follows the action — ./shell-client.ts swaps the label when a peek
|
|
45
|
+
starts and ends (decided 2026-09-24). */}
|
|
46
|
+
<button
|
|
47
|
+
class="lab-icon-btn lab-l2__close"
|
|
48
|
+
type="button"
|
|
49
|
+
data-lab-toggle="l2"
|
|
50
|
+
data-lab-l2-btn
|
|
51
|
+
aria-label="Hide panel"
|
|
52
|
+
title="Hide panel · ⌘B"
|
|
53
|
+
>
|
|
54
|
+
<Icon name="panel-close" class="lab-l2__close-hide" />
|
|
55
|
+
<Icon name="panel-open" class="lab-l2__close-dock" />
|
|
43
56
|
</button>
|
|
44
57
|
</div>
|
|
45
58
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The eye beside a component's (or page's) name, and the Properties panel it opens. It replaces the
|
|
3
|
+
// navbar's status pills and their status menu (decided 2026-09-24: no pills in the identity — a
|
|
4
|
+
// panel that opens when you hover a small eye beside the name). Figma `Astrolab` › Menu ›
|
|
5
|
+
// Type=Properties, and the four cases on the `Lab / Properties` sheet.
|
|
6
|
+
//
|
|
7
|
+
// In order: On the site (live and its slot, used by, or not on any page), Open pins (with a row to
|
|
8
|
+
// the task list), Responsive (the two marks, sections only), Source file. Mark for deletion is not
|
|
9
|
+
// here: it was removed from the lab as a feature (decided 2026-09-24).
|
|
10
|
+
//
|
|
11
|
+
// Not a menu. The panel is a `popover="manual"` dialog: hovering the eye opens it after a short
|
|
12
|
+
// delay and it stays open while the pointer is on the eye or the panel; clicking the eye (or
|
|
13
|
+
// Enter / Space) pins it open until Escape or a click outside. Behaviour: ./navbar-client.ts. The
|
|
14
|
+
// marks are live checkboxes whenever the panel is open (./marks-client.ts).
|
|
15
|
+
import Icon from "./Icon.astro";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** One line per fact under "On the site"; `live` draws the green dot, otherwise it is grey. */
|
|
19
|
+
site: { text: string; live: boolean }[];
|
|
20
|
+
/** Open and resolved pins, and where the row leads. Null when the pin board is not running. */
|
|
21
|
+
pins: { open: number; resolved: number; href: string } | null;
|
|
22
|
+
/** The responsive marks for this file, or null when the lab keeps none for it. */
|
|
23
|
+
responsive: { file: string; approved: boolean; done: boolean } | null;
|
|
24
|
+
source: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { site, pins, responsive, source } = Astro.props;
|
|
28
|
+
|
|
29
|
+
const pinLine = (open: number, resolved: number) =>
|
|
30
|
+
open === 0 && resolved === 0 ? "No pins" : `${open === 0 ? "No open pins" : `${open} open`} · ${resolved} resolved`;
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<button
|
|
34
|
+
class="lab-tool lab-tool--icon"
|
|
35
|
+
type="button"
|
|
36
|
+
aria-expanded="false"
|
|
37
|
+
aria-controls="lab-props"
|
|
38
|
+
title="Properties — hover to look, click to pin open"
|
|
39
|
+
data-lab-props-trigger
|
|
40
|
+
>
|
|
41
|
+
<Icon name="properties" size="sm" />
|
|
42
|
+
<span class="lab-visually-hidden">Properties</span>
|
|
43
|
+
</button>
|
|
44
|
+
<div class="lab-menu lab-props" id="lab-props" popover="manual" role="dialog" aria-label="Properties" data-lab-props>
|
|
45
|
+
<p class="lab-menu__label">On the site</p>
|
|
46
|
+
<div class="lab-props__facts">
|
|
47
|
+
{
|
|
48
|
+
site.map((line) => (
|
|
49
|
+
<p class="lab-props__fact">
|
|
50
|
+
<span class:list={["lab-props__dot", line.live && "lab-props__dot--live"]} aria-hidden="true" />
|
|
51
|
+
{line.text}
|
|
52
|
+
</p>
|
|
53
|
+
))
|
|
54
|
+
}
|
|
55
|
+
</div>
|
|
56
|
+
{
|
|
57
|
+
pins && (
|
|
58
|
+
<>
|
|
59
|
+
<hr />
|
|
60
|
+
<p class="lab-menu__label">Open pins</p>
|
|
61
|
+
<a class="lab-menu__item" href={pins.href}>
|
|
62
|
+
<span class="lab-menu__mark lab-props__pin">
|
|
63
|
+
<Icon name="marker" size="sm" />
|
|
64
|
+
</span>
|
|
65
|
+
<span class="lab-menu__text">{pinLine(pins.open, pins.resolved)}</span>
|
|
66
|
+
<Icon name="chevron-right" size="sm" class="lab-menu__trail" />
|
|
67
|
+
</a>
|
|
68
|
+
</>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
{
|
|
72
|
+
responsive && (
|
|
73
|
+
<>
|
|
74
|
+
<hr />
|
|
75
|
+
<p class="lab-menu__label">Responsive</p>
|
|
76
|
+
<label class="lab-menu__item">
|
|
77
|
+
<span class="lab-menu__mark">
|
|
78
|
+
<input class="lab-checkbox" type="checkbox" data-lab-mark="approved" data-file={responsive.file} checked={responsive.approved} disabled />
|
|
79
|
+
</span>
|
|
80
|
+
<span class="lab-menu__text">Approved for responsive work</span>
|
|
81
|
+
</label>
|
|
82
|
+
<label class="lab-menu__item">
|
|
83
|
+
<span class="lab-menu__mark">
|
|
84
|
+
<input class="lab-checkbox" type="checkbox" data-lab-mark="done" data-file={responsive.file} checked={responsive.done} disabled />
|
|
85
|
+
</span>
|
|
86
|
+
<span class="lab-menu__text">Made responsive</span>
|
|
87
|
+
</label>
|
|
88
|
+
</>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
{
|
|
92
|
+
source && (
|
|
93
|
+
<>
|
|
94
|
+
<hr />
|
|
95
|
+
<p class="lab-menu__label">Source file</p>
|
|
96
|
+
<p class="lab-props__source">{source}</p>
|
|
97
|
+
</>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
<hr />
|
|
101
|
+
<p class="lab-props__note" data-lab-marks-note>
|
|
102
|
+
{responsive ? "Click the eye to pin · marks save to the repo" : "Click the eye to pin"}
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Level 2 for the sections tier (Figma `Astrolab` › `Sections tree`, decided 2026-09-24): a
|
|
3
|
+
// "Sort by" dropdown, then one row per section with its slot badge — the sections a page mounts
|
|
4
|
+
// first, the rest under "Unslotted · on no page". The rows and their order come from
|
|
5
|
+
// ./trees.ts › sectionsTree; ./Tree.astro hands a tree to this component when its root rows carry a
|
|
6
|
+
// slot.
|
|
7
|
+
//
|
|
8
|
+
// It is the same tree to the shell script as any other: one [data-lab-tree] list of
|
|
9
|
+
// .lab-tree__node rows, so search, the filter menu, j / k and the scroll memory all work unchanged.
|
|
10
|
+
// What it adds is the sort. The rows are rendered in slot order; each carries its rank in both
|
|
11
|
+
// orders as CSS custom properties, and the viewer's choice (localStorage `lab-tree-sort`) is put on
|
|
12
|
+
// <html> before paint by the inline script in ./Shell.astro, so a remembered "Name" sort is drawn
|
|
13
|
+
// by CSS `order` from the first frame. ./shell-client.ts then moves the rows themselves into that
|
|
14
|
+
// order, so reading order, keyboard order and j / k agree with what is on screen.
|
|
15
|
+
import Icon from "./Icon.astro";
|
|
16
|
+
import { labConfig, sectionsTier, type TreeNode } from "./model";
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
nodes: TreeNode[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { nodes } = Astro.props;
|
|
23
|
+
|
|
24
|
+
const slotted = nodes.filter((n) => n.group !== "unslotted");
|
|
25
|
+
const unslotted = nodes.filter((n) => n.group === "unslotted");
|
|
26
|
+
|
|
27
|
+
const holdsCurrent = (node: TreeNode): boolean => Boolean(node.current) || (node.children ?? []).some(holdsCurrent);
|
|
28
|
+
const leaves = (list: TreeNode[]): number => list.reduce((n, node) => n + (node.children ? leaves(node.children) : 1), 0);
|
|
29
|
+
const count = leaves(nodes);
|
|
30
|
+
const tierName = (labConfig.tiers.find((t) => t.id === sectionsTier)?.label ?? "Sections").toLowerCase();
|
|
31
|
+
const noun = count === 1 && tierName.endsWith("s") ? tierName.slice(0, -1) : tierName;
|
|
32
|
+
|
|
33
|
+
const searchText = (node: TreeNode) => `${node.label} ${node.search ?? ""}`.toLowerCase();
|
|
34
|
+
const orderStyle = (node: TreeNode) =>
|
|
35
|
+
node.order ? `--lab-o-slot:${node.order.slot};--lab-o-name:${node.order.name}` : undefined;
|
|
36
|
+
const badge = (node: TreeNode) =>
|
|
37
|
+
node.slot
|
|
38
|
+
? {
|
|
39
|
+
text: String(node.slot.n),
|
|
40
|
+
state: node.current ? "current" : "slotted",
|
|
41
|
+
title: `Slot ${node.slot.n} of ${node.slot.of} on ${node.slot.page}`,
|
|
42
|
+
aria: `${node.label}, slot ${node.slot.n}`,
|
|
43
|
+
}
|
|
44
|
+
: {
|
|
45
|
+
text: "–",
|
|
46
|
+
state: node.current ? "current" : "unslotted",
|
|
47
|
+
title: "On no page",
|
|
48
|
+
aria: `${node.label}, on no page`,
|
|
49
|
+
};
|
|
50
|
+
const heading = slotted.length;
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
<div class="lab-stree-head">
|
|
54
|
+
<span class="lab-stree-head__count">{count} {noun}</span>
|
|
55
|
+
<button class="lab-stree-head__sort" type="button" popovertarget="lab-tree-sort" aria-haspopup="menu" data-lab-sort-btn>
|
|
56
|
+
Sort by
|
|
57
|
+
<b><span class="lab-stree-head__by-slot">Slot</span><span class="lab-stree-head__by-name">Name</span></b>
|
|
58
|
+
<Icon name="chevron-down" size="sm" />
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="lab-menu lab-menu--sort" id="lab-tree-sort" popover role="menu" data-lab-align="end">
|
|
62
|
+
<button class="lab-menu__item" type="button" role="menuitemradio" aria-checked="true" data-lab-sort-by="slot">
|
|
63
|
+
<Icon name="check" size="sm" class="lab-menu__tick" />
|
|
64
|
+
Slot number
|
|
65
|
+
</button>
|
|
66
|
+
<button class="lab-menu__item" type="button" role="menuitemradio" aria-checked="false" data-lab-sort-by="name">
|
|
67
|
+
<Icon name="check" size="sm" class="lab-menu__tick" />
|
|
68
|
+
Name (A–Z)
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<ul class="lab-tree lab-stree" role="tree" aria-label={`${count} ${noun}`} data-lab-tree data-lab-stree>
|
|
73
|
+
{
|
|
74
|
+
[...slotted, ...unslotted].map((node, i) => {
|
|
75
|
+
const b = badge(node);
|
|
76
|
+
const open = Boolean(node.children?.length) && (!node.childrenWhenCurrent || holdsCurrent(node));
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
{i === heading && unslotted.length > 0 && (
|
|
80
|
+
<li class="lab-stree__heading" role="none" style={`--lab-o-slot:${heading};--lab-o-name:${heading}`} data-lab-tree-heading>
|
|
81
|
+
Unslotted · on no page
|
|
82
|
+
</li>
|
|
83
|
+
)}
|
|
84
|
+
<li
|
|
85
|
+
class="lab-tree__node"
|
|
86
|
+
role="none"
|
|
87
|
+
style={orderStyle(node)}
|
|
88
|
+
data-lab-slot-group={node.group}
|
|
89
|
+
data-lab-search={searchText(node)}
|
|
90
|
+
data-lab-facets={node.facets}
|
|
91
|
+
>
|
|
92
|
+
<a
|
|
93
|
+
class="lab-tree__row"
|
|
94
|
+
role="treeitem"
|
|
95
|
+
href={node.href}
|
|
96
|
+
aria-current={node.current ? "page" : undefined}
|
|
97
|
+
aria-label={b.aria}
|
|
98
|
+
aria-expanded={node.children?.length ? String(open) : undefined}
|
|
99
|
+
title={node.title}
|
|
100
|
+
data-lab-leaf
|
|
101
|
+
>
|
|
102
|
+
<span class="lab-slot" data-state={b.state} title={b.title}>{b.text}</span>
|
|
103
|
+
<span class="lab-tree__label">{node.label}</span>
|
|
104
|
+
</a>
|
|
105
|
+
{open && (
|
|
106
|
+
<ul class="lab-tree__group" role="group">
|
|
107
|
+
{node.children!.map((child) => (
|
|
108
|
+
<li class="lab-tree__node" role="none" style="--lab-d:1" data-lab-search={searchText(child)} data-lab-facets={child.facets}>
|
|
109
|
+
<a
|
|
110
|
+
class="lab-tree__row"
|
|
111
|
+
role="treeitem"
|
|
112
|
+
href={child.href}
|
|
113
|
+
aria-current={child.current ? "page" : undefined}
|
|
114
|
+
title={child.title}
|
|
115
|
+
data-lab-leaf
|
|
116
|
+
>
|
|
117
|
+
<span class="lab-tree__label">{child.label}</span>
|
|
118
|
+
</a>
|
|
119
|
+
</li>
|
|
120
|
+
))}
|
|
121
|
+
</ul>
|
|
122
|
+
)}
|
|
123
|
+
</li>
|
|
124
|
+
</>
|
|
125
|
+
);
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
</ul>
|
package/src/chrome/Shell.astro
CHANGED
|
@@ -35,9 +35,11 @@ interface Props {
|
|
|
35
35
|
};
|
|
36
36
|
/** Extra class on main's content box, e.g. to make it a positioning context for the stage. */
|
|
37
37
|
contentClass?: string;
|
|
38
|
+
/** Extra class on the navbar, e.g. `lab-navbar--listing` for a listing's title bar. */
|
|
39
|
+
navbarClass?: string;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
const { title, active, panel, contentClass } = Astro.props;
|
|
42
|
+
const { title, active, panel, contentClass, navbarClass } = Astro.props;
|
|
41
43
|
---
|
|
42
44
|
|
|
43
45
|
<html lang="en" class="lab-shell" data-lab-page={active}>
|
|
@@ -47,8 +49,9 @@ const { title, active, panel, contentClass } = Astro.props;
|
|
|
47
49
|
<meta name="generator" content={Astro.generator} />
|
|
48
50
|
<title>{`${title} · ${labConfig.title}`}</title>
|
|
49
51
|
<LabHead />
|
|
50
|
-
{/* The collapse state, before paint — so a closed panel never
|
|
51
|
-
|
|
52
|
+
{/* The collapse state and the Sections tree's sort, before paint — so a closed panel never
|
|
53
|
+
flashes open and a tree sorted by name never flashes in slot order. Level 1 starts as the
|
|
54
|
+
rail on a narrow window unless it was explicitly expanded there. */}
|
|
52
55
|
<script is:inline>
|
|
53
56
|
(() => {
|
|
54
57
|
try {
|
|
@@ -57,6 +60,7 @@ const { title, active, panel, contentClass } = Astro.props;
|
|
|
57
60
|
const l2 = localStorage.getItem("lab-l2");
|
|
58
61
|
if (l1 === "rail" || (!l1 && window.innerWidth < 1024)) root.dataset.labL1 = "rail";
|
|
59
62
|
if (l2 === "closed") root.dataset.labL2 = "closed";
|
|
63
|
+
if (localStorage.getItem("lab-tree-sort") === "name") root.dataset.labSort = "name";
|
|
60
64
|
} catch {
|
|
61
65
|
/* storage blocked — the defaults are fine */
|
|
62
66
|
}
|
|
@@ -83,12 +87,22 @@ const { title, active, panel, contentClass } = Astro.props;
|
|
|
83
87
|
}
|
|
84
88
|
<main class="lab-main" id="lab-main">
|
|
85
89
|
{panel && <div class="lab-peek-zone" data-lab-peek-zone aria-hidden="true" />}
|
|
86
|
-
<header class="lab-navbar">
|
|
90
|
+
<header class:list={["lab-navbar", navbarClass]} data-lab-navbar>
|
|
87
91
|
{
|
|
88
92
|
panel && (
|
|
89
|
-
<button
|
|
93
|
+
<button
|
|
94
|
+
class="lab-icon-btn lab-navbar__panel-btn lab-tipped"
|
|
95
|
+
type="button"
|
|
96
|
+
data-lab-toggle="l2"
|
|
97
|
+
aria-label="Show panel"
|
|
98
|
+
aria-describedby="lab-tip-panel"
|
|
99
|
+
>
|
|
90
100
|
<Icon name="panel-open" />
|
|
91
|
-
<span class="lab-
|
|
101
|
+
<span class="lab-tip lab-tip--panel" id="lab-tip-panel" role="tooltip">
|
|
102
|
+
<span class="lab-tip__title">Show panel · ⌘B</span>
|
|
103
|
+
<span class="lab-tip__text">⌘⇧B collapses the menu</span>
|
|
104
|
+
<span class="lab-tip__text">⌘. hides both (focus mode)</span>
|
|
105
|
+
</span>
|
|
92
106
|
</button>
|
|
93
107
|
)
|
|
94
108
|
}
|