@orbytes/astrolab 0.4.0-next.1 → 0.4.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -84
- package/bin/pin-gallery.mjs +53 -19
- package/defaults.mjs +7 -20
- package/docs/PIN-CONTRACT.md +76 -10
- package/docs/PIN.md +93 -23
- package/index.d.ts +1 -7
- package/index.mjs +14 -81
- package/package.json +2 -2
- package/src/Home.astro +7 -8
- package/src/LabHead.astro +1 -1
- package/src/chrome/ActionsMenu.astro +97 -0
- package/src/chrome/ComponentCard.astro +9 -2
- package/src/chrome/Nav.astro +36 -10
- package/src/chrome/Panel.astro +17 -4
- package/src/chrome/Properties.astro +104 -0
- package/src/chrome/SectionsTree.astro +128 -0
- package/src/chrome/Shell.astro +20 -6
- package/src/chrome/StoryView.astro +103 -162
- package/src/chrome/Tree.astro +56 -53
- package/src/chrome/ViewportControls.astro +136 -61
- package/src/chrome/ViewportStage.astro +26 -3
- package/src/chrome/icons.ts +9 -0
- package/src/chrome/marks-client.ts +26 -53
- package/src/chrome/model.ts +14 -0
- package/src/chrome/navbar-client.ts +324 -0
- package/src/chrome/params-client.ts +434 -0
- package/src/chrome/pins-data.ts +42 -9
- package/src/chrome/shell-client.ts +99 -3
- package/src/chrome/trees.ts +112 -7
- package/src/chrome/viewport-client.ts +68 -242
- package/src/chrome/views/Assets.astro +21 -6
- package/src/chrome/views/Pages.astro +90 -54
- package/src/chrome/views/Placeholder.astro +3 -3
- package/src/chrome/views/Tasks.astro +12 -40
- package/src/core/LICENSE-astrobook +5 -0
- package/src/core/utils/kebab-case.ts +2 -2
- package/src/pin/board.mjs +25 -15
- package/src/pin/index.mjs +34 -20
- package/src/pin/tickets.mjs +6 -5
- package/src/pin/toolbar.js +81 -3
- package/src/shell/Browse.astro +35 -10
- package/src/shell/lab-index.ts +5 -4
- package/src/shell/lab-params.ts +113 -6
- package/src/shell/live-files.mjs +212 -10
- package/src/shell/marks.mjs +17 -41
- package/src/ui/components/preview-layout.astro +17 -0
- package/src/ui/components/theme-script.astro +4 -3
- package/src/ui/lab.css +2167 -566
- package/virtual.d.ts +0 -4
- package/bin/lab-cull.mjs +0 -401
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
// The
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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%.
|
|
7
17
|
import Icon from "./Icon.astro";
|
|
8
18
|
import type { IconName } from "./icons";
|
|
9
19
|
import { labConfig } from "./model";
|
|
@@ -14,16 +24,21 @@ interface Props {
|
|
|
14
24
|
defaultH: number;
|
|
15
25
|
/** Which remembered size to use: sections, components and pages each keep their own. */
|
|
16
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;
|
|
17
31
|
}
|
|
18
32
|
|
|
19
|
-
const { defaultW, defaultH, kind } = Astro.props;
|
|
33
|
+
const { defaultW, defaultH, kind, comment, parameters } = Astro.props;
|
|
20
34
|
const { design, breakpoints, devices } = labConfig.viewports;
|
|
21
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 ?? "";
|
|
22
37
|
const zooms = [25, 50, 75, 100, 150, 200];
|
|
23
38
|
---
|
|
24
39
|
|
|
25
40
|
<div
|
|
26
|
-
class="lab-
|
|
41
|
+
class="lab-nb__tools"
|
|
27
42
|
data-lab-vp-controls
|
|
28
43
|
data-kind={kind}
|
|
29
44
|
data-default-w={defaultW}
|
|
@@ -31,68 +46,128 @@ const zooms = [25, 50, 75, 100, 150, 200];
|
|
|
31
46
|
data-breakpoints={JSON.stringify(breakpoints)}
|
|
32
47
|
data-step-widths={[...new Set([...design.map((d) => d.width), ...edges])].sort((a, b) => a - b).join(",")}
|
|
33
48
|
>
|
|
34
|
-
<div class="lab-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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>
|
|
44
82
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
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>
|
|
53
139
|
</div>
|
|
54
140
|
|
|
55
|
-
<
|
|
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>
|
|
141
|
+
<div class="lab-nb__overlays">
|
|
61
142
|
{
|
|
62
|
-
|
|
63
|
-
<button
|
|
64
|
-
|
|
65
|
-
|
|
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>
|
|
66
154
|
</button>
|
|
67
|
-
)
|
|
155
|
+
)
|
|
68
156
|
}
|
|
69
|
-
<hr />
|
|
70
|
-
<p class="lab-menu__label">Devices</p>
|
|
71
157
|
{
|
|
72
|
-
|
|
73
|
-
<button
|
|
74
|
-
|
|
75
|
-
|
|
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>
|
|
76
169
|
</button>
|
|
77
|
-
)
|
|
170
|
+
)
|
|
78
171
|
}
|
|
79
172
|
</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
173
|
</div>
|
|
@@ -3,13 +3,28 @@
|
|
|
3
3
|
// dragging its edges. Media queries and vh units inside the frame follow the frame's own size,
|
|
4
4
|
// which is why it is an iframe and never an inline render. Zoom is a transform on the wrapper, so
|
|
5
5
|
// zooming never changes the frame's width — or its breakpoints. The parameters drawer sits on the
|
|
6
|
-
// canvas's right edge, over it
|
|
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.
|
|
7
16
|
interface Props {
|
|
8
17
|
src: string;
|
|
9
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;
|
|
10
25
|
}
|
|
11
26
|
|
|
12
|
-
const { src, title } = Astro.props;
|
|
27
|
+
const { src, title, pins = false, tasksHref = null, parameters = false } = Astro.props;
|
|
13
28
|
---
|
|
14
29
|
|
|
15
30
|
<div class="lab-vp" data-lab-vp>
|
|
@@ -22,9 +37,17 @@ const { src, title } = Astro.props;
|
|
|
22
37
|
<div class="lab-vp-handle lab-vp-handle--s" data-axis="y" title="Drag to resize the height" aria-hidden="true"></div>
|
|
23
38
|
<div class="lab-vp-handle lab-vp-handle--se" data-axis="xy" title="Drag to resize" aria-hidden="true"></div>
|
|
24
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>}
|
|
25
41
|
</div>
|
|
26
42
|
</div>
|
|
27
|
-
|
|
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>}
|
|
28
51
|
</div>
|
|
29
52
|
|
|
30
53
|
<script>
|
package/src/chrome/icons.ts
CHANGED
|
@@ -41,6 +41,12 @@ export const ICONS = {
|
|
|
41
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
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
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
|
|
44
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
|
|
45
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
|
|
46
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
|
|
@@ -48,6 +54,9 @@ export const ICONS = {
|
|
|
48
54
|
"phone": "<rect width=\"14\" height=\"20\" x=\"5\" y=\"2\" rx=\"2\" ry=\"2\" /><path d=\"M12 18h.01\" />", // smartphone
|
|
49
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
|
|
50
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
|
|
51
60
|
"close": "<path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />", // x
|
|
52
61
|
"more": "<circle cx=\"12\" cy=\"12\" r=\"1\" /><circle cx=\"19\" cy=\"12\" r=\"1\" /><circle cx=\"5\" cy=\"12\" r=\"1\" />", // ellipsis
|
|
53
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
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
// The
|
|
2
|
-
// server's
|
|
3
|
-
// probe fails, the boxes stay disabled and show the
|
|
4
|
-
// says why. The box always shows the server's
|
|
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.
|
|
5
6
|
//
|
|
6
7
|
// GET /__lab/responsive → { done, approved } PUT { done, approved }
|
|
7
|
-
//
|
|
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).
|
|
8
11
|
const boxes = [...document.querySelectorAll<HTMLInputElement>("input[data-lab-mark]")];
|
|
9
12
|
const note = document.querySelector<HTMLElement>("[data-lab-marks-note]");
|
|
10
|
-
const respPill = document.querySelector<HTMLElement>("[data-lab-resp-pill]");
|
|
11
|
-
const cullPill = document.querySelector<HTMLElement>("[data-lab-cull-pill]");
|
|
12
13
|
|
|
13
14
|
type Resp = { done: string[]; approved: string[] };
|
|
14
15
|
let resp: Resp | null = null;
|
|
15
|
-
let marked: string[] | null = null;
|
|
16
16
|
|
|
17
17
|
const getJson = async (url: string) => {
|
|
18
18
|
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
@@ -26,35 +26,16 @@ const putJson = async (url: string, body: unknown) => {
|
|
|
26
26
|
body: JSON.stringify(body),
|
|
27
27
|
});
|
|
28
28
|
const data = await response.json().catch(() => ({}));
|
|
29
|
-
if (!response.ok) {
|
|
30
|
-
const names = Array.isArray(data.offenders) ? data.offenders.map((o: { reason?: string }) => o.reason).join("; ") : "";
|
|
31
|
-
throw new Error(`${data.error || `HTTP ${response.status}`}${names ? ` — ${names}` : ""}`);
|
|
32
|
-
}
|
|
29
|
+
if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
|
|
33
30
|
return data;
|
|
34
31
|
};
|
|
35
32
|
|
|
36
33
|
const paint = () => {
|
|
34
|
+
if (!resp) return;
|
|
37
35
|
for (const box of boxes) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (kind === "cull") {
|
|
41
|
-
if (marked) {
|
|
42
|
-
box.checked = marked.includes(file);
|
|
43
|
-
box.disabled = Boolean(box.dataset.blocked);
|
|
44
|
-
}
|
|
45
|
-
} else if (resp) {
|
|
46
|
-
box.checked = (kind === "done" ? resp.done : resp.approved).includes(file);
|
|
47
|
-
box.disabled = false;
|
|
48
|
-
}
|
|
36
|
+
box.checked = (box.dataset.labMark === "done" ? resp.done : resp.approved).includes(box.dataset.file!);
|
|
37
|
+
box.disabled = false;
|
|
49
38
|
}
|
|
50
|
-
const doneBox = boxes.find((b) => b.dataset.labMark === "done");
|
|
51
|
-
if (respPill && doneBox && resp) {
|
|
52
|
-
respPill.textContent = doneBox.checked ? "responsive" : "not responsive";
|
|
53
|
-
respPill.classList.toggle("lab-pill--responsive", doneBox.checked);
|
|
54
|
-
respPill.classList.toggle("lab-pill--unresponsive", !doneBox.checked);
|
|
55
|
-
}
|
|
56
|
-
const cullBox = boxes.find((b) => b.dataset.labMark === "cull");
|
|
57
|
-
if (cullPill) cullPill.hidden = !(cullBox && cullBox.checked);
|
|
58
39
|
};
|
|
59
40
|
|
|
60
41
|
const say = (text: string) => {
|
|
@@ -62,35 +43,27 @@ const say = (text: string) => {
|
|
|
62
43
|
};
|
|
63
44
|
|
|
64
45
|
if (boxes.length) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (resp || marked) say("Saved to your repo as you tick.");
|
|
72
|
-
paint();
|
|
73
|
-
});
|
|
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."));
|
|
74
52
|
|
|
75
53
|
for (const box of boxes) {
|
|
76
54
|
box.addEventListener("change", async () => {
|
|
55
|
+
if (!resp) return;
|
|
77
56
|
const file = box.dataset.file!;
|
|
78
|
-
const kind = box.dataset.labMark;
|
|
79
57
|
const want = box.checked;
|
|
80
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] };
|
|
81
63
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
} else if (resp) {
|
|
86
|
-
const set = new Set(kind === "done" ? resp.done : resp.approved);
|
|
87
|
-
if (want) set.add(file);
|
|
88
|
-
else set.delete(file);
|
|
89
|
-
const body = kind === "done" ? { done: [...set], approved: resp.approved } : { done: resp.done, approved: [...set] };
|
|
90
|
-
const data = await putJson("/__lab/responsive", body);
|
|
91
|
-
resp = { done: data.done ?? body.done, approved: data.approved ?? body.approved };
|
|
92
|
-
}
|
|
93
|
-
say("Saved.");
|
|
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.");
|
|
94
67
|
} catch (error) {
|
|
95
68
|
say(`Not saved: ${(error as Error).message}`);
|
|
96
69
|
}
|
package/src/chrome/model.ts
CHANGED
|
@@ -8,6 +8,9 @@ import type { IconName } from "./icons";
|
|
|
8
8
|
import { labBase } from "../shell/lab-index";
|
|
9
9
|
import storyModules from "virtual:astrobook/story-modules.mjs";
|
|
10
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
|
+
|
|
11
14
|
/** Tiers that hold at least one stories file — an empty tier is not worth a row in level 1. */
|
|
12
15
|
const tiersInUse = new Set(storyModules.map((mod) => mod.directory.split("/")[0] ?? ""));
|
|
13
16
|
|
|
@@ -31,6 +34,17 @@ export interface TreeNode {
|
|
|
31
34
|
facets?: string;
|
|
32
35
|
disabled?: boolean;
|
|
33
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;
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
export interface Crumb {
|