@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,97 @@
1
+ ---
2
+ // The navbar's ⋯ button and its menu — Figma `Astrolab` › Menu › Type=Actions. Everything that
3
+ // leaves the lab (VS Code, the bare variant full screen, staging, a link at this exact size), then
4
+ // two sections that are switches rather than places:
5
+ //
6
+ // Pins Show pins (on) and Show resolved (off) — they govern the markers drawn on the framed
7
+ // preview. They moved here from the navbar (decided 2026-09-24); the navbar keeps only
8
+ // Comment. Only while the pin board runs, which is `astro dev` with the board enabled.
9
+ // Preview Dark mode — the PREVIEW's own light or dark, separate from the chrome's. Hidden until
10
+ // the framed site turns out to have a dark mode (./viewport-client.ts looks for one).
11
+ //
12
+ // Checkable rows are `menuitemcheckbox` buttons, so the menu stays one list a keyboard can walk.
13
+ // Behaviour: ./navbar-client.ts (pins), ./viewport-client.ts (copy link, dark mode).
14
+ import Icon from "./Icon.astro";
15
+
16
+ interface Props {
17
+ editorUrl?: string | null;
18
+ /** The bare variant, alone in a tab. */
19
+ fullScreenUrl?: string | null;
20
+ stagingUrl?: string | null;
21
+ /** Show the Pins section — only where the pin board runs. */
22
+ pins: boolean;
23
+ }
24
+
25
+ const { editorUrl, fullScreenUrl, stagingUrl, pins } = Astro.props;
26
+ ---
27
+
28
+ <button
29
+ class="lab-tool lab-tool--icon lab-nb__more"
30
+ type="button"
31
+ popovertarget="lab-actions-menu"
32
+ title="More actions"
33
+ >
34
+ <Icon name="more" size="sm" />
35
+ <span class="lab-visually-hidden">More actions</span>
36
+ </button>
37
+ <div class="lab-menu lab-menu--actions" id="lab-actions-menu" popover role="menu" data-lab-align="end">
38
+ {
39
+ editorUrl && (
40
+ <a class="lab-menu__item" role="menuitem" href={editorUrl}>
41
+ <span class="lab-menu__mark"><Icon name="code" size="sm" /></span>
42
+ <span class="lab-menu__text">Open in VS Code</span>
43
+ </a>
44
+ )
45
+ }
46
+ {
47
+ fullScreenUrl && (
48
+ <a class="lab-menu__item" role="menuitem" href={fullScreenUrl} target="_blank" rel="noopener">
49
+ <span class="lab-menu__mark"><Icon name="expand" size="sm" /></span>
50
+ <span class="lab-menu__text">Open full screen</span>
51
+ <span class="lab-menu__hint">the bare variant</span>
52
+ </a>
53
+ )
54
+ }
55
+ {
56
+ stagingUrl && (
57
+ <a class="lab-menu__item" role="menuitem" href={stagingUrl} target="_blank" rel="noopener">
58
+ <span class="lab-menu__mark"><Icon name="staging" size="sm" /></span>
59
+ <span class="lab-menu__text">View on staging</span>
60
+ </a>
61
+ )
62
+ }
63
+ {(editorUrl || fullScreenUrl || stagingUrl) && <hr />}
64
+ <button class="lab-menu__item" type="button" role="menuitem" data-lab-copy-link>
65
+ <span class="lab-menu__mark"><Icon name="link" size="sm" /></span>
66
+ <span class="lab-menu__text" data-lab-copy-label>Copy link to this exact size</span>
67
+ <span class="lab-menu__hint" data-lab-copy-size></span>
68
+ </button>
69
+ {
70
+ pins && (
71
+ <>
72
+ <hr />
73
+ <p class="lab-menu__label" id="lab-actions-pins">Pins</p>
74
+ <div role="group" aria-labelledby="lab-actions-pins">
75
+ <button class="lab-menu__item" type="button" role="menuitemcheckbox" aria-checked="true" data-lab-pins-toggle="show">
76
+ <span class="lab-menu__mark"><span class="lab-menu__box" aria-hidden="true" /></span>
77
+ <span class="lab-menu__text">Show pins</span>
78
+ </button>
79
+ <button class="lab-menu__item" type="button" role="menuitemcheckbox" aria-checked="false" data-lab-pins-toggle="resolved">
80
+ <span class="lab-menu__mark"><span class="lab-menu__box" aria-hidden="true" /></span>
81
+ <span class="lab-menu__text">Show resolved</span>
82
+ </button>
83
+ </div>
84
+ </>
85
+ )
86
+ }
87
+ <div data-lab-preview-group hidden>
88
+ <hr />
89
+ <p class="lab-menu__label" id="lab-actions-preview">Preview</p>
90
+ <div role="group" aria-labelledby="lab-actions-preview">
91
+ <button class="lab-menu__item" type="button" role="menuitemcheckbox" aria-checked="false" data-lab-preview-theme>
92
+ <span class="lab-menu__mark"><span class="lab-menu__box" aria-hidden="true" /></span>
93
+ <span class="lab-menu__text">Dark mode</span>
94
+ </button>
95
+ </div>
96
+ </div>
97
+ </div>
@@ -0,0 +1,76 @@
1
+ ---
2
+ // One card per COMPONENT (a stories module), not per story — the tree stops at the component and
3
+ // so does the grid. The thumbnail is its first story's bare render (/lab/stories/<id>) in a lazy
4
+ // <iframe> sized to a virtual viewport and scaled to the card by ../chrome/shell-client.ts: page
5
+ // width for section-like tiers, a component stage otherwise. The whole card opens the component;
6
+ // the icon links under it sit above that link.
7
+ //
8
+ // A live component carries the Sections tree's slot badge, not a "live · slot N of M" pill: one sign
9
+ // for "live in slot N" across the lab (decided 2026-09-24, ASTROL-26). The title keeps the long form.
10
+ import Icon from "./Icon.astro";
11
+ import { componentState, type LabComponent } from "./trees";
12
+ import { isSectionLike, livePillText, liveTitle } from "../shell/lab-index";
13
+
14
+ interface Props {
15
+ mod: LabComponent;
16
+ }
17
+
18
+ const { mod } = Astro.props;
19
+ const first = mod.stories[0]!;
20
+ const liveStory = mod.stories.find((s) => s.live);
21
+ const section = isSectionLike(first);
22
+ const state = componentState(mod);
23
+ ---
24
+
25
+ <article class="lab-card" data-lab-search={state.search} data-lab-facets={state.facets}>
26
+ <a class="lab-card__main" href={first.dashboardUrl}>
27
+ <span class="lab-card__thumb" data-frame-w={section ? 1440 : 960} data-frame-h={section ? 900 : 540}>
28
+ <iframe src={first.storyUrl} title={`${mod.moduleName} — ${first.storyName}`} loading="lazy" tabindex="-1"></iframe>
29
+ </span>
30
+ <span class="lab-card__body">
31
+ <span class="lab-card__name">
32
+ <span>{mod.moduleName}</span>
33
+ {mod.version && <span class="lab-card__version">{mod.version}</span>}
34
+ </span>
35
+ <span class="lab-card__sub">
36
+ {mod.stories.length} variant{mod.stories.length === 1 ? "" : "s"}{mod.summary ? ` · ${mod.summary}` : ""}
37
+ </span>
38
+ <span class="lab-card__pills">
39
+ {liveStory?.live && (
40
+ <span class="lab-slot" data-state="slotted" title={liveTitle(liveStory) ?? livePillText(liveStory) ?? undefined}>
41
+ <span class="lab-visually-hidden">Live in slot </span>{liveStory.live.slot}
42
+ </span>
43
+ )}
44
+ {!mod.live && mod.usedBy.length > 0 && (
45
+ <span class="lab-pill lab-pill--used" title={mod.usedBy.join("\n")}>used by {mod.usedBy.length}</span>
46
+ )}
47
+ {mod.responsive && (
48
+ <span
49
+ class:list={["lab-pill", mod.responsive.done ? "lab-pill--responsive" : "lab-pill--unresponsive"]}
50
+ title={mod.responsive.approved ? "approved for responsive work" : "not approved for responsive work yet"}
51
+ >
52
+ {mod.responsive.done ? "responsive" : "not responsive"}
53
+ </span>
54
+ )}
55
+ </span>
56
+ </span>
57
+ </a>
58
+ {
59
+ (first.editorUrl || first.stagingUrl) && (
60
+ <span class="lab-card__links">
61
+ {first.editorUrl && (
62
+ <a class="lab-icon-btn lab-card__link" href={first.editorUrl} title="Open in VS Code">
63
+ <Icon name="code" />
64
+ <span class="lab-visually-hidden">Open in VS Code</span>
65
+ </a>
66
+ )}
67
+ {first.stagingUrl && (
68
+ <a class="lab-icon-btn lab-card__link" href={first.stagingUrl} target="_blank" rel="noopener" title="View on staging">
69
+ <Icon name="external" />
70
+ <span class="lab-visually-hidden">View on staging</span>
71
+ </a>
72
+ )}
73
+ </span>
74
+ )
75
+ }
76
+ </article>
@@ -0,0 +1,21 @@
1
+ ---
2
+ // One icon from the sprite (./Sprite.astro). Decorative by default; pass `label` when the icon is
3
+ // the only thing naming a control.
4
+ import type { IconName } from "./icons";
5
+
6
+ interface Props {
7
+ name: IconName;
8
+ size?: "sm" | "md";
9
+ class?: string;
10
+ label?: string;
11
+ }
12
+
13
+ const { name, size = "md", class: extra, label } = Astro.props;
14
+ ---
15
+
16
+ <svg
17
+ class:list={["lab-icon", size === "sm" && "lab-icon--sm", extra]}
18
+ aria-hidden={label ? undefined : "true"}
19
+ role={label ? "img" : undefined}
20
+ aria-label={label}
21
+ ><use href={`#lab-i-${name}`}></use></svg>
@@ -0,0 +1,43 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Lucide Icons and Contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
17
+ ---
18
+
19
+ The following Lucide icons are derived from the Feather project:
20
+
21
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
22
+
23
+ The MIT License (MIT) (for the icons listed above)
24
+
25
+ Copyright (c) 2013-present Cole Bemis
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to deal
29
+ in the Software without restriction, including without limitation the rights
30
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
+ copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in all
35
+ copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43
+ SOFTWARE.
@@ -0,0 +1,131 @@
1
+ ---
2
+ // Level 1 — the main nav (Figma `Sidebar navigation`, the white card). Home, the Site group (Pages,
3
+ // one entry per tier, Assets), the Tasks group when the pin board is running, and Support and
4
+ // Settings at the foot. Collapses to a 64px icon rail (⌘⇧B); in the rail a group's items open as
5
+ // a flyout on hover or focus, under the group's name, drawn by CSS alone (../ui/lab.css › level 1
6
+ // as a rail).
7
+ //
8
+ // The header carries the collapse button and the LAB's own light/dark toggle — a moon, beside the
9
+ // logo, under the logomark in the rail (decided 2026-09-24). It switches the chrome only; the
10
+ // preview's light/dark is a separate setting of its own.
11
+ //
12
+ // The logo and the Support / Settings pages are placeholders until they are designed.
13
+ import Icon from "./Icon.astro";
14
+ import { labConfig, navModel, type NavKey } from "./model";
15
+
16
+ interface Props {
17
+ active: NavKey;
18
+ }
19
+
20
+ const { active } = Astro.props;
21
+ const model = navModel();
22
+ const isOpen = (group: { items: { key: NavKey }[] }) => group.items.some((item) => item.key === active);
23
+ ---
24
+
25
+ <nav class="lab-l1" aria-label="Lab">
26
+ <div class="lab-l1__card">
27
+ <div class="lab-l1__top">
28
+ <div class="lab-l1__header">
29
+ <a class="lab-brand" href={model.home.href} title={labConfig.title}>
30
+ <span class="lab-brand__mark" aria-hidden="true"></span>
31
+ <span class="lab-brand__name">Astrolab</span>
32
+ </a>
33
+ {/* The collapse button's name and tooltip follow the state: "Collapse menu" when level 1
34
+ is open, "Expand menu" in the rail (./shell-client.ts keeps aria-label in step). */}
35
+ <button
36
+ class="lab-icon-btn lab-l1__btn lab-tipped"
37
+ type="button"
38
+ data-lab-toggle="l1"
39
+ data-lab-l1-btn
40
+ aria-label="Collapse menu"
41
+ aria-describedby="lab-tip-l1"
42
+ >
43
+ <Icon name="chevrons-left" size="sm" class="lab-l1-btn__collapse" />
44
+ <Icon name="chevrons-right" size="sm" class="lab-l1-btn__expand" />
45
+ <span class="lab-tip lab-tip--l1" id="lab-tip-l1" role="tooltip">
46
+ <span class="lab-tip__title">
47
+ <span class="lab-l1-btn__collapse">Collapse menu</span><span class="lab-l1-btn__expand">Expand menu</span> · ⌘⇧B
48
+ </span>
49
+ <span class="lab-tip__text">⌘B shows or hides the panel</span>
50
+ <span class="lab-tip__text">⌘. hides both (focus mode)</span>
51
+ </span>
52
+ </button>
53
+ <button
54
+ class="lab-icon-btn lab-l1__btn lab-theme-btn"
55
+ type="button"
56
+ data-lab-theme-toggle
57
+ aria-pressed="false"
58
+ aria-label="Dark mode for the lab"
59
+ title="Dark mode for the lab"
60
+ >
61
+ <Icon name="moon" size="sm" />
62
+ </button>
63
+ </div>
64
+
65
+ <div class="lab-l1__nav">
66
+ <a
67
+ class="lab-nav-item"
68
+ href={model.home.href}
69
+ aria-current={active === "home" ? "page" : undefined}
70
+ data-tip={model.home.label}
71
+ data-lab-nav-key="home"
72
+ >
73
+ <Icon name={model.home.icon} />
74
+ <span class="lab-nav-item__label">{model.home.label}</span>
75
+ </a>
76
+ {
77
+ model.groups.map((group) => (
78
+ <>
79
+ <hr class="lab-divider" />
80
+ <div class="lab-nav-group" data-lab-group={group.id} data-lab-holds-current={isOpen(group) ? "" : undefined}>
81
+ <button
82
+ class="lab-nav-item"
83
+ type="button"
84
+ aria-expanded="true"
85
+ aria-current={isOpen(group) ? "true" : undefined}
86
+ data-tip={group.label}
87
+ data-lab-group-toggle
88
+ >
89
+ <Icon name={group.icon} />
90
+ <span class="lab-nav-item__label">{group.label}</span>
91
+ <Icon name="chevron-down" size="sm" class="lab-nav-group__chevron" />
92
+ </button>
93
+ <div class="lab-nav-group__items">
94
+ <p class="lab-nav-group__flyout-label" aria-hidden="true">{group.label}</p>
95
+ {group.items.map((item) => (
96
+ <a
97
+ class="lab-nav-item"
98
+ href={item.href}
99
+ aria-current={item.key === active ? "page" : undefined}
100
+ data-lab-nav-key={item.key}
101
+ >
102
+ <Icon name={item.icon} />
103
+ <span class="lab-nav-item__label">{item.label}</span>
104
+ </a>
105
+ ))}
106
+ </div>
107
+ </div>
108
+ </>
109
+ ))
110
+ }
111
+ </div>
112
+ </div>
113
+
114
+ <div class="lab-l1__footer">
115
+ {
116
+ model.footer.map((item) => (
117
+ <a
118
+ class="lab-nav-item"
119
+ href={item.href}
120
+ aria-current={item.key === active ? "page" : undefined}
121
+ data-tip={item.label}
122
+ data-lab-nav-key={item.key}
123
+ >
124
+ <Icon name={item.icon} />
125
+ <span class="lab-nav-item__label">{item.label}</span>
126
+ </a>
127
+ ))
128
+ }
129
+ </div>
130
+ </div>
131
+ </nav>
@@ -0,0 +1,117 @@
1
+ ---
2
+ // Level 2 — the panel (Figma `Sidebar navigation`, the grey column): breadcrumb, search with its
3
+ // filter menu, the tree in the default slot, and the coffee footer. Closes with ⌘B or its own
4
+ // button; while closed it peeks out over the stage — below the navbar, never over it — when the
5
+ // pointer reaches the stage's left edge.
6
+ //
7
+ // Only the Site views have one. Kanban Board and All Tasks have no level 2 (decided 2026-09-24).
8
+ //
9
+ // The coffee link is a placeholder until it has a destination.
10
+ import Icon from "./Icon.astro";
11
+ import { labBase, type Crumb, type FilterOption } from "./model";
12
+
13
+ interface Props {
14
+ crumbs: Crumb[];
15
+ /** Label for the panel landmark, e.g. "Pages". */
16
+ label: string;
17
+ search?: boolean;
18
+ searchPlaceholder?: string;
19
+ filters?: FilterOption[];
20
+ filterLabel?: string;
21
+ }
22
+
23
+ const { crumbs, label, search = true, searchPlaceholder = "Search", filters, filterLabel = "Show" } = Astro.props;
24
+ ---
25
+
26
+ <aside class="lab-l2" id="lab-l2" aria-label={label}>
27
+ <div class="lab-l2__header">
28
+ <div class="lab-l2__crumbs-row">
29
+ <nav aria-label="Breadcrumb">
30
+ <ol class="lab-crumbs">
31
+ <li class="lab-crumbs__item">
32
+ <a href={labBase || "/"} title="Lab home"><Icon name="home" /><span class="lab-visually-hidden">Lab home</span></a>
33
+ </li>
34
+ {
35
+ crumbs.map((crumb, i) => (
36
+ <li class="lab-crumbs__item" aria-current={i === crumbs.length - 1 ? "page" : undefined}>
37
+ {crumb.href && i < crumbs.length - 1 ? <a href={crumb.href}>{crumb.label}</a> : crumb.label}
38
+ </li>
39
+ ))
40
+ }
41
+ </ol>
42
+ </nav>
43
+ {/* One button, two actions: docked, it hides the panel; while the panel only peeks, it keeps
44
+ it open. Its name follows the action — ./shell-client.ts swaps the label when a peek
45
+ starts and ends (decided 2026-09-24). */}
46
+ <button
47
+ class="lab-icon-btn lab-l2__close"
48
+ type="button"
49
+ data-lab-toggle="l2"
50
+ data-lab-l2-btn
51
+ aria-label="Hide panel"
52
+ title="Hide panel · ⌘B"
53
+ >
54
+ <Icon name="panel-close" class="lab-l2__close-hide" />
55
+ <Icon name="panel-open" class="lab-l2__close-dock" />
56
+ </button>
57
+ </div>
58
+
59
+ {
60
+ search && (
61
+ <div class="lab-input">
62
+ <Icon name="search" size="sm" />
63
+ <input
64
+ class="lab-input__field"
65
+ type="search"
66
+ placeholder={searchPlaceholder}
67
+ aria-label={searchPlaceholder}
68
+ autocomplete="off"
69
+ spellcheck="false"
70
+ data-lab-tree-search
71
+ />
72
+ <kbd class="lab-input__kbd">⌘K</kbd>
73
+ {filters && filters.length > 0 && (
74
+ <button
75
+ class="lab-icon-btn"
76
+ type="button"
77
+ popovertarget="lab-tree-filter"
78
+ title="Filter"
79
+ data-lab-filter-btn
80
+ >
81
+ <Icon name="filter" size="sm" />
82
+ <span class="lab-visually-hidden">Filter</span>
83
+ </button>
84
+ )}
85
+ </div>
86
+ )
87
+ }
88
+ {
89
+ filters && filters.length > 0 && (
90
+ <div class="lab-menu" id="lab-tree-filter" popover role="menu" data-lab-align="end">
91
+ <p class="lab-menu__label">{filterLabel}</p>
92
+ <button class="lab-menu__item" type="button" role="menuitemradio" aria-checked="true" data-lab-filter="">
93
+ All
94
+ <Icon name="check" size="sm" class="lab-menu__tick" />
95
+ </button>
96
+ {filters.map((option) => (
97
+ <button class="lab-menu__item" type="button" role="menuitemradio" aria-checked="false" data-lab-filter={option.value}>
98
+ {option.label}
99
+ <Icon name="check" size="sm" class="lab-menu__tick" />
100
+ </button>
101
+ ))}
102
+ </div>
103
+ )
104
+ }
105
+ </div>
106
+
107
+ <div class="lab-l2__body" data-lab-l2-body>
108
+ <slot />
109
+ </div>
110
+
111
+ <div class="lab-l2__footer">
112
+ <span class="lab-nav-item lab-coffee" aria-disabled="true" title="The link is still to come">
113
+ <Icon name="coffee" />
114
+ <span class="lab-nav-item__label">Buy Me A Coffee</span>
115
+ </span>
116
+ </div>
117
+ </aside>
@@ -0,0 +1,104 @@
1
+ ---
2
+ // The eye beside a component's (or page's) name, and the Properties panel it opens. It replaces the
3
+ // navbar's status pills and their status menu (decided 2026-09-24: no pills in the identity — a
4
+ // panel that opens when you hover a small eye beside the name). Figma `Astrolab` › Menu ›
5
+ // Type=Properties, and the four cases on the `Lab / Properties` sheet.
6
+ //
7
+ // In order: On the site (live and its slot, used by, or not on any page), Open pins (with a row to
8
+ // the task list), Responsive (the two marks, sections only), Source file. Mark for deletion is not
9
+ // here: it was removed from the lab as a feature (decided 2026-09-24).
10
+ //
11
+ // Not a menu. The panel is a `popover="manual"` dialog: hovering the eye opens it after a short
12
+ // delay and it stays open while the pointer is on the eye or the panel; clicking the eye (or
13
+ // Enter / Space) pins it open until Escape or a click outside. Behaviour: ./navbar-client.ts. The
14
+ // marks are live checkboxes whenever the panel is open (./marks-client.ts).
15
+ import Icon from "./Icon.astro";
16
+
17
+ interface Props {
18
+ /** One line per fact under "On the site"; `live` draws the green dot, otherwise it is grey. */
19
+ site: { text: string; live: boolean }[];
20
+ /** Open and resolved pins, and where the row leads. Null when the pin board is not running. */
21
+ pins: { open: number; resolved: number; href: string } | null;
22
+ /** The responsive marks for this file, or null when the lab keeps none for it. */
23
+ responsive: { file: string; approved: boolean; done: boolean } | null;
24
+ source: string | null;
25
+ }
26
+
27
+ const { site, pins, responsive, source } = Astro.props;
28
+
29
+ const pinLine = (open: number, resolved: number) =>
30
+ open === 0 && resolved === 0 ? "No pins" : `${open === 0 ? "No open pins" : `${open} open`} · ${resolved} resolved`;
31
+ ---
32
+
33
+ <button
34
+ class="lab-tool lab-tool--icon"
35
+ type="button"
36
+ aria-expanded="false"
37
+ aria-controls="lab-props"
38
+ title="Properties — hover to look, click to pin open"
39
+ data-lab-props-trigger
40
+ >
41
+ <Icon name="properties" size="sm" />
42
+ <span class="lab-visually-hidden">Properties</span>
43
+ </button>
44
+ <div class="lab-menu lab-props" id="lab-props" popover="manual" role="dialog" aria-label="Properties" data-lab-props>
45
+ <p class="lab-menu__label">On the site</p>
46
+ <div class="lab-props__facts">
47
+ {
48
+ site.map((line) => (
49
+ <p class="lab-props__fact">
50
+ <span class:list={["lab-props__dot", line.live && "lab-props__dot--live"]} aria-hidden="true" />
51
+ {line.text}
52
+ </p>
53
+ ))
54
+ }
55
+ </div>
56
+ {
57
+ pins && (
58
+ <>
59
+ <hr />
60
+ <p class="lab-menu__label">Open pins</p>
61
+ <a class="lab-menu__item" href={pins.href}>
62
+ <span class="lab-menu__mark lab-props__pin">
63
+ <Icon name="marker" size="sm" />
64
+ </span>
65
+ <span class="lab-menu__text">{pinLine(pins.open, pins.resolved)}</span>
66
+ <Icon name="chevron-right" size="sm" class="lab-menu__trail" />
67
+ </a>
68
+ </>
69
+ )
70
+ }
71
+ {
72
+ responsive && (
73
+ <>
74
+ <hr />
75
+ <p class="lab-menu__label">Responsive</p>
76
+ <label class="lab-menu__item">
77
+ <span class="lab-menu__mark">
78
+ <input class="lab-checkbox" type="checkbox" data-lab-mark="approved" data-file={responsive.file} checked={responsive.approved} disabled />
79
+ </span>
80
+ <span class="lab-menu__text">Approved for responsive work</span>
81
+ </label>
82
+ <label class="lab-menu__item">
83
+ <span class="lab-menu__mark">
84
+ <input class="lab-checkbox" type="checkbox" data-lab-mark="done" data-file={responsive.file} checked={responsive.done} disabled />
85
+ </span>
86
+ <span class="lab-menu__text">Made responsive</span>
87
+ </label>
88
+ </>
89
+ )
90
+ }
91
+ {
92
+ source && (
93
+ <>
94
+ <hr />
95
+ <p class="lab-menu__label">Source file</p>
96
+ <p class="lab-props__source">{source}</p>
97
+ </>
98
+ )
99
+ }
100
+ <hr />
101
+ <p class="lab-props__note" data-lab-marks-note>
102
+ {responsive ? "Click the eye to pin · marks save to the repo" : "Click the eye to pin"}
103
+ </p>
104
+ </div>