@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.
Files changed (82) hide show
  1. package/README.md +261 -116
  2. package/bin/pin-gallery.mjs +53 -19
  3. package/defaults.mjs +72 -20
  4. package/dist/core/virtual-module/virtual-routes.js +12 -1
  5. package/docs/PIN-CONTRACT.md +84 -10
  6. package/docs/PIN.md +117 -37
  7. package/index.d.ts +41 -23
  8. package/index.mjs +46 -91
  9. package/package.json +7 -3
  10. package/src/Home.astro +166 -264
  11. package/src/LabHead.astro +37 -1047
  12. package/src/chrome/ActionsMenu.astro +97 -0
  13. package/src/chrome/ComponentCard.astro +76 -0
  14. package/src/chrome/Icon.astro +21 -0
  15. package/src/chrome/LICENSE-icons +43 -0
  16. package/src/chrome/Nav.astro +131 -0
  17. package/src/chrome/Panel.astro +117 -0
  18. package/src/chrome/Properties.astro +104 -0
  19. package/src/chrome/SectionsTree.astro +128 -0
  20. package/src/chrome/Shell.astro +120 -0
  21. package/src/chrome/Sprite.astro +23 -0
  22. package/src/chrome/StoryView.astro +192 -0
  23. package/src/chrome/Tree.astro +86 -0
  24. package/src/chrome/ViewportControls.astro +173 -0
  25. package/src/chrome/ViewportStage.astro +55 -0
  26. package/src/chrome/fonts/OFL.txt +93 -0
  27. package/src/chrome/fonts/inter-latin-wght-normal.woff2 +0 -0
  28. package/src/chrome/icons.ts +68 -0
  29. package/src/chrome/marks-client.ts +75 -0
  30. package/src/chrome/model.ts +156 -0
  31. package/src/chrome/navbar-client.ts +324 -0
  32. package/src/chrome/params-client.ts +434 -0
  33. package/src/chrome/pins-data.ts +63 -0
  34. package/src/chrome/shell-client.ts +468 -0
  35. package/src/chrome/site-data.ts +230 -0
  36. package/src/chrome/trees.ts +257 -0
  37. package/src/chrome/viewport-client.ts +405 -0
  38. package/src/chrome/views/Assets.astro +125 -0
  39. package/src/chrome/views/Pages.astro +214 -0
  40. package/src/chrome/views/Placeholder.astro +37 -0
  41. package/src/chrome/views/Tasks.astro +79 -0
  42. package/src/core/LICENSE-astrobook +21 -0
  43. package/src/core/lib/components/home.astro +4 -2
  44. package/src/core/lib/pages/story.astro +12 -10
  45. package/src/core/utils/kebab-case.ts +2 -2
  46. package/src/core/virtual-module/virtual-routes.ts +20 -4
  47. package/src/pin/board.mjs +414 -190
  48. package/src/pin/index.mjs +67 -22
  49. package/src/pin/tickets.mjs +6 -5
  50. package/src/pin/toolbar.js +82 -4
  51. package/src/shell/Browse.astro +131 -353
  52. package/src/shell/Viewport.astro +22 -1315
  53. package/src/shell/lab-index.ts +28 -18
  54. package/src/shell/lab-params.ts +113 -6
  55. package/src/shell/live-files.mjs +212 -10
  56. package/src/shell/marks.mjs +17 -41
  57. package/src/ui/components/app.astro +5 -7
  58. package/src/ui/components/preview-layout.astro +17 -0
  59. package/src/ui/components/theme-script.astro +17 -5
  60. package/src/ui/lab.css +3754 -371
  61. package/virtual.d.ts +13 -4
  62. package/bin/lab-cull.mjs +0 -401
  63. package/src/shell/CardGrid.astro +0 -297
  64. package/src/ui/components/build-path.ts +0 -13
  65. package/src/ui/components/build-tree.ts +0 -108
  66. package/src/ui/components/collapse-duration.ts +0 -28
  67. package/src/ui/components/compress-terms.ts +0 -10
  68. package/src/ui/components/dashboard-layout.astro +0 -39
  69. package/src/ui/components/home.astro +0 -65
  70. package/src/ui/components/layout.astro +0 -110
  71. package/src/ui/components/sidebar-button-fullscreen.astro +0 -38
  72. package/src/ui/components/sidebar-button-search.astro +0 -23
  73. package/src/ui/components/sidebar-button-theme.astro +0 -9
  74. package/src/ui/components/sidebar-button.astro +0 -24
  75. package/src/ui/components/sidebar-resize-handle.astro +0 -74
  76. package/src/ui/components/sidebar-search-panel.astro +0 -41
  77. package/src/ui/components/sidebar-search-script.ts +0 -103
  78. package/src/ui/components/sidebar-title.astro +0 -17
  79. package/src/ui/components/sidebar-tree-node.astro +0 -143
  80. package/src/ui/components/sidebar-tree.astro +0 -84
  81. package/src/ui/components/sidebar.astro +0 -29
  82. 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.
@@ -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 };