@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
|
@@ -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>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The chrome document — every lab page renders through this, and nothing of the consumer's does.
|
|
3
|
+
// Level 1 (./Nav.astro), level 2 (./Panel.astro, when the page has a list to show), and main: the
|
|
4
|
+
// navbar (slot "navbar") above the content (default slot). Figma `Astrolab` › Design › Desktop.
|
|
5
|
+
//
|
|
6
|
+
// PURE CHROME, NO CONSUMER CSS. Previews are <iframe>s of their own documents (the bare story
|
|
7
|
+
// route, or the site's own page), so this document never imports `virtual:astrobook/user-css.mjs`
|
|
8
|
+
// or the consumer's head. That split is what testbeds/orbytes-v2/scripts/check-isolation.mjs
|
|
9
|
+
// asserts, both ways, on every built page.
|
|
10
|
+
//
|
|
11
|
+
// NO CLIENT ROUTER. A click is a real page load; `@view-transition { navigation: auto }` in the
|
|
12
|
+
// stylesheet keeps the old page painted until the new one is ready, and everything worth keeping
|
|
13
|
+
// across a load — which levels are collapsed, which tree groups are closed, the tree's scroll — is
|
|
14
|
+
// in storage and re-applied before paint. That keeps every script here a plain module that runs
|
|
15
|
+
// once per page, with none of the re-run guards a swapped document needs.
|
|
16
|
+
import LabHead from "../LabHead.astro";
|
|
17
|
+
import Nav from "./Nav.astro";
|
|
18
|
+
import Panel from "./Panel.astro";
|
|
19
|
+
import Sprite from "./Sprite.astro";
|
|
20
|
+
import Icon from "./Icon.astro";
|
|
21
|
+
import { labConfig, type Crumb, type FilterOption, type NavKey } from "./model";
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
/** The document title's first half; the lab's own title follows it. */
|
|
25
|
+
title: string;
|
|
26
|
+
active: NavKey;
|
|
27
|
+
/** Level 2. Omit `panel` for a page with no list to show (Home, Settings): level 2 is not drawn. */
|
|
28
|
+
panel?: {
|
|
29
|
+
label: string;
|
|
30
|
+
crumbs: Crumb[];
|
|
31
|
+
search?: boolean;
|
|
32
|
+
searchPlaceholder?: string;
|
|
33
|
+
filters?: FilterOption[];
|
|
34
|
+
filterLabel?: string;
|
|
35
|
+
};
|
|
36
|
+
/** Extra class on main's content box, e.g. to make it a positioning context for the stage. */
|
|
37
|
+
contentClass?: string;
|
|
38
|
+
/** Extra class on the navbar, e.g. `lab-navbar--listing` for a listing's title bar. */
|
|
39
|
+
navbarClass?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { title, active, panel, contentClass, navbarClass } = Astro.props;
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<html lang="en" class="lab-shell" data-lab-page={active}>
|
|
46
|
+
<head>
|
|
47
|
+
<meta charset="utf-8" />
|
|
48
|
+
<meta name="viewport" content="width=device-width" />
|
|
49
|
+
<meta name="generator" content={Astro.generator} />
|
|
50
|
+
<title>{`${title} · ${labConfig.title}`}</title>
|
|
51
|
+
<LabHead />
|
|
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. */}
|
|
55
|
+
<script is:inline>
|
|
56
|
+
(() => {
|
|
57
|
+
try {
|
|
58
|
+
const root = document.documentElement;
|
|
59
|
+
const l1 = localStorage.getItem("lab-l1");
|
|
60
|
+
const l2 = localStorage.getItem("lab-l2");
|
|
61
|
+
if (l1 === "rail" || (!l1 && window.innerWidth < 1024)) root.dataset.labL1 = "rail";
|
|
62
|
+
if (l2 === "closed") root.dataset.labL2 = "closed";
|
|
63
|
+
if (localStorage.getItem("lab-tree-sort") === "name") root.dataset.labSort = "name";
|
|
64
|
+
} catch {
|
|
65
|
+
/* storage blocked — the defaults are fine */
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
</script>
|
|
69
|
+
</head>
|
|
70
|
+
<body class="lab-shell">
|
|
71
|
+
<Sprite />
|
|
72
|
+
<div class="lab-app">
|
|
73
|
+
<Nav active={active} />
|
|
74
|
+
{
|
|
75
|
+
panel && (
|
|
76
|
+
<Panel
|
|
77
|
+
label={panel.label}
|
|
78
|
+
crumbs={panel.crumbs}
|
|
79
|
+
search={panel.search}
|
|
80
|
+
searchPlaceholder={panel.searchPlaceholder}
|
|
81
|
+
filters={panel.filters}
|
|
82
|
+
filterLabel={panel.filterLabel}
|
|
83
|
+
>
|
|
84
|
+
<slot name="panel" />
|
|
85
|
+
</Panel>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
<main class="lab-main" id="lab-main">
|
|
89
|
+
{panel && <div class="lab-peek-zone" data-lab-peek-zone aria-hidden="true" />}
|
|
90
|
+
<header class:list={["lab-navbar", navbarClass]} data-lab-navbar>
|
|
91
|
+
{
|
|
92
|
+
panel && (
|
|
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
|
+
>
|
|
100
|
+
<Icon name="panel-open" />
|
|
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>
|
|
106
|
+
</button>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
<slot name="navbar" />
|
|
110
|
+
</header>
|
|
111
|
+
<div class:list={["lab-content", contentClass]} id="lab-content">
|
|
112
|
+
<slot />
|
|
113
|
+
</div>
|
|
114
|
+
</main>
|
|
115
|
+
</div>
|
|
116
|
+
<script>
|
|
117
|
+
import "./shell-client";
|
|
118
|
+
</script>
|
|
119
|
+
</body>
|
|
120
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The icon sprite — every icon the chrome uses, once per page, as <symbol>s that ./Icon.astro
|
|
3
|
+
// points at with <use>. A tree of fifty rows costs fifty `<use href>` tags rather than fifty copies
|
|
4
|
+
// of the same path data. Shapes and licence: ./icons.ts.
|
|
5
|
+
import { ICONS } from "./icons";
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<svg aria-hidden="true" focusable="false" style="position:absolute;width:0;height:0;overflow:hidden">
|
|
9
|
+
{
|
|
10
|
+
Object.entries(ICONS).map(([name, inner]) => (
|
|
11
|
+
<symbol
|
|
12
|
+
id={`lab-i-${name}`}
|
|
13
|
+
viewBox="0 0 24 24"
|
|
14
|
+
fill="none"
|
|
15
|
+
stroke="currentColor"
|
|
16
|
+
stroke-width="2"
|
|
17
|
+
stroke-linecap="round"
|
|
18
|
+
stroke-linejoin="round"
|
|
19
|
+
set:html={inner}
|
|
20
|
+
/>
|
|
21
|
+
))
|
|
22
|
+
}
|
|
23
|
+
</svg>
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A component's page — <subpath>/dashboard/<storyId>, the route Astrobook's core generates for every
|
|
3
|
+
// story (../core/lib/pages/story.astro renders this).
|
|
4
|
+
//
|
|
5
|
+
// · the story is an <iframe> of its bare route on a canvas you size (./ViewportStage.astro), never
|
|
6
|
+
// rendered inline beside the chrome — so this page carries none of the site's CSS;
|
|
7
|
+
// · the tree stops at the component; the component's variants are ALWAYS a dropdown ("Default
|
|
8
|
+
// 1 of 3"), at any count, and previous / next (and j / k) step through every variant of the
|
|
9
|
+
// tier in tree order (decided 2026-09-24: no tabs in the navbar);
|
|
10
|
+
// · the identity is the name, the version and an eye: no pills. The eye opens the Properties
|
|
11
|
+
// panel — live and slot, open pins, the responsive marks, the source file (./Properties.astro);
|
|
12
|
+
// · everything that leaves the lab, and the pin and preview switches, are in ⋯ (./ActionsMenu.astro).
|
|
13
|
+
//
|
|
14
|
+
// THE NAVBAR (Figma `Astrolab` › Lab / Components › Navbar, View=Story) is one grid laid out by a
|
|
15
|
+
// container query on its own width, because the sidebars change the main area and never the
|
|
16
|
+
// viewport: ONE row when the navbar is 1040px wide or more — identity · variants · previous / next
|
|
17
|
+
// · device switch + Viewport field · Comment · Parameters · ⋯ — otherwise TWO rows, the viewport
|
|
18
|
+
// row last, and under 552px three or four, the identity on a row of its own with ⋯. The rules are
|
|
19
|
+
// lab.css › "The navbar" and "The narrow navbar".
|
|
20
|
+
import Shell from "./Shell.astro";
|
|
21
|
+
import Tree from "./Tree.astro";
|
|
22
|
+
import Icon from "./Icon.astro";
|
|
23
|
+
import Properties from "./Properties.astro";
|
|
24
|
+
import ActionsMenu from "./ActionsMenu.astro";
|
|
25
|
+
import ViewportControls from "./ViewportControls.astro";
|
|
26
|
+
import ViewportStage from "./ViewportStage.astro";
|
|
27
|
+
import { hrefs, tasksBase, type Crumb, type TreeNode } from "./model";
|
|
28
|
+
import { componentsOf, tierTree } from "./trees";
|
|
29
|
+
import { pinCounts } from "./pins-data";
|
|
30
|
+
import { buildLabIndex, isSectionLike, tierLabel } from "../shell/lab-index";
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
story: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const index = buildLabIndex();
|
|
37
|
+
const item = index.items.find((i) => i.storyId === Astro.props.story)!;
|
|
38
|
+
const mod = componentsOf(index).find((c) => c.moduleId === item.moduleId)!;
|
|
39
|
+
const tree = tierTree(index, item.tier, mod.moduleId);
|
|
40
|
+
|
|
41
|
+
// Every story of the tier, in the tree's own order, for previous / next.
|
|
42
|
+
const leaves = (nodes: TreeNode[]): TreeNode[] => nodes.flatMap((n) => (n.children ? leaves(n.children) : [n]));
|
|
43
|
+
const modules = new Map(componentsOf(index).map((c) => [`mod:${c.moduleId}`, c]));
|
|
44
|
+
const sequence = leaves(tree).flatMap((leaf) => modules.get(leaf.id)?.stories ?? []);
|
|
45
|
+
const at = sequence.findIndex((s) => s.storyId === item.storyId);
|
|
46
|
+
const prev = at > 0 ? sequence[at - 1] : null;
|
|
47
|
+
const next = at >= 0 && at < sequence.length - 1 ? sequence[at + 1] : null;
|
|
48
|
+
|
|
49
|
+
const pins = await pinCounts();
|
|
50
|
+
const tally = pins.forFile(mod.componentFile);
|
|
51
|
+
const liveStory = mod.stories.find((s) => s.live);
|
|
52
|
+
const section = isSectionLike(item);
|
|
53
|
+
const variantAt = mod.stories.indexOf(item) + 1;
|
|
54
|
+
|
|
55
|
+
// The Properties panel's "On the site" lines: every page the component is mounted on, or what
|
|
56
|
+
// uses it, or the honest grey "Not on any page".
|
|
57
|
+
const site = liveStory
|
|
58
|
+
? liveStory.liveOn.map((m) => ({ text: `Live on ${m.page} — slot ${m.slot} of ${m.of}`, live: true }))
|
|
59
|
+
: mod.usedBy.length
|
|
60
|
+
? [{ text: `Used by ${mod.usedBy.length}`, live: true }]
|
|
61
|
+
: [{ text: "Not on any page", live: false }];
|
|
62
|
+
|
|
63
|
+
// Level 2's breadcrumb names the PANEL's place — Site › Sections — as the design does; where the
|
|
64
|
+
// component sits inside the tier is the tree's job, and which component it is, the navbar's.
|
|
65
|
+
const crumbs: Crumb[] = [{ label: "Site" }, { label: tierLabel(item.tier), href: hrefs.tier(item.tier) }];
|
|
66
|
+
const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
<Shell
|
|
70
|
+
title={`${mod.moduleName} — ${item.storyName}`}
|
|
71
|
+
active={`tier:${item.tier}`}
|
|
72
|
+
panel={{
|
|
73
|
+
label: tierLabel(item.tier),
|
|
74
|
+
crumbs,
|
|
75
|
+
searchPlaceholder: `Search ${tierLabel(item.tier).toLowerCase()}`,
|
|
76
|
+
filterLabel: "Show components",
|
|
77
|
+
filters: [
|
|
78
|
+
{ value: "live", label: "Live on the site" },
|
|
79
|
+
{ value: "used", label: "Used by something live" },
|
|
80
|
+
{ value: "unused", label: "Unused" },
|
|
81
|
+
],
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<Tree slot="panel" nodes={tree} />
|
|
85
|
+
|
|
86
|
+
<div class="lab-nb" slot="navbar" data-lab-nb>
|
|
87
|
+
<div class="lab-nb__id">
|
|
88
|
+
<div class="lab-nb__line">
|
|
89
|
+
<h1 class="lab-nb__name" title={mod.moduleName}>{mod.moduleName}</h1>
|
|
90
|
+
{mod.version && <span class="lab-nb__tag">{mod.version}</span>}
|
|
91
|
+
<Properties
|
|
92
|
+
site={site}
|
|
93
|
+
pins={tasksBase ? { ...tally, href: `${tasksBase}/all` } : null}
|
|
94
|
+
responsive={mod.responsive ? { file: mod.moduleFile, ...mod.responsive } : null}
|
|
95
|
+
source={mod.componentFile}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="lab-nb__variants">
|
|
101
|
+
<button
|
|
102
|
+
class="lab-variant-btn"
|
|
103
|
+
type="button"
|
|
104
|
+
popovertarget="lab-variant-menu"
|
|
105
|
+
title={`${plural(mod.stories.length, "variant")} · j / k step`}
|
|
106
|
+
>
|
|
107
|
+
<span class="lab-variant-btn__name">{item.storyName}</span>
|
|
108
|
+
<span class="lab-variant-btn__count">{variantAt} of {mod.stories.length}</span>
|
|
109
|
+
<Icon name="chevron-down" size="sm" />
|
|
110
|
+
</button>
|
|
111
|
+
<div class="lab-menu lab-menu--variants" id="lab-variant-menu" popover role="menu" data-lab-align="end">
|
|
112
|
+
<p class="lab-menu__label">{plural(mod.stories.length, "variant")}</p>
|
|
113
|
+
{
|
|
114
|
+
mod.stories.map((s) => (
|
|
115
|
+
<a
|
|
116
|
+
class="lab-menu__item"
|
|
117
|
+
role="menuitemradio"
|
|
118
|
+
aria-checked={String(s.storyId === item.storyId)}
|
|
119
|
+
href={s.dashboardUrl}
|
|
120
|
+
data-lab-keep-viewport
|
|
121
|
+
>
|
|
122
|
+
<span class="lab-menu__mark"><Icon name="check" size="sm" class="lab-menu__tick" /></span>
|
|
123
|
+
<span class="lab-menu__text">{s.storyName}</span>
|
|
124
|
+
</a>
|
|
125
|
+
))
|
|
126
|
+
}
|
|
127
|
+
<hr />
|
|
128
|
+
<p class="lab-menu__note">j / k — next and previous</p>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="lab-nb__step" role="group" aria-label="Previous and next variant">
|
|
132
|
+
{
|
|
133
|
+
prev ? (
|
|
134
|
+
<a
|
|
135
|
+
class="lab-tool lab-tool--icon"
|
|
136
|
+
href={prev.dashboardUrl}
|
|
137
|
+
title={`Previous variant · k — ${prev.moduleName} — ${prev.storyName}`}
|
|
138
|
+
data-lab-step="prev"
|
|
139
|
+
data-lab-keep-viewport
|
|
140
|
+
>
|
|
141
|
+
<Icon name="chevron-left" size="sm" />
|
|
142
|
+
<span class="lab-visually-hidden">Previous variant</span>
|
|
143
|
+
</a>
|
|
144
|
+
) : (
|
|
145
|
+
<button class="lab-tool lab-tool--icon" type="button" disabled data-lab-step>
|
|
146
|
+
<Icon name="chevron-left" size="sm" />
|
|
147
|
+
<span class="lab-visually-hidden">Previous variant</span>
|
|
148
|
+
</button>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
{
|
|
152
|
+
next ? (
|
|
153
|
+
<a
|
|
154
|
+
class="lab-tool lab-tool--icon"
|
|
155
|
+
href={next.dashboardUrl}
|
|
156
|
+
title={`Next variant · j — ${next.moduleName} — ${next.storyName}`}
|
|
157
|
+
data-lab-step="next"
|
|
158
|
+
data-lab-keep-viewport
|
|
159
|
+
>
|
|
160
|
+
<Icon name="chevron-right" size="sm" />
|
|
161
|
+
<span class="lab-visually-hidden">Next variant</span>
|
|
162
|
+
</a>
|
|
163
|
+
) : (
|
|
164
|
+
<button class="lab-tool lab-tool--icon" type="button" disabled data-lab-step>
|
|
165
|
+
<Icon name="chevron-right" size="sm" />
|
|
166
|
+
<span class="lab-visually-hidden">Next variant</span>
|
|
167
|
+
</button>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<ViewportControls
|
|
174
|
+
defaultW={section ? 1440 : 960}
|
|
175
|
+
defaultH={section ? 900 : 540}
|
|
176
|
+
kind={section ? "section" : "component"}
|
|
177
|
+
comment={Boolean(tasksBase)}
|
|
178
|
+
parameters
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<div class="lab-nb__actions">
|
|
182
|
+
<ActionsMenu editorUrl={item.editorUrl} fullScreenUrl={item.storyUrl} stagingUrl={item.stagingUrl} pins={Boolean(tasksBase)} />
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<ViewportStage src={item.storyUrl} title={`${mod.moduleName} — ${item.storyName}`} pins={Boolean(tasksBase)} tasksHref={tasksBase ? `${tasksBase}/all` : null} parameters />
|
|
187
|
+
</Shell>
|
|
188
|
+
|
|
189
|
+
<script>
|
|
190
|
+
import "./marks-client";
|
|
191
|
+
import "./navbar-client";
|
|
192
|
+
</script>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The level 2 tree (Figma `Tree view`, sm) — recursive, server-rendered, and usable with no script:
|
|
3
|
+
// groups are <details>, rows are links. The shell script adds memory (which groups you closed),
|
|
4
|
+
// search, the filter menu and j/k on top. Connector lines are CSS on the <li>, so the tree carries
|
|
5
|
+
// no connector markup at all.
|
|
6
|
+
//
|
|
7
|
+
// Rows carry no icons and read at 12px (decided 2026-09-24, to condense level 2). A node's `icon`
|
|
8
|
+
// is still accepted — the data layer keeps it — and simply not drawn.
|
|
9
|
+
//
|
|
10
|
+
// A tree whose root rows carry a slot is the Sections tree, which draws its own rows and sort
|
|
11
|
+
// control (./SectionsTree.astro).
|
|
12
|
+
import Icon from "./Icon.astro";
|
|
13
|
+
import SectionsTree from "./SectionsTree.astro";
|
|
14
|
+
import type { TreeNode } from "./model";
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
nodes: TreeNode[];
|
|
18
|
+
depth?: number;
|
|
19
|
+
/** Set on the outermost call only. */
|
|
20
|
+
root?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { nodes, depth = 0, root = true } = Astro.props;
|
|
24
|
+
const sections = root && nodes.some((node) => node.slot !== undefined);
|
|
25
|
+
|
|
26
|
+
/** Whether a node is, or holds, the current page — those groups render open whatever was stored. */
|
|
27
|
+
const holdsCurrent = (node: TreeNode): boolean =>
|
|
28
|
+
Boolean(node.current) || (node.children ?? []).some(holdsCurrent);
|
|
29
|
+
|
|
30
|
+
const searchText = (node: TreeNode) => `${node.label} ${node.search ?? ""}`.toLowerCase();
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
sections ? (
|
|
35
|
+
<SectionsTree nodes={nodes} />
|
|
36
|
+
) : (
|
|
37
|
+
<ul class={root ? "lab-tree" : "lab-tree__group"} role={root ? "tree" : "group"} data-lab-tree={root ? "" : undefined}>
|
|
38
|
+
{nodes.map((node) => {
|
|
39
|
+
const hasChildren = (node.children?.length ?? 0) > 0;
|
|
40
|
+
const trailing = (
|
|
41
|
+
<>
|
|
42
|
+
{node.count !== undefined && <span class="lab-tree__count">{node.count}</span>}
|
|
43
|
+
{node.dot && <span class={`lab-dot lab-dot--${node.dot}`} title={node.dotTitle} />}
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
return (
|
|
47
|
+
<li
|
|
48
|
+
class="lab-tree__node"
|
|
49
|
+
role="none"
|
|
50
|
+
style={`--lab-d:${depth}`}
|
|
51
|
+
data-lab-search={searchText(node)}
|
|
52
|
+
data-lab-facets={node.facets}
|
|
53
|
+
>
|
|
54
|
+
{hasChildren ? (
|
|
55
|
+
<details open data-lab-tree-id={node.id} data-lab-holds-current={holdsCurrent(node) ? "" : undefined}>
|
|
56
|
+
<summary class="lab-tree__row" role="treeitem" aria-current={node.current ? "page" : undefined} title={node.title}>
|
|
57
|
+
<span class="lab-tree__label">{node.href ? <a href={node.href}>{node.label}</a> : node.label}</span>
|
|
58
|
+
{trailing}
|
|
59
|
+
<Icon name="chevron-down" size="sm" class="lab-tree__chevron" />
|
|
60
|
+
</summary>
|
|
61
|
+
<Astro.self nodes={node.children!} depth={depth + 1} root={false} />
|
|
62
|
+
</details>
|
|
63
|
+
) : node.href && !node.disabled ? (
|
|
64
|
+
<a
|
|
65
|
+
class="lab-tree__row"
|
|
66
|
+
role="treeitem"
|
|
67
|
+
href={node.href}
|
|
68
|
+
aria-current={node.current ? "page" : undefined}
|
|
69
|
+
title={node.title}
|
|
70
|
+
data-lab-leaf
|
|
71
|
+
>
|
|
72
|
+
<span class="lab-tree__label">{node.label}</span>
|
|
73
|
+
{trailing}
|
|
74
|
+
</a>
|
|
75
|
+
) : (
|
|
76
|
+
<span class="lab-tree__row" role="treeitem" aria-disabled="true" title={node.title}>
|
|
77
|
+
<span class="lab-tree__label">{node.label}</span>
|
|
78
|
+
{trailing}
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
81
|
+
</li>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
</ul>
|
|
85
|
+
)
|
|
86
|
+
}
|