@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,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The navbar's tools half: the viewport group and the overlays. Figma `Astrolab` › Lab / Components ›
|
|
3
|
+
// Device switch, Viewport field, Menu › Type=Presets, Tool button.
|
|
4
|
+
//
|
|
5
|
+
// · the DEVICE SWITCH — the site's four design widths (the `viewports` option, ../../defaults.mjs);
|
|
6
|
+
// · the VIEWPORT FIELD — width × height edited in place, and a button naming the band and the zoom
|
|
7
|
+
// ("tablet Fit") that opens the Presets menu: rotate, both sides of every breakpoint, named
|
|
8
|
+
// devices, and the zoom row. Decided 2026-09-24 (ASTROL-19, option B): keep the four device
|
|
9
|
+
// icons and merge the rest into one field — so there is no separate rotate button, zoom select
|
|
10
|
+
// or band readout;
|
|
11
|
+
// · the OVERLAYS — Comment (click an element in the frame to pin it; only where the pin board
|
|
12
|
+
// runs) and Parameters (only for a variant that registers any; the drawer's own script shows it).
|
|
13
|
+
//
|
|
14
|
+
// This element is `[data-lab-vp-controls]`, which ./viewport-client.ts reads its settings from, so
|
|
15
|
+
// the Parameters button has to stay inside it. Every shortcut is unchanged: [ ] step the widths,
|
|
16
|
+
// r rotates, f fits, 0 is 100%.
|
|
17
|
+
import Icon from "./Icon.astro";
|
|
18
|
+
import type { IconName } from "./icons";
|
|
19
|
+
import { labConfig } from "./model";
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
/** The frame's size when neither the URL nor the last visit names one. */
|
|
23
|
+
defaultW: number;
|
|
24
|
+
defaultH: number;
|
|
25
|
+
/** Which remembered size to use: sections, components and pages each keep their own. */
|
|
26
|
+
kind: string;
|
|
27
|
+
/** Render the Comment button — only where the pin board runs. */
|
|
28
|
+
comment: boolean;
|
|
29
|
+
/** Render the Parameters button (hidden until the variant registers parameters). */
|
|
30
|
+
parameters: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { defaultW, defaultH, kind, comment, parameters } = Astro.props;
|
|
34
|
+
const { design, breakpoints, devices } = labConfig.viewports;
|
|
35
|
+
const edges = breakpoints.filter((b) => b.min > 0).flatMap((b) => [b.min, b.min - 1]);
|
|
36
|
+
const bandName = (w: number) => breakpoints.find((b) => w >= b.min)?.name ?? "";
|
|
37
|
+
const zooms = [25, 50, 75, 100, 150, 200];
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<div
|
|
41
|
+
class="lab-nb__tools"
|
|
42
|
+
data-lab-vp-controls
|
|
43
|
+
data-kind={kind}
|
|
44
|
+
data-default-w={defaultW}
|
|
45
|
+
data-default-h={defaultH}
|
|
46
|
+
data-breakpoints={JSON.stringify(breakpoints)}
|
|
47
|
+
data-step-widths={[...new Set([...design.map((d) => d.width), ...edges])].sort((a, b) => a - b).join(",")}
|
|
48
|
+
>
|
|
49
|
+
<div class="lab-nb__viewport">
|
|
50
|
+
<div class="lab-devsw" role="group" aria-label="Design widths">
|
|
51
|
+
{
|
|
52
|
+
design.map((d) => (
|
|
53
|
+
<button
|
|
54
|
+
class="lab-devsw__btn"
|
|
55
|
+
type="button"
|
|
56
|
+
aria-pressed="false"
|
|
57
|
+
data-lab-vp-width={d.width}
|
|
58
|
+
title={`${d.label} · ${d.width} ( [ / ] step )`}
|
|
59
|
+
>
|
|
60
|
+
<Icon name={d.icon as IconName} size="sm" />
|
|
61
|
+
<span class="lab-visually-hidden">{d.label}, {d.width} pixels wide</span>
|
|
62
|
+
</button>
|
|
63
|
+
))
|
|
64
|
+
}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="lab-vpf" role="group" aria-label="Frame size, band and zoom" data-lab-vpf>
|
|
68
|
+
<input class="lab-vpf__num lab-vpf__num--w" type="number" inputmode="numeric" min="240" step="1" aria-label="Width" data-lab-vp-w />
|
|
69
|
+
<span class="lab-vpf__x" aria-hidden="true">×</span>
|
|
70
|
+
<input class="lab-vpf__num" type="number" inputmode="numeric" min="240" step="1" aria-label="Height" data-lab-vp-h />
|
|
71
|
+
<button
|
|
72
|
+
class="lab-vpf__menu"
|
|
73
|
+
type="button"
|
|
74
|
+
popovertarget="lab-vp-presets"
|
|
75
|
+
title="Rotate, breakpoints, devices and zoom"
|
|
76
|
+
>
|
|
77
|
+
<span class="lab-vpf__band" data-lab-vp-band>—</span>
|
|
78
|
+
<span class="lab-vpf__zoom" data-lab-vp-zoom-label>Fit</span>
|
|
79
|
+
<Icon name="chevron-down" size="sm" />
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="lab-menu lab-menu--presets" id="lab-vp-presets" popover role="menu">
|
|
84
|
+
<button class="lab-menu__item" type="button" role="menuitem" data-lab-vp-rotate>
|
|
85
|
+
<span class="lab-menu__mark"><Icon name="rotate" size="sm" /></span>
|
|
86
|
+
<span class="lab-menu__text">Rotate</span>
|
|
87
|
+
<kbd class="lab-menu__hint">r</kbd>
|
|
88
|
+
</button>
|
|
89
|
+
<hr />
|
|
90
|
+
<p class="lab-menu__label">Breakpoint edges · width only</p>
|
|
91
|
+
{
|
|
92
|
+
edges.map((w) => (
|
|
93
|
+
<button class="lab-menu__item" type="button" role="menuitemradio" aria-checked="false" data-lab-vp-width={w}>
|
|
94
|
+
<span class="lab-menu__mark"><Icon name="check" size="sm" class="lab-menu__tick" /></span>
|
|
95
|
+
<span class="lab-menu__text">{w}</span>
|
|
96
|
+
<span class="lab-menu__hint">{bandName(w)}</span>
|
|
97
|
+
</button>
|
|
98
|
+
))
|
|
99
|
+
}
|
|
100
|
+
<hr />
|
|
101
|
+
<p class="lab-menu__label">Devices · width and height</p>
|
|
102
|
+
{
|
|
103
|
+
devices.map((d) => (
|
|
104
|
+
<button
|
|
105
|
+
class="lab-menu__item"
|
|
106
|
+
type="button"
|
|
107
|
+
role="menuitemradio"
|
|
108
|
+
aria-checked="false"
|
|
109
|
+
data-lab-vp-width={d.width}
|
|
110
|
+
data-lab-vp-height={d.height}
|
|
111
|
+
>
|
|
112
|
+
<span class="lab-menu__mark"><Icon name="check" size="sm" class="lab-menu__tick" /></span>
|
|
113
|
+
<span class="lab-menu__text">{d.label}</span>
|
|
114
|
+
<span class="lab-menu__hint">{d.width} × {d.height}</span>
|
|
115
|
+
</button>
|
|
116
|
+
))
|
|
117
|
+
}
|
|
118
|
+
<hr />
|
|
119
|
+
<p class="lab-menu__label" id="lab-vp-zoom-label">Zoom</p>
|
|
120
|
+
<div class="lab-zoom" role="group" aria-labelledby="lab-vp-zoom-label">
|
|
121
|
+
<button class="lab-zoom__btn" type="button" role="menuitemradio" aria-checked="false" data-lab-vp-zoom="fit" title="Fit (f)">Fit</button>
|
|
122
|
+
{
|
|
123
|
+
zooms.map((z) => (
|
|
124
|
+
<button
|
|
125
|
+
class="lab-zoom__btn"
|
|
126
|
+
type="button"
|
|
127
|
+
role="menuitemradio"
|
|
128
|
+
aria-checked="false"
|
|
129
|
+
data-lab-vp-zoom={String(z / 100)}
|
|
130
|
+
title={z === 100 ? "100% (0)" : `${z}%`}
|
|
131
|
+
>
|
|
132
|
+
{z}
|
|
133
|
+
</button>
|
|
134
|
+
))
|
|
135
|
+
}
|
|
136
|
+
</div>
|
|
137
|
+
<p class="lab-menu__note">[ ] step widths · r rotate · f fit · 0 is 100%</p>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="lab-nb__overlays">
|
|
142
|
+
{
|
|
143
|
+
comment && (
|
|
144
|
+
<button
|
|
145
|
+
class="lab-tool lab-tool--pins"
|
|
146
|
+
type="button"
|
|
147
|
+
aria-pressed="false"
|
|
148
|
+
disabled
|
|
149
|
+
title="Comment — click an element in the frame to pin it (Esc stops)"
|
|
150
|
+
data-lab-comment
|
|
151
|
+
>
|
|
152
|
+
<Icon name="comment" size="sm" />
|
|
153
|
+
<span class="lab-tool__label">Comment</span>
|
|
154
|
+
</button>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
{
|
|
158
|
+
parameters && (
|
|
159
|
+
<button
|
|
160
|
+
class="lab-tool lab-tool--icon"
|
|
161
|
+
type="button"
|
|
162
|
+
hidden
|
|
163
|
+
aria-pressed="false"
|
|
164
|
+
title="Parameters"
|
|
165
|
+
data-lab-params-toggle
|
|
166
|
+
>
|
|
167
|
+
<Icon name="parameters" size="sm" />
|
|
168
|
+
<span class="lab-visually-hidden">Parameters</span>
|
|
169
|
+
</button>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
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. It is mounted only where `parameters` is set — the component page,
|
|
7
|
+
// never the page view, which has no Parameters button to open or close it (decided 2026-09-24) —
|
|
8
|
+
// and shows only once the story registers parameters.
|
|
9
|
+
//
|
|
10
|
+
// Two overlays belong to the pin board and exist only where it runs (`astro dev`, board enabled):
|
|
11
|
+
// the PINS drawn on the framed page — one per ticket left on it, blue while open and grey once
|
|
12
|
+
// resolved, governed by ⋯ › Pins — and the HINT shown while Comment is picking. Both are chrome
|
|
13
|
+
// drawn over the frame, never inside it: a marker inside the frame would be photographed into
|
|
14
|
+
// the next ticket's screenshot. They carry `data-orbytes-pin-ui`, the attribute the pin picker
|
|
15
|
+
// skips, so the picker never pins the lab's own markers. Behaviour: ./navbar-client.ts.
|
|
16
|
+
interface Props {
|
|
17
|
+
src: string;
|
|
18
|
+
title: string;
|
|
19
|
+
/** Draw the pin overlays — only where the pin board runs. */
|
|
20
|
+
pins?: boolean;
|
|
21
|
+
/** Where a pin leads when clicked. */
|
|
22
|
+
tasksHref?: string | null;
|
|
23
|
+
/** Mount the parameters drawer — only where the navbar has the Parameters button to drive it. */
|
|
24
|
+
parameters?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { src, title, pins = false, tasksHref = null, parameters = false } = Astro.props;
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<div class="lab-vp" data-lab-vp>
|
|
31
|
+
<div class="lab-vp-stage" data-lab-vp-stage>
|
|
32
|
+
<div class="lab-vp-box" data-lab-vp-box>
|
|
33
|
+
<div class="lab-vp-wrapper" data-lab-vp-wrapper>
|
|
34
|
+
<iframe src={src} title={title} data-lab-vp-frame></iframe>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="lab-vp-handle lab-vp-handle--e" data-axis="x" title="Drag to resize the width" aria-hidden="true"></div>
|
|
37
|
+
<div class="lab-vp-handle lab-vp-handle--s" data-axis="y" title="Drag to resize the height" aria-hidden="true"></div>
|
|
38
|
+
<div class="lab-vp-handle lab-vp-handle--se" data-axis="xy" title="Drag to resize" aria-hidden="true"></div>
|
|
39
|
+
<span class="lab-vp-live" data-lab-vp-live aria-hidden="true"></span>
|
|
40
|
+
{pins && <div class="lab-vp-pins" data-lab-vp-pins data-orbytes-pin-ui="lab-pins" data-tasks-href={tasksHref ?? undefined}></div>}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
{
|
|
44
|
+
pins && (
|
|
45
|
+
<p class="lab-vp-hint" data-lab-pin-hint data-orbytes-pin-ui="lab-hint" hidden>
|
|
46
|
+
Click any element to pin it · Esc to stop
|
|
47
|
+
</p>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
{parameters && <aside class="lab-params" data-lab-params hidden aria-label="Parameters"></aside>}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<script>
|
|
54
|
+
import "./viewport-client";
|
|
55
|
+
</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.
|
|
Binary file
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
"properties": "<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\" /><circle cx=\"12\" cy=\"12\" r=\"3\" />", // eye
|
|
45
|
+
"comment": "<path d=\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719\" />", // message-circle
|
|
46
|
+
"marker": "<path d=\"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0\" /><circle cx=\"12\" cy=\"10\" r=\"3\" />", // map-pin
|
|
47
|
+
"staging": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20\" /><path d=\"M2 12h20\" />", // globe
|
|
48
|
+
"reload-page": "<path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" /><path d=\"M3 3v5h5\" />", // rotate-ccw
|
|
49
|
+
"parameters": "<path d=\"M10 8h4\" /><path d=\"M12 21v-9\" /><path d=\"M12 8V3\" /><path d=\"M17 16h4\" /><path d=\"M19 12V3\" /><path d=\"M19 21v-5\" /><path d=\"M3 14h4\" /><path d=\"M5 10V3\" /><path d=\"M5 21v-7\" />", // sliders-vertical
|
|
50
|
+
"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
|
|
51
|
+
"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
|
|
52
|
+
"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
|
|
53
|
+
"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
|
|
54
|
+
"phone": "<rect width=\"14\" height=\"20\" x=\"5\" y=\"2\" rx=\"2\" ry=\"2\" /><path d=\"M12 18h.01\" />", // smartphone
|
|
55
|
+
"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
|
|
56
|
+
"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
|
|
57
|
+
"reset": "<path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" /><path d=\"M3 3v5h5\" />", // rotate-ccw
|
|
58
|
+
"copy": "<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
|
|
59
|
+
"help": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\" /><path d=\"M12 17h.01\" />", // circle-help
|
|
60
|
+
"close": "<path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />", // x
|
|
61
|
+
"more": "<circle cx=\"12\" cy=\"12\" r=\"1\" /><circle cx=\"19\" cy=\"12\" r=\"1\" /><circle cx=\"5\" cy=\"12\" r=\"1\" />", // ellipsis
|
|
62
|
+
"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
|
|
63
|
+
"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
|
|
64
|
+
"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
|
|
65
|
+
"check": "<path d=\"M20 6 9 17l-5-5\" />", // check
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
export type IconName = keyof typeof ICONS;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The responsive marks in the Properties panel (./Properties.astro) — "approved for responsive
|
|
2
|
+
// work" and "made responsive" — on the dev server's responsive API (../../index.mjs). It is
|
|
3
|
+
// dev-only middleware: on a staging build the probe fails, the boxes stay disabled and show the
|
|
4
|
+
// state the page was built with, and the panel says why. The box always shows the server's
|
|
5
|
+
// answer, never the click's.
|
|
6
|
+
//
|
|
7
|
+
// GET /__lab/responsive → { done, approved } PUT { done, approved }
|
|
8
|
+
//
|
|
9
|
+
// There is no deletion mark any more: mark for deletion was removed from the lab as a feature
|
|
10
|
+
// (decided 2026-09-24).
|
|
11
|
+
const boxes = [...document.querySelectorAll<HTMLInputElement>("input[data-lab-mark]")];
|
|
12
|
+
const note = document.querySelector<HTMLElement>("[data-lab-marks-note]");
|
|
13
|
+
|
|
14
|
+
type Resp = { done: string[]; approved: string[] };
|
|
15
|
+
let resp: Resp | null = null;
|
|
16
|
+
|
|
17
|
+
const getJson = async (url: string) => {
|
|
18
|
+
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
19
|
+
if (!response.ok) throw new Error(String(response.status));
|
|
20
|
+
return response.json();
|
|
21
|
+
};
|
|
22
|
+
const putJson = async (url: string, body: unknown) => {
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
method: "PUT",
|
|
25
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
26
|
+
body: JSON.stringify(body),
|
|
27
|
+
});
|
|
28
|
+
const data = await response.json().catch(() => ({}));
|
|
29
|
+
if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
|
|
30
|
+
return data;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const paint = () => {
|
|
34
|
+
if (!resp) return;
|
|
35
|
+
for (const box of boxes) {
|
|
36
|
+
box.checked = (box.dataset.labMark === "done" ? resp.done : resp.approved).includes(box.dataset.file!);
|
|
37
|
+
box.disabled = false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const say = (text: string) => {
|
|
42
|
+
if (note) note.textContent = text;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (boxes.length) {
|
|
46
|
+
getJson("/__lab/responsive")
|
|
47
|
+
.then((d: Resp) => {
|
|
48
|
+
resp = { done: d.done ?? [], approved: d.approved ?? [] };
|
|
49
|
+
paint();
|
|
50
|
+
})
|
|
51
|
+
.catch(() => say("Marks can be changed on the dev server only."));
|
|
52
|
+
|
|
53
|
+
for (const box of boxes) {
|
|
54
|
+
box.addEventListener("change", async () => {
|
|
55
|
+
if (!resp) return;
|
|
56
|
+
const file = box.dataset.file!;
|
|
57
|
+
const want = box.checked;
|
|
58
|
+
box.checked = !want; // the server's answer decides
|
|
59
|
+
const set = new Set(box.dataset.labMark === "done" ? resp.done : resp.approved);
|
|
60
|
+
if (want) set.add(file);
|
|
61
|
+
else set.delete(file);
|
|
62
|
+
const body = box.dataset.labMark === "done" ? { done: [...set], approved: resp.approved } : { done: resp.done, approved: [...set] };
|
|
63
|
+
try {
|
|
64
|
+
const data = await putJson("/__lab/responsive", body);
|
|
65
|
+
resp = { done: data.done ?? body.done, approved: data.approved ?? body.approved };
|
|
66
|
+
say("Saved to the repo.");
|
|
67
|
+
} catch (error) {
|
|
68
|
+
say(`Not saved: ${(error as Error).message}`);
|
|
69
|
+
}
|
|
70
|
+
paint();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// The chrome's data shapes, and the one place that decides what level 1 lists.
|
|
2
|
+
//
|
|
3
|
+
// Server-side only: everything here runs in the .astro frontmatter of a chrome page (dev: per
|
|
4
|
+
// request; staging: once, at build). Nothing is sent to the browser but the HTML it renders.
|
|
5
|
+
/// <reference path="../../virtual.d.ts" />
|
|
6
|
+
import labConfig from "virtual:orbytes-lab/config.mjs";
|
|
7
|
+
import type { IconName } from "./icons";
|
|
8
|
+
import { labBase } from "../shell/lab-index";
|
|
9
|
+
import storyModules from "virtual:astrobook/story-modules.mjs";
|
|
10
|
+
|
|
11
|
+
/** The tier that holds page sections — its level 2 is the Sections tree. Null when none is set. */
|
|
12
|
+
export const sectionsTier: string | null = labConfig.sectionsTier ?? null;
|
|
13
|
+
|
|
14
|
+
/** Tiers that hold at least one stories file — an empty tier is not worth a row in level 1. */
|
|
15
|
+
const tiersInUse = new Set(storyModules.map((mod) => mod.directory.split("/")[0] ?? ""));
|
|
16
|
+
|
|
17
|
+
/** One row of a level 2 tree. A node with `children` is a group; one with only `href` is a leaf. */
|
|
18
|
+
export interface TreeNode {
|
|
19
|
+
/** Stable across requests — the key its open/closed state is remembered under. */
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
href?: string;
|
|
23
|
+
icon?: IconName;
|
|
24
|
+
children?: TreeNode[];
|
|
25
|
+
/** This row is the page being shown. Its ancestors are forced open. */
|
|
26
|
+
current?: boolean;
|
|
27
|
+
/** The one-glyph status: live on the site, used by something live, an open task, or muted. */
|
|
28
|
+
dot?: "live" | "used" | "open" | "muted";
|
|
29
|
+
dotTitle?: string;
|
|
30
|
+
count?: number | string;
|
|
31
|
+
/** Extra words the panel search matches, beyond the label. */
|
|
32
|
+
search?: string;
|
|
33
|
+
/** Values the panel filter menu matches against, space-separated (e.g. "live section"). */
|
|
34
|
+
facets?: string;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
title?: string;
|
|
37
|
+
/**
|
|
38
|
+
* The Sections tree's slot badge (./SectionsTree.astro): where the row is mounted, or null for
|
|
39
|
+
* "on no page". A root row carrying this key makes the whole tree a Sections tree.
|
|
40
|
+
*/
|
|
41
|
+
slot?: { n: number; of: number; page: string } | null;
|
|
42
|
+
/** Which half of the Sections tree a root row sits in: mounted first, the rest under a heading. */
|
|
43
|
+
group?: "slotted" | "unslotted";
|
|
44
|
+
/** The row's place in each sort order, so a remembered "Name" sort applies before paint. */
|
|
45
|
+
order?: { slot: number; name: number };
|
|
46
|
+
/** Draw the children only while this row holds the current page (a section's versions). */
|
|
47
|
+
childrenWhenCurrent?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Crumb {
|
|
51
|
+
label: string;
|
|
52
|
+
href?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One choice in the level 2 filter menu. `value` is matched against a node's `facets`. */
|
|
56
|
+
export interface FilterOption {
|
|
57
|
+
value: string;
|
|
58
|
+
label: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Which level 1 item a page belongs to. Tiers are the consumer's (`tiers` option), so they are
|
|
63
|
+
* keyed by id rather than listed here.
|
|
64
|
+
*/
|
|
65
|
+
export type NavKey =
|
|
66
|
+
| "home"
|
|
67
|
+
| "pages"
|
|
68
|
+
| `tier:${string}`
|
|
69
|
+
| "assets"
|
|
70
|
+
| "tasks-board"
|
|
71
|
+
| "tasks-all"
|
|
72
|
+
| "support"
|
|
73
|
+
| "settings";
|
|
74
|
+
|
|
75
|
+
export interface NavItem {
|
|
76
|
+
key: NavKey;
|
|
77
|
+
label: string;
|
|
78
|
+
href: string;
|
|
79
|
+
icon: IconName;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface NavGroup {
|
|
83
|
+
id: string;
|
|
84
|
+
label: string;
|
|
85
|
+
icon: IconName;
|
|
86
|
+
items: NavItem[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The tier's icon: the two conventional tiers get the design's own; anything else a component. */
|
|
90
|
+
const tierIcon = (id: string): IconName =>
|
|
91
|
+
id === labConfig.sectionsTier || id === "sections" ? "sections" : id === "components" ? "components" : "component";
|
|
92
|
+
|
|
93
|
+
/** Where the task views live, or null when the pin board is not running (a build, or `pin: false`). */
|
|
94
|
+
export const tasksBase: string | null = labConfig.tasks ? labConfig.tasks.base : null;
|
|
95
|
+
|
|
96
|
+
export const hrefs = {
|
|
97
|
+
home: labBase || "/",
|
|
98
|
+
pages: `${labBase}/pages`,
|
|
99
|
+
page: (route: string) => `${labBase}/pages/${route === "/" ? "index" : route.replace(/^\/+/, "")}`,
|
|
100
|
+
tier: (id: string) => `${labBase}/browse/${id}`,
|
|
101
|
+
folder: (path: string) => `${labBase}/browse/${path}`,
|
|
102
|
+
assets: `${labBase}/assets`,
|
|
103
|
+
support: `${labBase}/support`,
|
|
104
|
+
settings: `${labBase}/settings`,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const navModel = (): { home: NavItem; groups: NavGroup[]; footer: NavItem[] } => {
|
|
108
|
+
const site: NavGroup = {
|
|
109
|
+
id: "site",
|
|
110
|
+
label: "Site",
|
|
111
|
+
icon: "site",
|
|
112
|
+
items: [
|
|
113
|
+
{ key: "pages", label: "Pages", href: hrefs.pages, icon: "pages" },
|
|
114
|
+
...labConfig.tiers.filter((tier) => tiersInUse.has(tier.id)).map((tier) => ({
|
|
115
|
+
key: `tier:${tier.id}` as NavKey,
|
|
116
|
+
label: tier.label,
|
|
117
|
+
href: hrefs.tier(tier.id),
|
|
118
|
+
icon: tierIcon(tier.id),
|
|
119
|
+
})),
|
|
120
|
+
{ key: "assets", label: "Assets", href: hrefs.assets, icon: "assets" },
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
const groups = [site];
|
|
124
|
+
// Tasks are the pin board, which is dev only — so the group exists only where the board does.
|
|
125
|
+
if (tasksBase) {
|
|
126
|
+
groups.push({
|
|
127
|
+
id: "tasks",
|
|
128
|
+
label: "Tasks",
|
|
129
|
+
icon: "tasks",
|
|
130
|
+
items: [
|
|
131
|
+
{ key: "tasks-board", label: "Kanban Board", href: tasksBase, icon: "board" },
|
|
132
|
+
{ key: "tasks-all", label: "All Tasks", href: `${tasksBase}/all`, icon: "all-tasks" },
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
home: { key: "home", label: "Home", href: hrefs.home, icon: "home" },
|
|
138
|
+
groups,
|
|
139
|
+
footer: [
|
|
140
|
+
{ key: "support", label: "Support", href: hrefs.support, icon: "support" },
|
|
141
|
+
{ key: "settings", label: "Settings", href: hrefs.settings, icon: "settings" },
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** The group a nav key sits in, for the breadcrumb ("Site › Pages"). */
|
|
147
|
+
export const crumbsFor = (key: NavKey, extra: Crumb[] = []): Crumb[] => {
|
|
148
|
+
const model = navModel();
|
|
149
|
+
for (const group of model.groups) {
|
|
150
|
+
const item = group.items.find((i) => i.key === key);
|
|
151
|
+
if (item) return [{ label: group.label }, { label: item.label, href: item.href }, ...extra];
|
|
152
|
+
}
|
|
153
|
+
return extra;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export { labBase, labConfig };
|