@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.
Files changed (69) hide show
  1. package/README.md +109 -64
  2. package/defaults.mjs +65 -0
  3. package/dist/core/virtual-module/virtual-routes.js +12 -1
  4. package/docs/PIN-CONTRACT.md +8 -0
  5. package/docs/PIN.md +29 -19
  6. package/index.d.ts +40 -16
  7. package/index.mjs +32 -10
  8. package/package.json +6 -2
  9. package/src/Home.astro +167 -264
  10. package/src/LabHead.astro +37 -1047
  11. package/src/chrome/ComponentCard.astro +69 -0
  12. package/src/chrome/Icon.astro +21 -0
  13. package/src/chrome/LICENSE-icons +43 -0
  14. package/src/chrome/Nav.astro +105 -0
  15. package/src/chrome/Panel.astro +104 -0
  16. package/src/chrome/Shell.astro +106 -0
  17. package/src/chrome/Sprite.astro +23 -0
  18. package/src/chrome/StoryView.astro +251 -0
  19. package/src/chrome/Tree.astro +83 -0
  20. package/src/chrome/ViewportControls.astro +98 -0
  21. package/src/chrome/ViewportStage.astro +32 -0
  22. package/src/chrome/fonts/OFL.txt +93 -0
  23. package/src/chrome/fonts/inter-latin-wght-normal.woff2 +0 -0
  24. package/src/chrome/icons.ts +59 -0
  25. package/src/chrome/marks-client.ts +102 -0
  26. package/src/chrome/model.ts +142 -0
  27. package/src/chrome/pins-data.ts +30 -0
  28. package/src/chrome/shell-client.ts +372 -0
  29. package/src/chrome/site-data.ts +230 -0
  30. package/src/chrome/trees.ts +152 -0
  31. package/src/chrome/viewport-client.ts +579 -0
  32. package/src/chrome/views/Assets.astro +110 -0
  33. package/src/chrome/views/Pages.astro +178 -0
  34. package/src/chrome/views/Placeholder.astro +37 -0
  35. package/src/chrome/views/Tasks.astro +107 -0
  36. package/src/core/LICENSE-astrobook +16 -0
  37. package/src/core/lib/components/home.astro +4 -2
  38. package/src/core/lib/pages/story.astro +12 -10
  39. package/src/core/virtual-module/virtual-routes.ts +20 -4
  40. package/src/pin/board.mjs +389 -175
  41. package/src/pin/index.mjs +33 -2
  42. package/src/pin/toolbar.js +1 -1
  43. package/src/shell/Browse.astro +106 -353
  44. package/src/shell/Viewport.astro +22 -1315
  45. package/src/shell/lab-index.ts +23 -14
  46. package/src/ui/components/app.astro +5 -7
  47. package/src/ui/components/theme-script.astro +13 -2
  48. package/src/ui/lab.css +2152 -370
  49. package/virtual.d.ts +13 -0
  50. package/src/shell/CardGrid.astro +0 -297
  51. package/src/ui/components/build-path.ts +0 -13
  52. package/src/ui/components/build-tree.ts +0 -108
  53. package/src/ui/components/collapse-duration.ts +0 -28
  54. package/src/ui/components/compress-terms.ts +0 -10
  55. package/src/ui/components/dashboard-layout.astro +0 -39
  56. package/src/ui/components/home.astro +0 -65
  57. package/src/ui/components/layout.astro +0 -110
  58. package/src/ui/components/sidebar-button-fullscreen.astro +0 -38
  59. package/src/ui/components/sidebar-button-search.astro +0 -23
  60. package/src/ui/components/sidebar-button-theme.astro +0 -9
  61. package/src/ui/components/sidebar-button.astro +0 -24
  62. package/src/ui/components/sidebar-resize-handle.astro +0 -74
  63. package/src/ui/components/sidebar-search-panel.astro +0 -41
  64. package/src/ui/components/sidebar-search-script.ts +0 -103
  65. package/src/ui/components/sidebar-title.astro +0 -17
  66. package/src/ui/components/sidebar-tree-node.astro +0 -143
  67. package/src/ui/components/sidebar-tree.astro +0 -84
  68. package/src/ui/components/sidebar.astro +0 -29
  69. package/src/ui/components/theme-toggle.astro +0 -63
@@ -0,0 +1,251 @@
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). REDESIGNED 2026-09-24:
4
+ //
5
+ // · the story is an <iframe> of its bare route on a canvas you size (./ViewportStage.astro), no
6
+ // longer rendered inline beside the chrome — so this page carries none of the site's CSS;
7
+ // · the tree stops at the component, and the component's stories are TABS here, a dropdown past
8
+ // four; previous / next (and j / k) step through every story of the tier in tree order;
9
+ // · the component's marks (responsive, marked for deletion) live in the status menu behind its
10
+ // pills, and everything that leaves the lab (VS Code, full screen, staging, a link) in ⋯.
11
+ //
12
+ // The navbar is interim — ops/navbar-brief.md is the design it will be replaced with.
13
+ import Shell from "./Shell.astro";
14
+ import Tree from "./Tree.astro";
15
+ import Icon from "./Icon.astro";
16
+ import ViewportControls from "./ViewportControls.astro";
17
+ import ViewportStage from "./ViewportStage.astro";
18
+ import { hrefs, labConfig, type Crumb, type TreeNode } from "./model";
19
+ import { componentsOf, tierTree } from "./trees";
20
+ import { pinCounts } from "./pins-data";
21
+ import { buildLabIndex, isSectionLike, livePillText, liveTitle, tierLabel } from "../shell/lab-index";
22
+
23
+ interface Props {
24
+ story: string;
25
+ }
26
+
27
+ const index = buildLabIndex();
28
+ const item = index.items.find((i) => i.storyId === Astro.props.story)!;
29
+ const mod = componentsOf(index).find((c) => c.moduleId === item.moduleId)!;
30
+ const tree = tierTree(index, item.tier, mod.moduleId);
31
+
32
+ // Every story of the tier, in the tree's own order, for previous / next.
33
+ const leaves = (nodes: TreeNode[]): TreeNode[] => nodes.flatMap((n) => (n.children ? leaves(n.children) : [n]));
34
+ const modules = new Map(componentsOf(index).map((c) => [`mod:${c.moduleId}`, c]));
35
+ const sequence = leaves(tree).flatMap((leaf) => modules.get(leaf.id)?.stories ?? []);
36
+ const at = sequence.findIndex((s) => s.storyId === item.storyId);
37
+ const prev = at > 0 ? sequence[at - 1] : null;
38
+ const next = at >= 0 && at < sequence.length - 1 ? sequence[at + 1] : null;
39
+
40
+ const pins = await pinCounts();
41
+ const openPins = pins.openFor(mod.componentFile);
42
+ const liveStory = mod.stories.find((s) => s.live);
43
+ const section = isSectionLike(item);
44
+ const tierDef = labConfig.tiers.find((t) => t.id === item.tier);
45
+ const cullable = Boolean(tierDef?.cullable);
46
+ const blocked = mod.live
47
+ ? "live on the site — cannot be marked"
48
+ : mod.usedBy.length
49
+ ? `used by ${mod.usedBy.length} live component${mod.usedBy.length === 1 ? "" : "s"} — cannot be marked`
50
+ : null;
51
+
52
+ // Level 2's breadcrumb names the PANEL's place — Site › Sections — as the design does; where the
53
+ // component sits inside the tier is the tree's job, and which component it is, the navbar's.
54
+ const crumbs: Crumb[] = [{ label: "Site" }, { label: tierLabel(item.tier), href: hrefs.tier(item.tier) }];
55
+ const TAB_LIMIT = 4;
56
+ ---
57
+
58
+ <Shell
59
+ title={`${mod.moduleName} — ${item.storyName}`}
60
+ active={`tier:${item.tier}`}
61
+ panel={{
62
+ label: tierLabel(item.tier),
63
+ crumbs,
64
+ searchPlaceholder: `Search ${tierLabel(item.tier).toLowerCase()}`,
65
+ filterLabel: "Show components",
66
+ filters: [
67
+ { value: "live", label: "Live on the site" },
68
+ { value: "used", label: "Used by something live" },
69
+ { value: "unused", label: "Unused" },
70
+ ],
71
+ }}
72
+ >
73
+ <Tree slot="panel" nodes={tree} />
74
+
75
+ <Fragment slot="navbar">
76
+ <div class="lab-navbar__id">
77
+ <h1 class="lab-navbar__title">
78
+ {mod.moduleName}
79
+ {mod.version && <small>{mod.version}</small>}
80
+ </h1>
81
+ <button class="lab-status-btn" type="button" popovertarget="lab-status-menu" title="Status and marks">
82
+ {liveStory && <span class="lab-pill lab-pill--live" title={liveTitle(liveStory)}>{livePillText(liveStory)}</span>}
83
+ {!mod.live && mod.usedBy.length > 0 && <span class="lab-pill lab-pill--used">used by {mod.usedBy.length}</span>}
84
+ {!mod.live && !mod.usedBy.length && <span class="lab-pill">unused</span>}
85
+ {mod.responsive && (
86
+ <span class:list={["lab-pill", mod.responsive.done ? "lab-pill--responsive" : "lab-pill--unresponsive"]} data-lab-resp-pill>
87
+ {mod.responsive.done ? "responsive" : "not responsive"}
88
+ </span>
89
+ )}
90
+ <span class="lab-pill lab-pill--marked" data-lab-cull-pill hidden>marked for deletion</span>
91
+ {openPins > 0 && <span class="lab-pill lab-pill--brand">{openPins} open pin{openPins === 1 ? "" : "s"}</span>}
92
+ <Icon name="chevron-down" size="sm" />
93
+ </button>
94
+ </div>
95
+
96
+ <nav class="lab-tabs" aria-label="Stories">
97
+ {
98
+ mod.stories.length <= TAB_LIMIT ? (
99
+ mod.stories.map((s) => (
100
+ <a
101
+ class="lab-tab"
102
+ href={s.dashboardUrl}
103
+ aria-current={s.storyId === item.storyId ? "page" : undefined}
104
+ data-lab-keep-viewport
105
+ >
106
+ {s.storyName}
107
+ </a>
108
+ ))
109
+ ) : (
110
+ <>
111
+ <button class="lab-btn lab-btn--sm" type="button" popovertarget="lab-story-menu">
112
+ {item.storyName}
113
+ <span class="lab-tabs__count">{mod.stories.indexOf(item) + 1} / {mod.stories.length}</span>
114
+ <Icon name="chevron-down" />
115
+ </button>
116
+ <div class="lab-menu" id="lab-story-menu" popover role="menu">
117
+ <p class="lab-menu__label">{mod.stories.length} stories</p>
118
+ {mod.stories.map((s) => (
119
+ <a
120
+ class="lab-menu__item"
121
+ role="menuitemradio"
122
+ aria-checked={String(s.storyId === item.storyId)}
123
+ href={s.dashboardUrl}
124
+ data-lab-keep-viewport
125
+ >
126
+ {s.storyName}
127
+ <Icon name="check" size="sm" class="lab-menu__tick" />
128
+ </a>
129
+ ))}
130
+ </div>
131
+ </>
132
+ )
133
+ }
134
+ </nav>
135
+
136
+ <div class="lab-navbar__tools">
137
+ <div class="lab-seg" role="group" aria-label="Previous and next story">
138
+ {
139
+ prev ? (
140
+ <a class="lab-btn lab-btn--sm lab-btn--icon" href={prev.dashboardUrl} title={`Previous: ${prev.moduleName} — ${prev.storyName} (k)`} data-lab-step="prev" data-lab-keep-viewport>
141
+ <Icon name="chevron-left" />
142
+ <span class="lab-visually-hidden">Previous story</span>
143
+ </a>
144
+ ) : (
145
+ <span class="lab-btn lab-btn--sm lab-btn--icon" aria-disabled="true" data-lab-step><Icon name="chevron-left" /></span>
146
+ )
147
+ }
148
+ {
149
+ next ? (
150
+ <a class="lab-btn lab-btn--sm lab-btn--icon" href={next.dashboardUrl} title={`Next: ${next.moduleName} — ${next.storyName} (j)`} data-lab-step="next" data-lab-keep-viewport>
151
+ <Icon name="chevron-right" />
152
+ <span class="lab-visually-hidden">Next story</span>
153
+ </a>
154
+ ) : (
155
+ <span class="lab-btn lab-btn--sm lab-btn--icon" aria-disabled="true" data-lab-step><Icon name="chevron-right" /></span>
156
+ )
157
+ }
158
+ </div>
159
+ </div>
160
+
161
+ <ViewportControls defaultW={section ? 1440 : 960} defaultH={section ? 900 : 540} kind={section ? "section" : "component"} />
162
+
163
+ <div class="lab-navbar__tools">
164
+ <button class="lab-btn lab-btn--sm lab-btn--icon" type="button" popovertarget="lab-actions-menu" title="More">
165
+ <Icon name="more" />
166
+ <span class="lab-visually-hidden">More actions</span>
167
+ </button>
168
+ </div>
169
+
170
+ <div class="lab-menu" id="lab-actions-menu" popover role="menu" data-lab-align="end">
171
+ {
172
+ item.editorUrl && (
173
+ <a class="lab-menu__item" role="menuitem" href={item.editorUrl}>
174
+ <Icon name="code" />
175
+ Open in VS Code
176
+ </a>
177
+ )
178
+ }
179
+ <a class="lab-menu__item" role="menuitem" href={item.storyUrl} target="_blank" rel="noopener">
180
+ <Icon name="expand" />
181
+ Open full screen
182
+ </a>
183
+ {
184
+ item.stagingUrl && (
185
+ <a class="lab-menu__item" role="menuitem" href={item.stagingUrl} target="_blank" rel="noopener">
186
+ <Icon name="external" />
187
+ View on staging
188
+ </a>
189
+ )
190
+ }
191
+ <button class="lab-menu__item" type="button" role="menuitem" data-lab-copy-link>
192
+ <Icon name="link" />
193
+ <span data-lab-copy-label>Copy link to this size</span>
194
+ </button>
195
+ </div>
196
+
197
+ <div class="lab-menu lab-status-menu" id="lab-status-menu" popover role="menu">
198
+ <p class="lab-menu__label">On the site</p>
199
+ <p class="lab-status-menu__fact">
200
+ {
201
+ liveStory
202
+ ? liveStory.liveOn.map((m) => `${m.page} · slot ${m.slot} of ${m.of}`).join(", ")
203
+ : mod.usedBy.length
204
+ ? `Used by ${mod.usedBy.map((f) => f.split("/").pop()).join(", ")}`
205
+ : "Not mounted by any page, and nothing live uses it."
206
+ }
207
+ </p>
208
+ {
209
+ mod.componentFile && (
210
+ <p class="lab-status-menu__fact lab-mono">{mod.componentFile}</p>
211
+ )
212
+ }
213
+ {
214
+ mod.responsive && (
215
+ <>
216
+ <hr />
217
+ <p class="lab-menu__label">Responsive</p>
218
+ <label class="lab-menu__item lab-menu__check">
219
+ <input type="checkbox" data-lab-mark="approved" data-file={mod.moduleFile} checked={mod.responsive.approved} disabled />
220
+ Approved for responsive work
221
+ </label>
222
+ <label class="lab-menu__item lab-menu__check">
223
+ <input type="checkbox" data-lab-mark="done" data-file={mod.moduleFile} checked={mod.responsive.done} disabled />
224
+ Made responsive
225
+ </label>
226
+ </>
227
+ )
228
+ }
229
+ {
230
+ cullable && (
231
+ <>
232
+ <hr />
233
+ <p class="lab-menu__label">Clean-up</p>
234
+ <label class="lab-menu__item lab-menu__check" title={blocked ?? undefined}>
235
+ <input type="checkbox" data-lab-mark="cull" data-file={mod.moduleFile} data-blocked={blocked ?? undefined} disabled />
236
+ Mark for deletion
237
+ </label>
238
+ {blocked && <p class="lab-status-menu__fact">{blocked}</p>}
239
+ </>
240
+ )
241
+ }
242
+ <p class="lab-status-menu__note" data-lab-marks-note>Marks can be changed on the dev server only.</p>
243
+ </div>
244
+ </Fragment>
245
+
246
+ <ViewportStage src={item.storyUrl} title={`${mod.moduleName} — ${item.storyName}`} />
247
+ </Shell>
248
+
249
+ <script>
250
+ import "./marks-client";
251
+ </script>
@@ -0,0 +1,83 @@
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
+ import Icon from "./Icon.astro";
7
+ import type { TreeNode } from "./model";
8
+
9
+ interface Props {
10
+ nodes: TreeNode[];
11
+ depth?: number;
12
+ /** Set on the outermost call only. */
13
+ root?: boolean;
14
+ }
15
+
16
+ const { nodes, depth = 0, root = true } = Astro.props;
17
+
18
+ /** Whether a node is, or holds, the current page — those groups render open whatever was stored. */
19
+ const holdsCurrent = (node: TreeNode): boolean =>
20
+ Boolean(node.current) || (node.children ?? []).some(holdsCurrent);
21
+
22
+ const searchText = (node: TreeNode) => `${node.label} ${node.search ?? ""}`.toLowerCase();
23
+ ---
24
+
25
+ <ul class={root ? "lab-tree" : "lab-tree__group"} role={root ? "tree" : "group"} data-lab-tree={root ? "" : undefined}>
26
+ {
27
+ nodes.map((node) => {
28
+ const hasChildren = (node.children?.length ?? 0) > 0;
29
+ const icon = node.icon ?? (hasChildren ? "folder" : "page");
30
+ const trailing = (
31
+ <>
32
+ {node.count !== undefined && <span class="lab-tree__count">{node.count}</span>}
33
+ {node.dot && <span class={`lab-dot lab-dot--${node.dot}`} title={node.dotTitle} />}
34
+ </>
35
+ );
36
+ return (
37
+ <li
38
+ class="lab-tree__node"
39
+ role="none"
40
+ style={`--lab-d:${depth}`}
41
+ data-lab-search={searchText(node)}
42
+ data-lab-facets={node.facets}
43
+ >
44
+ {hasChildren ? (
45
+ <details open data-lab-tree-id={node.id} data-lab-holds-current={holdsCurrent(node) ? "" : undefined}>
46
+ <summary
47
+ class="lab-tree__row"
48
+ role="treeitem"
49
+ aria-current={node.current ? "page" : undefined}
50
+ title={node.title}
51
+ >
52
+ <Icon name={icon} size="sm" />
53
+ <span class="lab-tree__label">{node.href ? <a href={node.href}>{node.label}</a> : node.label}</span>
54
+ {trailing}
55
+ <Icon name="chevron-down" size="sm" class="lab-tree__chevron" />
56
+ </summary>
57
+ <Astro.self nodes={node.children!} depth={depth + 1} root={false} />
58
+ </details>
59
+ ) : node.href && !node.disabled ? (
60
+ <a
61
+ class="lab-tree__row"
62
+ role="treeitem"
63
+ href={node.href}
64
+ aria-current={node.current ? "page" : undefined}
65
+ title={node.title}
66
+ data-lab-leaf
67
+ >
68
+ <Icon name={icon} size="sm" />
69
+ <span class="lab-tree__label">{node.label}</span>
70
+ {trailing}
71
+ </a>
72
+ ) : (
73
+ <span class="lab-tree__row" role="treeitem" aria-disabled="true" title={node.title}>
74
+ <Icon name={icon} size="sm" />
75
+ <span class="lab-tree__label">{node.label}</span>
76
+ {trailing}
77
+ </span>
78
+ )}
79
+ </li>
80
+ );
81
+ })
82
+ }
83
+ </ul>
@@ -0,0 +1,98 @@
1
+ ---
2
+ // The viewport's controls, for the navbar: the device switch (the site's design widths), the
3
+ // W × H field with rotate, the presets menu (both sides of every breakpoint, and named devices), the
4
+ // band readout, zoom (Fit by default), and — only when the frame supports them — the preview's own
5
+ // light/dark switch and the parameters drawer's button. Decided 2026-09-24; the sizes are the
6
+ // `viewports` option (../../defaults.mjs). Behaviour: ./viewport-client.ts.
7
+ import Icon from "./Icon.astro";
8
+ import type { IconName } from "./icons";
9
+ import { labConfig } from "./model";
10
+
11
+ interface Props {
12
+ /** The frame's size when neither the URL nor the last visit names one. */
13
+ defaultW: number;
14
+ defaultH: number;
15
+ /** Which remembered size to use: sections, components and pages each keep their own. */
16
+ kind: string;
17
+ }
18
+
19
+ const { defaultW, defaultH, kind } = Astro.props;
20
+ const { design, breakpoints, devices } = labConfig.viewports;
21
+ const edges = breakpoints.filter((b) => b.min > 0).flatMap((b) => [b.min, b.min - 1]);
22
+ const zooms = [25, 50, 75, 100, 150, 200];
23
+ ---
24
+
25
+ <div
26
+ class="lab-navbar__tools"
27
+ data-lab-vp-controls
28
+ data-kind={kind}
29
+ data-default-w={defaultW}
30
+ data-default-h={defaultH}
31
+ data-breakpoints={JSON.stringify(breakpoints)}
32
+ data-step-widths={[...new Set([...design.map((d) => d.width), ...edges])].sort((a, b) => a - b).join(",")}
33
+ >
34
+ <div class="lab-seg" role="group" aria-label="Design widths">
35
+ {
36
+ design.map((d) => (
37
+ <button class="lab-btn lab-btn--sm lab-btn--icon" type="button" data-lab-vp-width={d.width} title={`${d.label} · ${d.width}px`}>
38
+ <Icon name={d.icon as IconName} />
39
+ <span class="lab-visually-hidden">{d.label}, {d.width} pixels wide</span>
40
+ </button>
41
+ ))
42
+ }
43
+ </div>
44
+
45
+ <div class="lab-vp-size" role="group" aria-label="Frame size">
46
+ <input type="number" inputmode="numeric" min="240" step="1" aria-label="Width" data-lab-vp-w />
47
+ <span aria-hidden="true">×</span>
48
+ <input type="number" inputmode="numeric" min="240" step="1" aria-label="Height" data-lab-vp-h />
49
+ <button class="lab-icon-btn" type="button" title="Rotate — swap width and height (r)" data-lab-vp-rotate>
50
+ <Icon name="rotate" />
51
+ <span class="lab-visually-hidden">Rotate</span>
52
+ </button>
53
+ </div>
54
+
55
+ <button class="lab-btn lab-btn--sm" type="button" popovertarget="lab-vp-presets" title="Breakpoints and devices ([ and ] step)">
56
+ Presets
57
+ <Icon name="chevron-down" />
58
+ </button>
59
+ <div class="lab-menu" id="lab-vp-presets" popover role="menu">
60
+ <p class="lab-menu__label">Breakpoint edges — width only</p>
61
+ {
62
+ edges.map((w) => (
63
+ <button class="lab-menu__item" type="button" role="menuitem" data-lab-vp-width={w}>
64
+ {w}px
65
+ <span class="lab-menu__hint">{breakpoints.find((b) => b.min <= w)?.name}</span>
66
+ </button>
67
+ ))
68
+ }
69
+ <hr />
70
+ <p class="lab-menu__label">Devices</p>
71
+ {
72
+ devices.map((d) => (
73
+ <button class="lab-menu__item" type="button" role="menuitem" data-lab-vp-width={d.width} data-lab-vp-height={d.height}>
74
+ {d.label}
75
+ <span class="lab-menu__hint">{d.width} × {d.height}</span>
76
+ </button>
77
+ ))
78
+ }
79
+ </div>
80
+
81
+ <span class="lab-pill lab-vp-band" data-lab-vp-band aria-live="polite">—</span>
82
+
83
+ <select class="lab-select lab-select--sm" aria-label="Zoom" title="Zoom (f fits, 0 is 100%)" data-lab-vp-zoom>
84
+ <option value="fit">Fit</option>
85
+ {zooms.map((z) => <option value={String(z / 100)}>{z}%</option>)}
86
+ </select>
87
+
88
+ <button class="lab-btn lab-btn--sm lab-btn--icon lab-preview-theme-btn" type="button" hidden title="The preview's light or dark" data-lab-preview-theme>
89
+ <Icon name="sun" class="lab-icon--sun" />
90
+ <Icon name="moon" class="lab-icon--moon" />
91
+ <span class="lab-visually-hidden">Switch the preview between light and dark</span>
92
+ </button>
93
+
94
+ <button class="lab-btn lab-btn--sm" type="button" hidden aria-pressed="false" title="Tuning parameters" data-lab-params-toggle>
95
+ <Icon name="params" />
96
+ Parameters
97
+ </button>
98
+ </div>
@@ -0,0 +1,32 @@
1
+ ---
2
+ // The canvas: the story or page in an <iframe> whose LOGICAL size is set by the controls or by
3
+ // dragging its edges. Media queries and vh units inside the frame follow the frame's own size,
4
+ // which is why it is an iframe and never an inline render. Zoom is a transform on the wrapper, so
5
+ // zooming never changes the frame's width — or its breakpoints. The parameters drawer sits on the
6
+ // canvas's right edge, over it, and exists only for a story that registered parameters.
7
+ interface Props {
8
+ src: string;
9
+ title: string;
10
+ }
11
+
12
+ const { src, title } = Astro.props;
13
+ ---
14
+
15
+ <div class="lab-vp" data-lab-vp>
16
+ <div class="lab-vp-stage" data-lab-vp-stage>
17
+ <div class="lab-vp-box" data-lab-vp-box>
18
+ <div class="lab-vp-wrapper" data-lab-vp-wrapper>
19
+ <iframe src={src} title={title} data-lab-vp-frame></iframe>
20
+ </div>
21
+ <div class="lab-vp-handle lab-vp-handle--e" data-axis="x" title="Drag to resize the width" aria-hidden="true"></div>
22
+ <div class="lab-vp-handle lab-vp-handle--s" data-axis="y" title="Drag to resize the height" aria-hidden="true"></div>
23
+ <div class="lab-vp-handle lab-vp-handle--se" data-axis="xy" title="Drag to resize" aria-hidden="true"></div>
24
+ <span class="lab-vp-live" data-lab-vp-live aria-hidden="true"></span>
25
+ </div>
26
+ </div>
27
+ <aside class="lab-params" data-lab-params hidden aria-label="Parameters"></aside>
28
+ </div>
29
+
30
+ <script>
31
+ import "./viewport-client";
32
+ </script>
@@ -0,0 +1,93 @@
1
+ Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,59 @@
1
+ // The chrome's icon set: Lucide 1.47.0 (lucide-static), ISC — a few shapes Lucide inherited from
2
+ // Feather are MIT. Both notices are in ./LICENSE-icons, which must travel with this file.
3
+ //
4
+ // WHY LUCIDE AND NOT THE DESIGN'S OWN ICONS (2026-09-24). The Figma file draws Untitled UI icons.
5
+ // Their npm package declares "MIT" in package.json, but the licence file it ships forbids
6
+ // distributing the icons in original or modified form — and this package is published to npm, so
7
+ // bundling them would be distributing them. Lucide draws the same 24px, 2px-stroke, round-cap
8
+ // family, so each design icon maps to its nearest Lucide shape by name below; the right-hand
9
+ // comment is the Lucide file each one came from.
10
+ //
11
+ // Every icon is `fill="none" stroke="currentColor"` (set once on the <symbol> in ./Sprite.astro), so
12
+ // it takes the colour of the text around it and themes with the chrome for free.
13
+ export const ICONS = {
14
+ "home": "<path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\" /><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />", // house
15
+ "site": "<path d=\"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z\" /><path d=\"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12\" /><path d=\"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17\" />", // layers
16
+ "sections": "<path d=\"M13 13.74a2 2 0 0 1-2 0L2.5 8.87a1 1 0 0 1 0-1.74L11 2.26a2 2 0 0 1 2 0l8.5 4.87a1 1 0 0 1 0 1.74z\" /><path d=\"m20 14.285 1.5.845a1 1 0 0 1 0 1.74L13 21.74a2 2 0 0 1-2 0l-8.5-4.87a1 1 0 0 1 0-1.74l1.5-.845\" />", // layers-2
17
+ "pages": "<path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\" /><path d=\"M14 2v5a1 1 0 0 0 1 1h5\" />", // file
18
+ "page": "<path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\" /><path d=\"M14 2v5a1 1 0 0 0 1 1h5\" />", // file
19
+ "components": "<rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" /><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />", // copy
20
+ "assets": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\" /><circle cx=\"9\" cy=\"9\" r=\"2\" /><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\" />", // image
21
+ "tasks": "<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\" ry=\"1\" /><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\" /><path d=\"m9 14 2 2 4-4\" />", // clipboard-check
22
+ "board": "<path d=\"m12.296 3.464 3.02 3.956\" /><path d=\"M20.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3z\" /><path d=\"M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" /><path d=\"m6.18 5.276 3.1 3.899\" />", // clapperboard
23
+ "all-tasks": "<path d=\"m12 15 2 2 4-4\" /><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" /><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />", // copy-check
24
+ "support": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"m4.93 4.93 4.24 4.24\" /><path d=\"m14.83 9.17 4.24-4.24\" /><path d=\"m14.83 14.83 4.24 4.24\" /><path d=\"m9.17 14.83-4.24 4.24\" /><circle cx=\"12\" cy=\"12\" r=\"4\" />", // life-buoy
25
+ "settings": "<path d=\"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\" /><circle cx=\"12\" cy=\"12\" r=\"3\" />", // settings
26
+ "chevron-down": "<path d=\"m6 9 6 6 6-6\" />", // chevron-down
27
+ "chevron-up": "<path d=\"m18 15-6-6-6 6\" />", // chevron-up
28
+ "chevron-right": "<path d=\"m9 18 6-6-6-6\" />", // chevron-right
29
+ "chevron-left": "<path d=\"m15 18-6-6 6-6\" />", // chevron-left
30
+ "chevrons-left": "<path d=\"m11 17-5-5 5-5\" /><path d=\"m18 17-5-5 5-5\" />", // chevrons-left
31
+ "chevrons-right": "<path d=\"m6 17 5-5-5-5\" /><path d=\"m13 17 5-5-5-5\" />", // chevrons-right
32
+ "search": "<path d=\"m21 21-4.34-4.34\" /><circle cx=\"11\" cy=\"11\" r=\"8\" />", // search
33
+ "filter": "<path d=\"M2 5h20\" /><path d=\"M6 12h12\" /><path d=\"M9 19h6\" />", // list-filter
34
+ "folder": "<path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\" />", // folder
35
+ "coffee": "<path d=\"M10 2v2\" /><path d=\"M14 2v2\" /><path d=\"M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1\" /><path d=\"M6 2v2\" />", // coffee
36
+ "panel-close": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" /><path d=\"M9 3v18\" /><path d=\"m16 15-3-3 3-3\" />", // panel-left-close
37
+ "panel-open": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" /><path d=\"M9 3v18\" /><path d=\"m14 9 3 3-3 3\" />", // panel-left-open
38
+ "sun": "<circle cx=\"12\" cy=\"12\" r=\"4\" /><path d=\"M12 2v2\" /><path d=\"M12 20v2\" /><path d=\"m4.93 4.93 1.41 1.41\" /><path d=\"m17.66 17.66 1.41 1.41\" /><path d=\"M2 12h2\" /><path d=\"M20 12h2\" /><path d=\"m6.34 17.66-1.41 1.41\" /><path d=\"m19.07 4.93-1.41 1.41\" />", // sun
39
+ "moon": "<path d=\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\" />", // moon
40
+ "code": "<path d=\"m18 16 4-4-4-4\" /><path d=\"m6 8-4 4 4 4\" /><path d=\"m14.5 4-5 16\" />", // code-xml
41
+ "link": "<path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\" /><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\" />", // link
42
+ "expand": "<path d=\"M15 3h6v6\" /><path d=\"m21 3-7 7\" /><path d=\"m3 21 7-7\" /><path d=\"M9 21H3v-6\" />", // maximize-2
43
+ "external": "<path d=\"M15 3h6v6\" /><path d=\"M10 14 21 3\" /><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />", // external-link
44
+ "reload": "<path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\" /><path d=\"M21 3v5h-5\" /><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\" /><path d=\"M8 16H3v5\" />", // refresh-cw
45
+ "rotate": "<path d=\"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8\" /><path d=\"M21 3v5h-5\" />", // rotate-cw
46
+ "monitor": "<rect width=\"20\" height=\"14\" x=\"2\" y=\"3\" rx=\"2\" /><line x1=\"8\" x2=\"16\" y1=\"21\" y2=\"21\" /><line x1=\"12\" x2=\"12\" y1=\"17\" y2=\"21\" />", // monitor
47
+ "tablet": "<rect width=\"16\" height=\"20\" x=\"4\" y=\"2\" rx=\"2\" ry=\"2\" /><line x1=\"12\" x2=\"12.01\" y1=\"18\" y2=\"18\" />", // tablet
48
+ "phone": "<rect width=\"14\" height=\"20\" x=\"5\" y=\"2\" rx=\"2\" ry=\"2\" /><path d=\"M12 18h.01\" />", // smartphone
49
+ "phone-landscape": "<g transform=\"rotate(90 12 12)\"><rect width=\"14\" height=\"20\" x=\"5\" y=\"2\" rx=\"2\" ry=\"2\" /><path d=\"M12 18h.01\" /></g>", // smartphone, turned a quarter
50
+ "params": "<path d=\"M10 5H3\" /><path d=\"M12 19H3\" /><path d=\"M14 3v4\" /><path d=\"M16 17v4\" /><path d=\"M21 12h-9\" /><path d=\"M21 19h-5\" /><path d=\"M21 5h-7\" /><path d=\"M8 10v4\" /><path d=\"M8 12H3\" />", // sliders-horizontal
51
+ "close": "<path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />", // x
52
+ "more": "<circle cx=\"12\" cy=\"12\" r=\"1\" /><circle cx=\"19\" cy=\"12\" r=\"1\" /><circle cx=\"5\" cy=\"12\" r=\"1\" />", // ellipsis
53
+ "component": "<path d=\"M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z\" /><path d=\"M2.297 11.293a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0z\" /><path d=\"M8.916 17.912a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0z\" /><path d=\"M8.916 4.674a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z\" />", // component
54
+ "story": "<path d=\"M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z\" />", // bookmark
55
+ "pin": "<path d=\"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z\" />", // message-square
56
+ "check": "<path d=\"M20 6 9 17l-5-5\" />", // check
57
+ } as const;
58
+
59
+ export type IconName = keyof typeof ICONS;