@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
@@ -1,371 +1,149 @@
1
1
  ---
2
- // Folder pages — <subpath>/browse/[...path], injected by ../../index.mjs. One page per folder in
3
- // the lab index (sections, sections/Section02Manifesto, …) plus the root, which lists the tiers.
4
- // Each page shows the folder's sub-folders as cards and its stories as live-render cards with
5
- // their pills, behind a client-side filter box.
2
+ // Folder pages — <subpath>/browse/[...path], injected by ../../index.mjs. One page per tier and per
3
+ // folder under it (sections, sections/Section02Manifesto, …), plus the root, which lists the tiers.
4
+ // Level 2 is the tier's tree; the content is the folder's components as cards, grouped by the
5
+ // sub-folder they sit in, and the panel's search and filter narrow both at once.
6
6
  //
7
- // This page is NOT rendered through Astrobook's layout, so it carries its own <head> — and with
8
- // it <LabHead />, which brings the chrome stylesheet (../ui/lab.css) and the UNCONDITIONAL
9
- // noindex. A consumer's lab gate walks every built lab page and fails on any without it. Do not
10
- // drop that import.
7
+ // REDESIGNED 2026-09-24 onto the chrome shell (../chrome/Shell.astro). Cards are per component
8
+ // now, not per story — a component's stories are its variants, chosen on its own page. The title
9
+ // bar is the listing one (Figma `Lab / Listing`): title, counts, and filter chips that drive the
10
+ // same filter as level 2's menu. On the sections tier, level 2 is the Sections tree.
11
11
  //
12
- // CHANGED 2026-09-22 — the lab is not styled in the styles of the website being built. This
13
- // file used to open with `import "virtual:orbytes-lab/user-css.mjs"` — it asked for the
14
- // consumer's stylesheets by hand, because Astrobook injects the `css` option into its own pages
15
- // only. A folder page is pure chrome: it renders no story, and its thumbnails are iframes of
16
- // /lab/stories/<id>, which are their own documents and get the consumer's CSS there. So it asks
17
- // for nothing now, and the chrome below is drawn entirely in `--lab-*` tokens. Reasoning:
18
- // ../ui/lab.css.
12
+ // Pure chrome: this page renders no story. Its thumbnails are iframes of /lab/stories/<id>, which
13
+ // are their own documents and get the consumer's CSS there (decided 2026-09-22 — ../ui/lab.css).
19
14
  import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
20
- import LabHead from "../LabHead.astro";
21
- import CardGrid from "./CardGrid.astro";
22
- import { buildLabIndex, itemsUnder, labBase, type LabFolder } from "./lab-index";
15
+ import Shell from "../chrome/Shell.astro";
16
+ import Tree from "../chrome/Tree.astro";
17
+ import ComponentCard from "../chrome/ComponentCard.astro";
18
+ import { hrefs, labConfig, type Crumb, type FilterOption, type TreeNode } from "../chrome/model";
19
+ import { componentsOf, tierTree, type LabComponent } from "../chrome/trees";
20
+ import { buildLabIndex, sortTiers, tierLabel } from "./lab-index";
23
21
 
24
22
  export const getStaticPaths = (() => {
25
23
  const index = buildLabIndex();
26
24
  return [
27
- { params: { path: undefined }, props: { index, folder: null } },
28
- ...index.folders.map((folder) => ({ params: { path: folder.path }, props: { index, folder } })),
25
+ { params: { path: undefined }, props: { folder: "" } },
26
+ ...index.folders.map((folder) => ({ params: { path: folder.path }, props: { folder: folder.path } })),
29
27
  ];
30
28
  }) satisfies GetStaticPaths;
31
29
 
32
30
  type Props = InferGetStaticPropsType<typeof getStaticPaths>;
33
31
 
34
- const { index, folder } = Astro.props as Props;
35
- const browseBase = `${labBase}/browse`;
36
-
37
- const folderByPath = new Map(index.folders.map((f) => [f.path, f]));
38
- const itemById = new Map(index.items.map((i) => [i.storyId, i]));
39
-
40
- const children: LabFolder[] = folder
41
- ? folder.children.map((p) => folderByPath.get(p)).filter((f): f is LabFolder => Boolean(f))
42
- : index.folders.filter((f) => !f.path.includes("/"));
43
-
44
- const items = folder
45
- ? folder.items.map((id) => itemById.get(id)).filter((i) => i !== undefined)
46
- : index.items.filter((i) => i.directory === "");
47
-
48
- const storyCount = (f: LabFolder) => itemsUnder(index, f.path).length;
49
- const firstStory = folder ? itemsUnder(index, folder.path)[0] : index.items[0];
50
-
51
- const segments = folder ? folder.path.split("/") : [];
52
- const crumbs = [
53
- { label: "lab", href: labBase },
54
- ...segments.map((segment, i) => ({
55
- label: segment,
56
- href: `${browseBase}/${segments.slice(0, i + 1).join("/")}`,
57
- })),
32
+ const { folder } = Astro.props as Props;
33
+ const index = buildLabIndex();
34
+ const all = componentsOf(index);
35
+ const segments = folder ? folder.split("/") : [];
36
+ const tier = segments[0] ?? "";
37
+
38
+ const under = (mod: LabComponent) => !folder || mod.directory === folder || mod.directory.startsWith(`${folder}/`);
39
+
40
+ // One grid, in the tree's own order, so the cards read in the same sequence as level 2. The root
41
+ // page (no folder) lists every tier in turn, each under its name.
42
+ const leafOrder = (tierId: string) => {
43
+ const ids: string[] = [];
44
+ const walk = (nodes: TreeNode[]) => {
45
+ for (const node of nodes) node.children ? walk(node.children) : ids.push(node.id);
46
+ };
47
+ walk(tierTree(index, tierId));
48
+ return ids;
49
+ };
50
+ const inOrder = (tierId: string) => {
51
+ const byId = new Map(all.filter((m) => m.tier === tierId).map((m) => [`mod:${m.moduleId}`, m]));
52
+ return leafOrder(tierId).map((id) => byId.get(id)!).filter(Boolean);
53
+ };
54
+ const ordered: (readonly [string, LabComponent[]])[] = folder
55
+ ? [["", inOrder(tier).filter(under)] as const]
56
+ : sortTiers([...new Set(all.map((m) => m.tier))]).map((t) => [t, inOrder(t)] as const);
57
+ const mods = ordered.flatMap(([, list]) => list);
58
+
59
+ const tierDef = labConfig.tiers.find((t) => t.id === tier);
60
+ const title = folder ? (segments.length === 1 ? tierLabel(tier) : segments[segments.length - 1]!) : "All components";
61
+ // Level 2's breadcrumb names the panel's place (Site › Sections); a deeper folder is the navbar's.
62
+ const crumbs: Crumb[] = folder
63
+ ? [{ label: "Site" }, { label: tierLabel(tier), href: hrefs.tier(tier) }]
64
+ : [{ label: "Site" }, { label: "All components" }];
65
+
66
+ const filters: FilterOption[] = [
67
+ { value: "live", label: "Live on the site" },
68
+ { value: "used", label: "Used by something live" },
69
+ { value: "unused", label: "Unused" },
70
+ ...(tierDef?.responsive
71
+ ? [
72
+ { value: "responsive", label: "Made responsive" },
73
+ { value: "unresponsive", label: "Not responsive yet" },
74
+ ]
75
+ : []),
76
+ ];
77
+ const live = mods.filter((m) => m.live).length;
78
+ const used = mods.filter((m) => !m.live && m.usedBy.length).length;
79
+ const unused = mods.length - live - used;
80
+ const stories = mods.reduce((n, m) => n + m.stories.length, 0);
81
+ const plural = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
82
+ // What one of these is called: "sections" on the sections tier, "components" anywhere else.
83
+ const noun = tier ? tierLabel(tier).toLowerCase() : "components";
84
+ const counts = [
85
+ segments.length > 1 ? segments.slice(0, -1).map((s, i) => (i === 0 ? tierLabel(s) : s)).join(" / ") : null,
86
+ plural(mods.length, noun.replace(/s$/, ""), noun),
87
+ plural(stories, "variant"),
88
+ live > 0 ? `${live} live` : null,
89
+ used > 0 ? `${used} used` : null,
90
+ unused > 0 ? `${unused} unused` : null,
91
+ ].filter(Boolean);
92
+ // The title bar's filter chips set the same filter as level 2's menu (./../chrome/shell-client.ts).
93
+ const chips = [
94
+ { value: "", label: "All", n: mods.length },
95
+ { value: "live", label: "Live", n: live },
96
+ { value: "used", label: "Used", n: used },
97
+ { value: "unused", label: "Unused", n: unused },
58
98
  ];
59
- const title = crumbs.map((c) => c.label).join(" › ");
60
99
  ---
61
100
 
62
- <html lang="en">
63
- <head>
64
- <meta charset="utf-8" />
65
- <meta name="viewport" content="width=device-width" />
66
- <meta name="generator" content={Astro.generator} />
67
- <title>{title}</title>
68
- <LabHead />
69
- </head>
70
- <body>
71
- <div class="browse lab-chrome">
72
- <header class="browse__header">
73
- <nav class="browse__crumbs" aria-label="Breadcrumb">
74
- <a class="browse__back" href={labBase}>← lab</a>
75
- <ol>
76
- {
77
- crumbs.map((crumb, i) => (
78
- <li>
79
- {i < crumbs.length - 1 ? <a href={crumb.href}>{crumb.label}</a> : <span aria-current="page">{crumb.label}</span>}
80
- </li>
81
- ))
82
- }
83
- </ol>
84
- </nav>
85
- <h1 class="browse__title">{folder ? folder.name : "Orbytes lab"}</h1>
86
- <p class="browse__meta">
87
- {children.length > 0 && <span>{children.length} folder{children.length === 1 ? "" : "s"}</span>}
88
- {items.length > 0 && <span>{items.length} stor{items.length === 1 ? "y" : "ies"} here</span>}
89
- {folder && folder.liveCount > 0 && <span class="browse__live">{folder.liveCount} live</span>}
90
- {
91
- folder && folder.responsive.of > 0 && (
92
- <span class="browse__responsive" title="section versions made responsive">
93
- {folder.responsive.done}/{folder.responsive.of} responsive
94
- </span>
95
- )
96
- }
97
- {
98
- firstStory && (
99
- <a href={firstStory.dashboardUrl}>
100
- open in the dashboard <span aria-hidden="true">&#8599;</span>
101
- </a>
102
- )
103
- }
104
- </p>
105
- <label class="browse__filter">
106
- <span class="browse__filter-label">// filter</span>
107
- <input id="lab-filter" type="search" placeholder="name, section, version, tag…" autocomplete="off" />
108
- </label>
109
- </header>
110
-
111
- {
112
- children.length > 0 && (
113
- <section class="browse__group">
114
- <h2 class="browse__group-title">Folders</h2>
115
- <div class="browse__folders">
116
- {children.map((child) => {
117
- const count = storyCount(child);
118
- return (
119
- <a class="folder-card" href={`${browseBase}/${child.path}`} data-search={child.path.toLowerCase()}>
120
- <span class="folder-card__name">{child.name}</span>
121
- <span class="folder-card__meta">
122
- <span>
123
- {count} stor{count === 1 ? "y" : "ies"}
124
- </span>
125
- {child.responsive.of > 0 && (
126
- <span class="folder-card__responsive" title="section versions made responsive">
127
- {child.responsive.done}/{child.responsive.of} responsive
128
- </span>
129
- )}
130
- {child.liveCount > 0 && <span class="folder-card__live">{child.liveCount} live</span>}
131
- </span>
132
- </a>
133
- );
134
- })}
101
+ <Shell
102
+ title={title}
103
+ active={tier ? `tier:${tier}` : "home"}
104
+ navbarClass="lab-navbar--listing"
105
+ panel={folder ? { label: tierLabel(tier), crumbs, filters, filterLabel: "Show components", searchPlaceholder: `Search ${tierLabel(tier).toLowerCase()}` } : undefined}
106
+ >
107
+ {folder && <Tree slot="panel" nodes={tierTree(index, tier)} />}
108
+
109
+ <Fragment slot="navbar">
110
+ <div class="lab-listing-bar">
111
+ <h1 class="lab-listing-bar__title">{title}</h1>
112
+ <p class="lab-listing-bar__counts">{counts.join(" · ")}</p>
113
+ <div class="lab-chips" role="radiogroup" aria-label="Show">
114
+ {
115
+ chips.map((chip) => (
116
+ <button class="lab-chip" type="button" role="radio" aria-checked={String(chip.value === "")} data-lab-filter={chip.value}>
117
+ {chip.label} {chip.n}
118
+ </button>
119
+ ))
120
+ }
121
+ </div>
122
+ </div>
123
+ </Fragment>
124
+
125
+ <div class="lab-page">
126
+ {
127
+ mods.length === 0 ? (
128
+ <div class="lab-empty">
129
+ <strong>Nothing here yet</strong>
130
+ Add a <code>*.stories.ts</code> file under <code>{labConfig.directory}/{folder}</code> and it appears here.
131
+ </div>
132
+ ) : (
133
+ ordered.map(([key, list]) => (
134
+ <section data-lab-filter-section>
135
+ {key && (
136
+ <h2 class="lab-section-title">
137
+ <a href={hrefs.tier(key)}>{tierLabel(key)}</a>
138
+ <small>{list.length}</small>
139
+ </h2>
140
+ )}
141
+ <div class="lab-cards">
142
+ {list.map((mod) => <ComponentCard mod={mod} />)}
135
143
  </div>
136
144
  </section>
137
- )
138
- }
139
-
140
- {
141
- items.length > 0 && (
142
- <section class="browse__group">
143
- <h2 class="browse__group-title">Stories</h2>
144
- <CardGrid items={items} />
145
- </section>
146
- )
147
- }
148
-
149
- {children.length === 0 && items.length === 0 && <p class="browse__empty">Nothing in this folder.</p>}
150
- </div>
151
-
152
- <script>
153
- // Client-side filter: every term typed must appear in a card's data-search text.
154
- const input = document.getElementById("lab-filter") as HTMLInputElement | null;
155
- const cards = document.querySelectorAll<HTMLElement>("[data-search]");
156
- input?.addEventListener("input", () => {
157
- const terms = input.value.trim().toLowerCase().split(/\s+/).filter(Boolean);
158
- for (const card of cards) {
159
- const text = card.dataset.search ?? "";
160
- card.hidden = !terms.every((term) => text.includes(term));
161
- }
162
- });
163
- </script>
164
- </body>
165
- </html>
166
-
167
- <style is:global>
168
- /* The filter hides cards with the `hidden` attribute; the cards' own `display` rules (CardGrid's
169
- scoped styles) would otherwise beat the UA stylesheet, so this is global and !important. */
170
- [hidden] {
171
- display: none !important;
172
- }
173
- </style>
174
-
175
- <style>
176
- .browse {
177
- background-color: var(--lab-color-bg);
178
- color: var(--lab-color-text);
179
- font-family: var(--lab-font-body);
180
- min-height: 100vh;
181
- padding: 3em 2.5em 5em;
182
- }
183
-
184
- .browse__crumbs {
185
- display: flex;
186
- align-items: baseline;
187
- gap: 1.5em;
188
- font-family: var(--lab-font-mono);
189
- font-weight: 500;
190
- font-size: 0.85em;
191
- color: var(--lab-color-text-muted);
192
- margin-bottom: 1.25em;
193
- }
194
-
195
- .browse__crumbs ol {
196
- display: flex;
197
- flex-wrap: wrap;
198
- list-style: none;
199
- gap: 0.5em;
200
- }
201
-
202
- .browse__crumbs li + li::before {
203
- content: "›";
204
- color: var(--lab-color-text-faint);
205
- margin-right: 0.5em;
206
- }
207
-
208
- .browse__crumbs a,
209
- .browse__back {
210
- color: var(--lab-color-text-faint);
211
- text-decoration: none;
212
- }
213
-
214
- .browse__crumbs a:hover,
215
- .browse__crumbs a:focus-visible,
216
- .browse__back:hover,
217
- .browse__back:focus-visible {
218
- color: var(--lab-color-text-strong);
219
- text-decoration: underline;
220
- }
221
-
222
- .browse__crumbs [aria-current] {
223
- color: var(--lab-color-text-strong);
224
- }
225
-
226
- .browse__title {
227
- font-family: var(--lab-font-heading);
228
- color: var(--lab-color-text-strong);
229
- font-size: 2.25em;
230
- font-weight: 500;
231
- letter-spacing: -0.5px;
232
- margin-bottom: 0.4em;
233
- }
234
-
235
- .browse__meta {
236
- display: flex;
237
- flex-wrap: wrap;
238
- align-items: baseline;
239
- gap: 1.25em;
240
- font-family: var(--lab-font-mono);
241
- font-size: 0.85em;
242
- color: var(--lab-color-text-faint);
243
- margin-bottom: 1.5em;
244
- }
245
-
246
- .browse__meta a {
247
- color: var(--lab-color-text-strong);
248
- text-decoration: none;
249
- border-bottom: 1px solid var(--lab-color-border);
250
- }
251
-
252
- .browse__meta a:hover,
253
- .browse__meta a:focus-visible {
254
- border-bottom-color: var(--lab-color-text-strong);
255
- }
256
-
257
- .browse__responsive,
258
- .folder-card__responsive {
259
- line-height: 1;
260
- padding: 0.45em 0.85em;
261
- border-radius: var(--lab-radius-pill);
262
- border: 1px dashed var(--lab-color-border);
263
- color: var(--lab-color-text-faint);
264
- }
265
-
266
- .browse__live,
267
- .folder-card__live {
268
- font-weight: 500;
269
- line-height: 1;
270
- padding: 0.45em 0.85em;
271
- border-radius: var(--lab-radius-pill);
272
- background-color: var(--lab-color-accent);
273
- color: var(--lab-color-accent-text);
274
- }
275
-
276
- .browse__filter {
277
- display: flex;
278
- align-items: baseline;
279
- gap: 1em;
280
- max-width: 36em;
281
- border: 1px solid var(--lab-color-border);
282
- border-left: 1px solid var(--lab-color-text-strong);
283
- padding: 0.65em 1em;
284
- }
285
-
286
- .browse__filter-label {
287
- font-family: var(--lab-font-mono);
288
- font-weight: 500;
289
- font-size: 0.85em;
290
- color: var(--lab-color-text-muted);
291
- white-space: nowrap;
292
- }
293
-
294
- .browse__filter input {
295
- flex: 1;
296
- font: inherit;
297
- font-family: var(--lab-font-mono);
298
- font-size: 0.9em;
299
- color: var(--lab-color-text-strong);
300
- background: transparent;
301
- border: 0;
302
- outline: none;
303
- }
304
-
305
- .browse__filter input::placeholder {
306
- color: var(--lab-color-text-faint);
307
- }
308
-
309
- .browse__filter:focus-within {
310
- border-color: var(--lab-color-border-strong);
311
- border-left-color: var(--lab-color-text-strong);
312
- }
313
-
314
- .browse__group {
315
- margin-top: 3em;
316
- }
317
-
318
- .browse__group-title {
319
- font-family: var(--lab-font-mono);
320
- font-weight: 500;
321
- font-size: 1em;
322
- color: var(--lab-color-text-muted);
323
- margin-bottom: 1em;
324
- padding-bottom: 0.5em;
325
- border-bottom: 1px solid var(--lab-color-border);
326
- }
327
-
328
- .browse__folders {
329
- display: grid;
330
- grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
331
- gap: 1em;
332
- }
333
-
334
- .folder-card {
335
- display: flex;
336
- flex-direction: column;
337
- gap: 0.75em;
338
- text-decoration: none;
339
- border: 1px solid var(--lab-color-border);
340
- background-color: var(--lab-color-surface);
341
- padding: 1.1em 1.25em;
342
- transition: border-color var(--lab-transition);
343
- }
344
-
345
- .folder-card:hover,
346
- .folder-card:focus-visible {
347
- border-color: var(--lab-color-border-strong);
348
- }
349
-
350
- .folder-card__name {
351
- color: var(--lab-color-text-strong);
352
- font-weight: 500;
353
- }
354
-
355
- .folder-card__meta {
356
- display: flex;
357
- align-items: baseline;
358
- justify-content: space-between;
359
- gap: 1em;
360
- font-family: var(--lab-font-mono);
361
- font-size: 0.75em;
362
- color: var(--lab-color-text-faint);
363
- }
364
-
365
- .browse__empty {
366
- margin-top: 3em;
367
- font-family: var(--lab-font-mono);
368
- font-size: 0.85em;
369
- color: var(--lab-color-text-faint);
370
- }
371
- </style>
145
+ ))
146
+ )
147
+ }
148
+ </div>
149
+ </Shell>