@orbytes/astrolab 0.3.0 → 0.4.0-next.1
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 +109 -64
- package/defaults.mjs +65 -0
- package/dist/core/virtual-module/virtual-routes.js +12 -1
- package/docs/PIN-CONTRACT.md +8 -0
- package/docs/PIN.md +29 -19
- package/index.d.ts +40 -16
- package/index.mjs +32 -10
- package/package.json +6 -2
- package/src/Home.astro +167 -264
- package/src/LabHead.astro +37 -1047
- package/src/chrome/ComponentCard.astro +69 -0
- package/src/chrome/Icon.astro +21 -0
- package/src/chrome/LICENSE-icons +43 -0
- package/src/chrome/Nav.astro +105 -0
- package/src/chrome/Panel.astro +104 -0
- package/src/chrome/Shell.astro +106 -0
- package/src/chrome/Sprite.astro +23 -0
- package/src/chrome/StoryView.astro +251 -0
- package/src/chrome/Tree.astro +83 -0
- package/src/chrome/ViewportControls.astro +98 -0
- package/src/chrome/ViewportStage.astro +32 -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 +59 -0
- package/src/chrome/marks-client.ts +102 -0
- package/src/chrome/model.ts +142 -0
- package/src/chrome/pins-data.ts +30 -0
- package/src/chrome/shell-client.ts +372 -0
- package/src/chrome/site-data.ts +230 -0
- package/src/chrome/trees.ts +152 -0
- package/src/chrome/viewport-client.ts +579 -0
- package/src/chrome/views/Assets.astro +110 -0
- package/src/chrome/views/Pages.astro +178 -0
- package/src/chrome/views/Placeholder.astro +37 -0
- package/src/chrome/views/Tasks.astro +107 -0
- package/src/core/LICENSE-astrobook +16 -0
- package/src/core/lib/components/home.astro +4 -2
- package/src/core/lib/pages/story.astro +12 -10
- package/src/core/virtual-module/virtual-routes.ts +20 -4
- package/src/pin/board.mjs +389 -175
- package/src/pin/index.mjs +33 -2
- package/src/pin/toolbar.js +1 -1
- package/src/shell/Browse.astro +106 -353
- package/src/shell/Viewport.astro +22 -1315
- package/src/shell/lab-index.ts +23 -14
- package/src/ui/components/app.astro +5 -7
- package/src/ui/components/theme-script.astro +13 -2
- package/src/ui/lab.css +2152 -370
- package/virtual.d.ts +13 -0
- 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,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
// One card per COMPONENT (a stories module), not per story — the tree stops at the component and
|
|
3
|
+
// so does the grid. The thumbnail is its first story's bare render (/lab/stories/<id>) in a lazy
|
|
4
|
+
// <iframe> sized to a virtual viewport and scaled to the card by ../chrome/shell-client.ts: page
|
|
5
|
+
// width for section-like tiers, a component stage otherwise. The whole card opens the component;
|
|
6
|
+
// the icon links under it sit above that link.
|
|
7
|
+
import Icon from "./Icon.astro";
|
|
8
|
+
import { componentState, type LabComponent } from "./trees";
|
|
9
|
+
import { isSectionLike, livePillText, liveTitle } from "../shell/lab-index";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
mod: LabComponent;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { mod } = Astro.props;
|
|
16
|
+
const first = mod.stories[0]!;
|
|
17
|
+
const liveStory = mod.stories.find((s) => s.live);
|
|
18
|
+
const section = isSectionLike(first);
|
|
19
|
+
const state = componentState(mod);
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<article class="lab-card" data-lab-search={state.search} data-lab-facets={state.facets}>
|
|
23
|
+
<a class="lab-card__main" href={first.dashboardUrl}>
|
|
24
|
+
<span class="lab-card__thumb" data-frame-w={section ? 1440 : 960} data-frame-h={section ? 900 : 540}>
|
|
25
|
+
<iframe src={first.storyUrl} title={`${mod.moduleName} — ${first.storyName}`} loading="lazy" tabindex="-1"></iframe>
|
|
26
|
+
</span>
|
|
27
|
+
<span class="lab-card__body">
|
|
28
|
+
<span class="lab-card__name">
|
|
29
|
+
<span>{mod.moduleName}</span>
|
|
30
|
+
{mod.version && <span class="lab-card__version">{mod.version}</span>}
|
|
31
|
+
</span>
|
|
32
|
+
<span class="lab-card__sub">
|
|
33
|
+
{mod.stories.length} stor{mod.stories.length === 1 ? "y" : "ies"}{mod.summary ? ` · ${mod.summary}` : ""}
|
|
34
|
+
</span>
|
|
35
|
+
<span class="lab-card__pills">
|
|
36
|
+
{liveStory && <span class="lab-pill lab-pill--live" title={liveTitle(liveStory)}>{livePillText(liveStory)}</span>}
|
|
37
|
+
{!mod.live && mod.usedBy.length > 0 && (
|
|
38
|
+
<span class="lab-pill lab-pill--used" title={mod.usedBy.join("\n")}>used by {mod.usedBy.length}</span>
|
|
39
|
+
)}
|
|
40
|
+
{mod.responsive && (
|
|
41
|
+
<span
|
|
42
|
+
class:list={["lab-pill", mod.responsive.done ? "lab-pill--responsive" : "lab-pill--unresponsive"]}
|
|
43
|
+
title={mod.responsive.approved ? "approved for responsive work" : "not approved for responsive work yet"}
|
|
44
|
+
>
|
|
45
|
+
{mod.responsive.done ? "responsive" : "not responsive"}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
</span>
|
|
49
|
+
</span>
|
|
50
|
+
</a>
|
|
51
|
+
{
|
|
52
|
+
(first.editorUrl || first.stagingUrl) && (
|
|
53
|
+
<span class="lab-card__links">
|
|
54
|
+
{first.editorUrl && (
|
|
55
|
+
<a class="lab-icon-btn lab-card__link" href={first.editorUrl} title="Open in VS Code">
|
|
56
|
+
<Icon name="code" />
|
|
57
|
+
<span class="lab-visually-hidden">Open in VS Code</span>
|
|
58
|
+
</a>
|
|
59
|
+
)}
|
|
60
|
+
{first.stagingUrl && (
|
|
61
|
+
<a class="lab-icon-btn lab-card__link" href={first.stagingUrl} target="_blank" rel="noopener" title="View on staging">
|
|
62
|
+
<Icon name="external" />
|
|
63
|
+
<span class="lab-visually-hidden">View on staging</span>
|
|
64
|
+
</a>
|
|
65
|
+
)}
|
|
66
|
+
</span>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
</article>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
// One icon from the sprite (./Sprite.astro). Decorative by default; pass `label` when the icon is
|
|
3
|
+
// the only thing naming a control.
|
|
4
|
+
import type { IconName } from "./icons";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
name: IconName;
|
|
8
|
+
size?: "sm" | "md";
|
|
9
|
+
class?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { name, size = "md", class: extra, label } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<svg
|
|
17
|
+
class:list={["lab-icon", size === "sm" && "lab-icon--sm", extra]}
|
|
18
|
+
aria-hidden={label ? undefined : "true"}
|
|
19
|
+
role={label ? "img" : undefined}
|
|
20
|
+
aria-label={label}
|
|
21
|
+
><use href={`#lab-i-${name}`}></use></svg>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucide Icons and Contributors
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
The following Lucide icons are derived from the Feather project:
|
|
20
|
+
|
|
21
|
+
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
|
22
|
+
|
|
23
|
+
The MIT License (MIT) (for the icons listed above)
|
|
24
|
+
|
|
25
|
+
Copyright (c) 2013-present Cole Bemis
|
|
26
|
+
|
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
29
|
+
in the Software without restriction, including without limitation the rights
|
|
30
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
31
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
furnished to do so, subject to the following conditions:
|
|
33
|
+
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
42
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
43
|
+
SOFTWARE.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Level 1 — the main nav (Figma `Sidebar navigation`, the white card). Home, the Site group (Pages,
|
|
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 56px icon strip (⌘⇧B); in the strip a group's items open
|
|
5
|
+
// as a flyout on hover or focus, drawn by CSS alone (../ui/lab.css › level 1 as a rail).
|
|
6
|
+
//
|
|
7
|
+
// The logo and the Support / Settings pages are placeholders until they are designed.
|
|
8
|
+
import Icon from "./Icon.astro";
|
|
9
|
+
import { labConfig, navModel, type NavKey } from "./model";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
active: NavKey;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { active } = Astro.props;
|
|
16
|
+
const model = navModel();
|
|
17
|
+
const isOpen = (group: { items: { key: NavKey }[] }) => group.items.some((item) => item.key === active);
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<nav class="lab-l1" aria-label="Lab">
|
|
21
|
+
<div class="lab-l1__card">
|
|
22
|
+
<div class="lab-l1__top">
|
|
23
|
+
<div class="lab-l1__header">
|
|
24
|
+
<a class="lab-brand" href={model.home.href} title={labConfig.title}>
|
|
25
|
+
<span class="lab-brand__mark" aria-hidden="true"></span>
|
|
26
|
+
<span class="lab-brand__name">Astrolab</span>
|
|
27
|
+
</a>
|
|
28
|
+
<button class="lab-icon-btn lab-theme-btn" type="button" data-lab-theme-toggle title="Light or dark lab">
|
|
29
|
+
<Icon name="sun" class="lab-icon--sun" />
|
|
30
|
+
<Icon name="moon" class="lab-icon--moon" />
|
|
31
|
+
<span class="lab-visually-hidden">Toggle the lab's light or dark theme</span>
|
|
32
|
+
</button>
|
|
33
|
+
<button class="lab-icon-btn" type="button" data-lab-toggle="l1" title="Collapse the menu (⌘⇧B)" data-lab-l1-btn>
|
|
34
|
+
<Icon name="chevrons-left" class="lab-l1-btn__collapse" />
|
|
35
|
+
<Icon name="chevrons-right" class="lab-l1-btn__expand" />
|
|
36
|
+
<span class="lab-visually-hidden">Collapse or expand the menu</span>
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="lab-l1__nav">
|
|
41
|
+
<a
|
|
42
|
+
class="lab-nav-item"
|
|
43
|
+
href={model.home.href}
|
|
44
|
+
aria-current={active === "home" ? "page" : undefined}
|
|
45
|
+
data-tip={model.home.label}
|
|
46
|
+
data-lab-nav-key="home"
|
|
47
|
+
>
|
|
48
|
+
<Icon name={model.home.icon} />
|
|
49
|
+
<span class="lab-nav-item__label">{model.home.label}</span>
|
|
50
|
+
</a>
|
|
51
|
+
{
|
|
52
|
+
model.groups.map((group) => (
|
|
53
|
+
<>
|
|
54
|
+
<hr class="lab-divider" />
|
|
55
|
+
<div class="lab-nav-group" data-lab-group={group.id} data-lab-holds-current={isOpen(group) ? "" : undefined}>
|
|
56
|
+
<button
|
|
57
|
+
class="lab-nav-item"
|
|
58
|
+
type="button"
|
|
59
|
+
aria-expanded="true"
|
|
60
|
+
aria-current={isOpen(group) ? "true" : undefined}
|
|
61
|
+
data-tip={group.label}
|
|
62
|
+
data-lab-group-toggle
|
|
63
|
+
>
|
|
64
|
+
<Icon name={group.icon} />
|
|
65
|
+
<span class="lab-nav-item__label">{group.label}</span>
|
|
66
|
+
<Icon name="chevron-down" size="sm" class="lab-nav-group__chevron" />
|
|
67
|
+
</button>
|
|
68
|
+
<div class="lab-nav-group__items">
|
|
69
|
+
{group.items.map((item) => (
|
|
70
|
+
<a
|
|
71
|
+
class="lab-nav-item"
|
|
72
|
+
href={item.href}
|
|
73
|
+
aria-current={item.key === active ? "page" : undefined}
|
|
74
|
+
data-lab-nav-key={item.key}
|
|
75
|
+
>
|
|
76
|
+
<Icon name={item.icon} />
|
|
77
|
+
<span class="lab-nav-item__label">{item.label}</span>
|
|
78
|
+
</a>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</>
|
|
83
|
+
))
|
|
84
|
+
}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="lab-l1__footer">
|
|
89
|
+
{
|
|
90
|
+
model.footer.map((item) => (
|
|
91
|
+
<a
|
|
92
|
+
class="lab-nav-item"
|
|
93
|
+
href={item.href}
|
|
94
|
+
aria-current={item.key === active ? "page" : undefined}
|
|
95
|
+
data-tip={item.label}
|
|
96
|
+
data-lab-nav-key={item.key}
|
|
97
|
+
>
|
|
98
|
+
<Icon name={item.icon} />
|
|
99
|
+
<span class="lab-nav-item__label">{item.label}</span>
|
|
100
|
+
</a>
|
|
101
|
+
))
|
|
102
|
+
}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</nav>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Level 2 — the panel (Figma `Sidebar navigation`, the grey column): breadcrumb, search with its
|
|
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 when the pointer reaches the stage's left edge.
|
|
5
|
+
//
|
|
6
|
+
// The coffee link is a placeholder until it has a destination.
|
|
7
|
+
import Icon from "./Icon.astro";
|
|
8
|
+
import { labBase, type Crumb, type FilterOption } from "./model";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
crumbs: Crumb[];
|
|
12
|
+
/** Label for the panel landmark, e.g. "Pages". */
|
|
13
|
+
label: string;
|
|
14
|
+
search?: boolean;
|
|
15
|
+
searchPlaceholder?: string;
|
|
16
|
+
filters?: FilterOption[];
|
|
17
|
+
filterLabel?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { crumbs, label, search = true, searchPlaceholder = "Search", filters, filterLabel = "Show" } = Astro.props;
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<aside class="lab-l2" id="lab-l2" aria-label={label}>
|
|
24
|
+
<div class="lab-l2__header">
|
|
25
|
+
<div class="lab-l2__crumbs-row">
|
|
26
|
+
<nav aria-label="Breadcrumb">
|
|
27
|
+
<ol class="lab-crumbs">
|
|
28
|
+
<li class="lab-crumbs__item">
|
|
29
|
+
<a href={labBase || "/"} title="Lab home"><Icon name="home" /><span class="lab-visually-hidden">Lab home</span></a>
|
|
30
|
+
</li>
|
|
31
|
+
{
|
|
32
|
+
crumbs.map((crumb, i) => (
|
|
33
|
+
<li class="lab-crumbs__item" aria-current={i === crumbs.length - 1 ? "page" : undefined}>
|
|
34
|
+
{crumb.href && i < crumbs.length - 1 ? <a href={crumb.href}>{crumb.label}</a> : crumb.label}
|
|
35
|
+
</li>
|
|
36
|
+
))
|
|
37
|
+
}
|
|
38
|
+
</ol>
|
|
39
|
+
</nav>
|
|
40
|
+
<button class="lab-icon-btn lab-l2__close" type="button" data-lab-toggle="l2" title="Close the panel (⌘B)">
|
|
41
|
+
<Icon name="panel-close" />
|
|
42
|
+
<span class="lab-visually-hidden">Close the panel</span>
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
search && (
|
|
48
|
+
<div class="lab-input">
|
|
49
|
+
<Icon name="search" size="sm" />
|
|
50
|
+
<input
|
|
51
|
+
class="lab-input__field"
|
|
52
|
+
type="search"
|
|
53
|
+
placeholder={searchPlaceholder}
|
|
54
|
+
aria-label={searchPlaceholder}
|
|
55
|
+
autocomplete="off"
|
|
56
|
+
spellcheck="false"
|
|
57
|
+
data-lab-tree-search
|
|
58
|
+
/>
|
|
59
|
+
<kbd class="lab-input__kbd">⌘K</kbd>
|
|
60
|
+
{filters && filters.length > 0 && (
|
|
61
|
+
<button
|
|
62
|
+
class="lab-icon-btn"
|
|
63
|
+
type="button"
|
|
64
|
+
popovertarget="lab-tree-filter"
|
|
65
|
+
title="Filter"
|
|
66
|
+
data-lab-filter-btn
|
|
67
|
+
>
|
|
68
|
+
<Icon name="filter" size="sm" />
|
|
69
|
+
<span class="lab-visually-hidden">Filter</span>
|
|
70
|
+
</button>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
{
|
|
76
|
+
filters && filters.length > 0 && (
|
|
77
|
+
<div class="lab-menu" id="lab-tree-filter" popover role="menu" data-lab-align="end">
|
|
78
|
+
<p class="lab-menu__label">{filterLabel}</p>
|
|
79
|
+
<button class="lab-menu__item" type="button" role="menuitemradio" aria-checked="true" data-lab-filter="">
|
|
80
|
+
All
|
|
81
|
+
<Icon name="check" size="sm" class="lab-menu__tick" />
|
|
82
|
+
</button>
|
|
83
|
+
{filters.map((option) => (
|
|
84
|
+
<button class="lab-menu__item" type="button" role="menuitemradio" aria-checked="false" data-lab-filter={option.value}>
|
|
85
|
+
{option.label}
|
|
86
|
+
<Icon name="check" size="sm" class="lab-menu__tick" />
|
|
87
|
+
</button>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="lab-l2__body" data-lab-l2-body>
|
|
95
|
+
<slot />
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="lab-l2__footer">
|
|
99
|
+
<span class="lab-nav-item lab-coffee" aria-disabled="true" title="The link is still to come">
|
|
100
|
+
<Icon name="coffee" />
|
|
101
|
+
<span class="lab-nav-item__label">Buy Me A Coffee</span>
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
</aside>
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
}
|
|
39
|
+
|
|
40
|
+
const { title, active, panel, contentClass } = Astro.props;
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
<html lang="en" class="lab-shell" data-lab-page={active}>
|
|
44
|
+
<head>
|
|
45
|
+
<meta charset="utf-8" />
|
|
46
|
+
<meta name="viewport" content="width=device-width" />
|
|
47
|
+
<meta name="generator" content={Astro.generator} />
|
|
48
|
+
<title>{`${title} · ${labConfig.title}`}</title>
|
|
49
|
+
<LabHead />
|
|
50
|
+
{/* The collapse state, before paint — so a closed panel never flashes open. Level 1 starts as
|
|
51
|
+
the rail on a narrow window unless it was explicitly expanded there. */}
|
|
52
|
+
<script is:inline>
|
|
53
|
+
(() => {
|
|
54
|
+
try {
|
|
55
|
+
const root = document.documentElement;
|
|
56
|
+
const l1 = localStorage.getItem("lab-l1");
|
|
57
|
+
const l2 = localStorage.getItem("lab-l2");
|
|
58
|
+
if (l1 === "rail" || (!l1 && window.innerWidth < 1024)) root.dataset.labL1 = "rail";
|
|
59
|
+
if (l2 === "closed") root.dataset.labL2 = "closed";
|
|
60
|
+
} catch {
|
|
61
|
+
/* storage blocked — the defaults are fine */
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
</script>
|
|
65
|
+
</head>
|
|
66
|
+
<body class="lab-shell">
|
|
67
|
+
<Sprite />
|
|
68
|
+
<div class="lab-app">
|
|
69
|
+
<Nav active={active} />
|
|
70
|
+
{
|
|
71
|
+
panel && (
|
|
72
|
+
<Panel
|
|
73
|
+
label={panel.label}
|
|
74
|
+
crumbs={panel.crumbs}
|
|
75
|
+
search={panel.search}
|
|
76
|
+
searchPlaceholder={panel.searchPlaceholder}
|
|
77
|
+
filters={panel.filters}
|
|
78
|
+
filterLabel={panel.filterLabel}
|
|
79
|
+
>
|
|
80
|
+
<slot name="panel" />
|
|
81
|
+
</Panel>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
<main class="lab-main" id="lab-main">
|
|
85
|
+
{panel && <div class="lab-peek-zone" data-lab-peek-zone aria-hidden="true" />}
|
|
86
|
+
<header class="lab-navbar">
|
|
87
|
+
{
|
|
88
|
+
panel && (
|
|
89
|
+
<button class="lab-icon-btn lab-navbar__panel-btn" type="button" data-lab-toggle="l2" title="Open the panel (⌘B)">
|
|
90
|
+
<Icon name="panel-open" />
|
|
91
|
+
<span class="lab-visually-hidden">Open the panel</span>
|
|
92
|
+
</button>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
<slot name="navbar" />
|
|
96
|
+
</header>
|
|
97
|
+
<div class:list={["lab-content", contentClass]} id="lab-content">
|
|
98
|
+
<slot />
|
|
99
|
+
</div>
|
|
100
|
+
</main>
|
|
101
|
+
</div>
|
|
102
|
+
<script>
|
|
103
|
+
import "./shell-client";
|
|
104
|
+
</script>
|
|
105
|
+
</body>
|
|
106
|
+
</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>
|