@orbytes/astrolab 0.3.0 → 0.4.0-next.1
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 +109 -64
- package/defaults.mjs +65 -0
- package/dist/core/virtual-module/virtual-routes.js +12 -1
- package/docs/PIN-CONTRACT.md +8 -0
- package/docs/PIN.md +29 -19
- package/index.d.ts +40 -16
- package/index.mjs +32 -10
- package/package.json +6 -2
- package/src/Home.astro +167 -264
- package/src/LabHead.astro +37 -1047
- package/src/chrome/ComponentCard.astro +69 -0
- package/src/chrome/Icon.astro +21 -0
- package/src/chrome/LICENSE-icons +43 -0
- package/src/chrome/Nav.astro +105 -0
- package/src/chrome/Panel.astro +104 -0
- package/src/chrome/Shell.astro +106 -0
- package/src/chrome/Sprite.astro +23 -0
- package/src/chrome/StoryView.astro +251 -0
- package/src/chrome/Tree.astro +83 -0
- package/src/chrome/ViewportControls.astro +98 -0
- package/src/chrome/ViewportStage.astro +32 -0
- package/src/chrome/fonts/OFL.txt +93 -0
- package/src/chrome/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/src/chrome/icons.ts +59 -0
- package/src/chrome/marks-client.ts +102 -0
- package/src/chrome/model.ts +142 -0
- package/src/chrome/pins-data.ts +30 -0
- package/src/chrome/shell-client.ts +372 -0
- package/src/chrome/site-data.ts +230 -0
- package/src/chrome/trees.ts +152 -0
- package/src/chrome/viewport-client.ts +579 -0
- package/src/chrome/views/Assets.astro +110 -0
- package/src/chrome/views/Pages.astro +178 -0
- package/src/chrome/views/Placeholder.astro +37 -0
- package/src/chrome/views/Tasks.astro +107 -0
- package/src/core/LICENSE-astrobook +16 -0
- package/src/core/lib/components/home.astro +4 -2
- package/src/core/lib/pages/story.astro +12 -10
- package/src/core/virtual-module/virtual-routes.ts +20 -4
- package/src/pin/board.mjs +389 -175
- package/src/pin/index.mjs +33 -2
- package/src/pin/toolbar.js +1 -1
- package/src/shell/Browse.astro +106 -353
- package/src/shell/Viewport.astro +22 -1315
- package/src/shell/lab-index.ts +23 -14
- package/src/ui/components/app.astro +5 -7
- package/src/ui/components/theme-script.astro +13 -2
- package/src/ui/lab.css +2152 -370
- package/virtual.d.ts +13 -0
- package/src/shell/CardGrid.astro +0 -297
- package/src/ui/components/build-path.ts +0 -13
- package/src/ui/components/build-tree.ts +0 -108
- package/src/ui/components/collapse-duration.ts +0 -28
- package/src/ui/components/compress-terms.ts +0 -10
- package/src/ui/components/dashboard-layout.astro +0 -39
- package/src/ui/components/home.astro +0 -65
- package/src/ui/components/layout.astro +0 -110
- package/src/ui/components/sidebar-button-fullscreen.astro +0 -38
- package/src/ui/components/sidebar-button-search.astro +0 -23
- package/src/ui/components/sidebar-button-theme.astro +0 -9
- package/src/ui/components/sidebar-button.astro +0 -24
- package/src/ui/components/sidebar-resize-handle.astro +0 -74
- package/src/ui/components/sidebar-search-panel.astro +0 -41
- package/src/ui/components/sidebar-search-script.ts +0 -103
- package/src/ui/components/sidebar-title.astro +0 -17
- package/src/ui/components/sidebar-tree-node.astro +0 -143
- package/src/ui/components/sidebar-tree.astro +0 -84
- package/src/ui/components/sidebar.astro +0 -29
- package/src/ui/components/theme-toggle.astro +0 -63
package/index.mjs
CHANGED
|
@@ -25,8 +25,12 @@
|
|
|
25
25
|
// components (which render the consumer's head inside them).
|
|
26
26
|
// 2. inject the shell's routes, prerendered like every Astrobook route:
|
|
27
27
|
// <subpath>/browse/[...path] folder pages → ./src/shell/Browse.astro
|
|
28
|
-
// <subpath>/
|
|
28
|
+
// <subpath>/pages/[...path] the site's pages → ./src/chrome/views/Pages.astro
|
|
29
|
+
// <subpath>/assets the site's images → ./src/chrome/views/Assets.astro
|
|
30
|
+
// <subpath>/[section] support, settings → ./src/chrome/views/Placeholder.astro
|
|
31
|
+
// <subpath>/viewport/[...id] a redirect, kept for old links → ./src/shell/Viewport.astro
|
|
29
32
|
// <subpath>/index.json the lab index → ./src/shell/index.json.ts
|
|
33
|
+
// (the task views, <subpath>/tasks and /tasks/all, are the pin half's, dev only)
|
|
30
34
|
// and define `virtual:orbytes-lab/*` so those pages can read the resolved options, the
|
|
31
35
|
// consumer's head component and the consumer's CSS without importing anything by path.
|
|
32
36
|
// 3. serve the two mark APIs on the Vite dev server only (astro:server:setup never runs in a
|
|
@@ -117,14 +121,23 @@ const send = (res, status, payload) => {
|
|
|
117
121
|
res.end(JSON.stringify(payload));
|
|
118
122
|
};
|
|
119
123
|
|
|
124
|
+
/**
|
|
125
|
+
* What the lab and the pin half tell each other at setup. One object, created per `orbytesLab()`
|
|
126
|
+
* call and handed to both. The pin half fills `tasks` in its own `astro:config:setup` — only in dev,
|
|
127
|
+
* only when it found a repository — and the lab reads it when its config module LOADS, which Vite
|
|
128
|
+
* does on first request, after every integration's setup hook has run. So the lab's Tasks group
|
|
129
|
+
* exists exactly when the board does, and neither half decides for the other.
|
|
130
|
+
* @typedef {{ tasks: import("./defaults.mjs").LabConfig["tasks"] | { base: string; api: string; assets: string; repoRoot: string; backlogDir: string } }} Shared
|
|
131
|
+
*/
|
|
132
|
+
|
|
120
133
|
/** The virtual modules the package's own pages read. Everything in them is JSON-serialisable. */
|
|
121
|
-
const virtualModulesPlugin = (config, headEntrypoint) => ({
|
|
134
|
+
const virtualModulesPlugin = (config, headEntrypoint, shared) => ({
|
|
122
135
|
name: "orbytes-lab/virtual",
|
|
123
136
|
resolveId: (id) => RESOLVED[id],
|
|
124
137
|
load(id) {
|
|
125
138
|
switch (id) {
|
|
126
139
|
case RESOLVED[VIRTUAL.config]:
|
|
127
|
-
return `export default ${JSON.stringify(config)};`;
|
|
140
|
+
return `export default ${JSON.stringify({ ...config, tasks: shared.tasks ?? null })};`;
|
|
128
141
|
case RESOLVED[VIRTUAL.head]:
|
|
129
142
|
return `export { default } from ${JSON.stringify(headEntrypoint)};`;
|
|
130
143
|
case RESOLVED[VIRTUAL.css]:
|
|
@@ -133,8 +146,8 @@ const virtualModulesPlugin = (config, headEntrypoint) => ({
|
|
|
133
146
|
},
|
|
134
147
|
});
|
|
135
148
|
|
|
136
|
-
/** @param {Record<string, unknown>} [options] */
|
|
137
|
-
function labShell(options) {
|
|
149
|
+
/** @param {Record<string, unknown>} [options] @param {Shared} [shared] */
|
|
150
|
+
function labShell(options, shared = { tasks: null }) {
|
|
138
151
|
const config = resolveLabOptions(options);
|
|
139
152
|
let rootDir = process.cwd();
|
|
140
153
|
return {
|
|
@@ -153,6 +166,9 @@ function labShell(options) {
|
|
|
153
166
|
|
|
154
167
|
for (const [pattern, entrypoint] of [
|
|
155
168
|
[`${config.subpath}/browse/[...path]`, "./src/shell/Browse.astro"],
|
|
169
|
+
[`${config.subpath}/pages/[...path]`, "./src/chrome/views/Pages.astro"],
|
|
170
|
+
[`${config.subpath}/assets`, "./src/chrome/views/Assets.astro"],
|
|
171
|
+
[`${config.subpath}/[section]`, "./src/chrome/views/Placeholder.astro"],
|
|
156
172
|
[`${config.subpath}/viewport/[...id]`, "./src/shell/Viewport.astro"],
|
|
157
173
|
[`${config.subpath}/index.json`, "./src/shell/index.json.ts"],
|
|
158
174
|
]) {
|
|
@@ -161,7 +177,7 @@ function labShell(options) {
|
|
|
161
177
|
|
|
162
178
|
updateConfig({
|
|
163
179
|
vite: {
|
|
164
|
-
plugins: [virtualModulesPlugin(config, head)],
|
|
180
|
+
plugins: [virtualModulesPlugin(config, head, shared)],
|
|
165
181
|
// This package ships SOURCE — .astro and .ts compiled by the consumer's Vite, the way
|
|
166
182
|
// Starlight ships its components. Vite externalises node_modules for SSR by default,
|
|
167
183
|
// and an externalised .astro file is handed to Node, which cannot read it. The
|
|
@@ -176,7 +192,7 @@ function labShell(options) {
|
|
|
176
192
|
},
|
|
177
193
|
});
|
|
178
194
|
logger.info(
|
|
179
|
-
`
|
|
195
|
+
`pages at ${config.subpath}/pages, folders at ${config.subpath}/browse, assets at ${config.subpath}/assets, index at ${config.subpath}/index.json`,
|
|
180
196
|
);
|
|
181
197
|
},
|
|
182
198
|
"astro:server:setup": ({ server, logger }) => {
|
|
@@ -277,7 +293,7 @@ function labShell(options) {
|
|
|
277
293
|
* @param {Record<string, unknown>} [options] the lab's options
|
|
278
294
|
* @param {import("./defaults.mjs").LabConfig} config the RESOLVED lab config
|
|
279
295
|
*/
|
|
280
|
-
const pinHalf = (options, config) => {
|
|
296
|
+
const pinHalf = (options, config, shared) => {
|
|
281
297
|
const pin = options?.pin;
|
|
282
298
|
if (pin === false || pin === null) return [];
|
|
283
299
|
const given = typeof pin === "object" ? pin : {};
|
|
@@ -289,7 +305,11 @@ const pinHalf = (options, config) => {
|
|
|
289
305
|
{ href: "/", label: "Site" },
|
|
290
306
|
{ href: config.subpath || "/", label: "Lab" },
|
|
291
307
|
],
|
|
308
|
+
// ONE URL (decided 2026-09-24): the board lives under the lab, at <subpath>/tasks, drawn in
|
|
309
|
+
// the lab's own chrome. `/pin` still answers, with a redirect. A consumer's `route` wins.
|
|
310
|
+
route: `${config.subpath}/tasks`,
|
|
292
311
|
...given,
|
|
312
|
+
lab: { shared },
|
|
293
313
|
}),
|
|
294
314
|
];
|
|
295
315
|
};
|
|
@@ -302,6 +322,8 @@ const pinHalf = (options, config) => {
|
|
|
302
322
|
*/
|
|
303
323
|
export default function orbytesLab(options = {}) {
|
|
304
324
|
const config = resolveLabOptions(options);
|
|
325
|
+
/** @type {Shared} */
|
|
326
|
+
const shared = { tasks: null };
|
|
305
327
|
return [
|
|
306
328
|
createAstrobookIntegration({
|
|
307
329
|
directory: config.directory,
|
|
@@ -313,8 +335,8 @@ export default function orbytesLab(options = {}) {
|
|
|
313
335
|
head: "@orbytes/astrolab/head",
|
|
314
336
|
home: "@orbytes/astrolab/home",
|
|
315
337
|
}),
|
|
316
|
-
labShell(options),
|
|
317
|
-
...pinHalf(options, config),
|
|
338
|
+
labShell(options, shared),
|
|
339
|
+
...pinHalf(options, config, shared),
|
|
318
340
|
];
|
|
319
341
|
}
|
|
320
342
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbytes/astrolab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-next.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "The orbytes
|
|
5
|
+
"description": "The orbytes lab for Astro sites — every page, section and component rendered live, which of them the site actually mounts, a viewport canvas to size them on, and a dev-only click-to-ticket pin board — in one integration, all local.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"astro",
|
|
8
8
|
"astro-integration",
|
|
@@ -70,6 +70,10 @@
|
|
|
70
70
|
"src",
|
|
71
71
|
"virtual.d.ts"
|
|
72
72
|
],
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public",
|
|
75
|
+
"tag": "next"
|
|
76
|
+
},
|
|
73
77
|
"dependencies": {
|
|
74
78
|
"@sveltejs/acorn-typescript": "^1.0.12",
|
|
75
79
|
"acorn": "^8.18.0",
|
package/src/Home.astro
CHANGED
|
@@ -1,47 +1,69 @@
|
|
|
1
1
|
---
|
|
2
|
-
// Lab home — Astrobook's `home` component, given to it by the integration (../index.mjs)
|
|
3
|
-
//
|
|
4
|
-
// at <subpath>/browse/<tier>), then every story as a live thumbnail card grouped by tier, each
|
|
5
|
-
// card carrying its derived pill — live slot, used-by, or nothing.
|
|
2
|
+
// Lab home — Astrobook's `home` component, given to it by the integration (../index.mjs) and
|
|
3
|
+
// rendered as the whole `/lab` document (./ui/components/app.astro).
|
|
6
4
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// A summary, not a gallery (redesigned 2026-09-24). The previous home drew every story in the lab
|
|
6
|
+
// as a live <iframe> thumbnail — forty-odd full renders of the site, each loading its fonts and
|
|
7
|
+
// stylesheets, before you could click anything. Those cards now live one tier at a time on the
|
|
8
|
+
// tier pages, lazily; the home page is the counts and the one thing only this lab can tell you —
|
|
9
|
+
// what is mounted on which page, in which slot, right now.
|
|
11
10
|
import { execSync } from "node:child_process";
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
sortTiers,
|
|
19
|
-
STAGING_URL,
|
|
20
|
-
tierLabel,
|
|
21
|
-
} from "./shell/lab-index";
|
|
11
|
+
import Shell from "./chrome/Shell.astro";
|
|
12
|
+
import Icon from "./chrome/Icon.astro";
|
|
13
|
+
import { hrefs, labConfig, tasksBase } from "./chrome/model";
|
|
14
|
+
import { componentsOf } from "./chrome/trees";
|
|
15
|
+
import { rootDir, siteAssetsModel, sitePagesModel } from "./chrome/site-data";
|
|
16
|
+
import { buildLabIndex, STAGING_URL, sortTiers, tierLabel } from "./shell/lab-index";
|
|
22
17
|
|
|
23
18
|
const index = buildLabIndex();
|
|
24
|
-
const
|
|
25
|
-
|
|
19
|
+
const components = componentsOf(index);
|
|
20
|
+
const pages = sitePagesModel();
|
|
21
|
+
const assets = siteAssetsModel();
|
|
22
|
+
|
|
23
|
+
const tiers = sortTiers([...new Set(components.map((c) => c.tier))]).map((tier) => {
|
|
24
|
+
const mods = components.filter((c) => c.tier === tier);
|
|
26
25
|
return {
|
|
27
26
|
tier,
|
|
28
27
|
label: tierLabel(tier),
|
|
29
|
-
href: tier
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
href: hrefs.tier(tier),
|
|
29
|
+
components: mods.length,
|
|
30
|
+
stories: mods.reduce((n, m) => n + m.stories.length, 0),
|
|
31
|
+
live: mods.filter((m) => m.live).length,
|
|
32
|
+
used: mods.filter((m) => !m.live && m.usedBy.length).length,
|
|
33
|
+
unused: mods.filter((m) => !m.live && !m.usedBy.length).length,
|
|
33
34
|
};
|
|
34
35
|
});
|
|
35
36
|
|
|
37
|
+
// The pin board's counts, in dev only (tasksBase is null in every build).
|
|
38
|
+
let tasks: { open: number; review: number } | null = null;
|
|
39
|
+
if (tasksBase && labConfig.tasks) {
|
|
40
|
+
const { collectTickets, OPEN_STATUSES, READY_FOR_REVIEW } = await import("./pin/board.mjs");
|
|
41
|
+
try {
|
|
42
|
+
const { tickets } = collectTickets(labConfig.tasks.repoRoot, { backlogDir: labConfig.tasks.backlogDir });
|
|
43
|
+
tasks = {
|
|
44
|
+
open: tickets.filter((t: { status: string }) => OPEN_STATUSES.includes(t.status)).length,
|
|
45
|
+
review: tickets.filter((t: { status: string }) => t.status === READY_FOR_REVIEW).length,
|
|
46
|
+
};
|
|
47
|
+
} catch {
|
|
48
|
+
tasks = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// What is on the site: every page, and in slot order what it mounts, with the lab component it is.
|
|
53
|
+
const byComponentFile = new Map(components.filter((c) => c.componentFile).map((c) => [c.componentFile!, c]));
|
|
54
|
+
const onSite = pages
|
|
55
|
+
.filter((page) => page.mounts.length > 0)
|
|
56
|
+
.map((page) => ({
|
|
57
|
+
page,
|
|
58
|
+
slots: page.mounts.map((file, i) => ({ slot: i + 1, file, component: byComponentFile.get(file) ?? null })),
|
|
59
|
+
}));
|
|
60
|
+
|
|
36
61
|
// Local branches → per-branch preview builds, when the consumer configured a URL template.
|
|
37
|
-
// `previewUrlTemplate` is a string with a {branch} placeholder rather than a function because the
|
|
38
|
-
// config crosses a virtual module and has to stay JSON-serialisable. No template, no branch list:
|
|
39
|
-
// a host that does not build branches has nothing to link to.
|
|
40
62
|
const previewTemplate = labConfig.previewUrlTemplate;
|
|
41
63
|
let branches: string[] = [];
|
|
42
64
|
if (previewTemplate) {
|
|
43
65
|
try {
|
|
44
|
-
branches = execSync('git branch --format="%(refname:short)"', { encoding: "utf8" })
|
|
66
|
+
branches = execSync('git branch --format="%(refname:short)"', { encoding: "utf8", cwd: rootDir })
|
|
45
67
|
.split("\n")
|
|
46
68
|
.map((line) => line.trim())
|
|
47
69
|
.filter(Boolean);
|
|
@@ -52,247 +74,128 @@ if (previewTemplate) {
|
|
|
52
74
|
const previewAlias = (branch: string) =>
|
|
53
75
|
(previewTemplate ?? "").replace(
|
|
54
76
|
/\{branch\}/g,
|
|
55
|
-
branch
|
|
56
|
-
.toLowerCase()
|
|
57
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
58
|
-
.replace(/^-+|-+$/g, ""),
|
|
77
|
+
branch.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""),
|
|
59
78
|
);
|
|
79
|
+
const unusedAssets = assets.filter((a) => a.usedBy.length === 0).length;
|
|
60
80
|
---
|
|
61
81
|
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
<Shell title="Home" active="home">
|
|
83
|
+
<Fragment slot="navbar">
|
|
84
|
+
<div class="lab-navbar__id">
|
|
85
|
+
<h1 class="lab-navbar__title">Home <small>{labConfig.title}</small></h1>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="lab-navbar__tools">
|
|
88
|
+
{
|
|
89
|
+
STAGING_URL && (
|
|
90
|
+
<a class="lab-btn lab-btn--sm" href={STAGING_URL} target="_blank" rel="noopener">
|
|
91
|
+
<Icon name="external" />
|
|
92
|
+
Staging
|
|
93
|
+
</a>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
</div>
|
|
97
|
+
</Fragment>
|
|
98
|
+
|
|
99
|
+
<div class="lab-page">
|
|
100
|
+
<header class="lab-page__head">
|
|
101
|
+
<h2 class="lab-page__title">What is on this site</h2>
|
|
102
|
+
<p class="lab-page__lede">
|
|
103
|
+
Every page, section and component in the build, rendered live — and which of them the site
|
|
104
|
+
actually mounts right now, read from <code>src/pages/</code> rather than declared.
|
|
105
|
+
</p>
|
|
106
|
+
</header>
|
|
107
|
+
|
|
108
|
+
<div class="lab-stats">
|
|
109
|
+
<a class="lab-stat" href={hrefs.pages}>
|
|
110
|
+
<span class="lab-stat__head"><Icon name="pages" />Pages</span>
|
|
111
|
+
<span class="lab-stat__value">{pages.length}</span>
|
|
112
|
+
<span class="lab-stat__foot">
|
|
113
|
+
<span class="lab-pill">{onSite.length} with components</span>
|
|
114
|
+
{pages.some((p) => p.dynamic) && <span class="lab-pill">{pages.filter((p) => p.dynamic).length} dynamic</span>}
|
|
115
|
+
</span>
|
|
116
|
+
</a>
|
|
117
|
+
{
|
|
118
|
+
tiers.map((tier) => (
|
|
119
|
+
<a class="lab-stat" href={tier.href}>
|
|
120
|
+
<span class="lab-stat__head">
|
|
121
|
+
<Icon name={tier.tier === labConfig.sectionsTier ? "sections" : tier.tier === "components" ? "components" : "component"} />
|
|
122
|
+
{tier.label}
|
|
123
|
+
</span>
|
|
124
|
+
<span class="lab-stat__value">{tier.components}</span>
|
|
125
|
+
<span class="lab-stat__foot">
|
|
126
|
+
{tier.live > 0 && <span class="lab-pill lab-pill--live">{tier.live} live</span>}
|
|
127
|
+
{tier.used > 0 && <span class="lab-pill lab-pill--used">{tier.used} used</span>}
|
|
128
|
+
{tier.unused > 0 && <span class="lab-pill">{tier.unused} unused</span>}
|
|
129
|
+
<span class="lab-pill">{tier.stories} stories</span>
|
|
130
|
+
</span>
|
|
131
|
+
</a>
|
|
132
|
+
))
|
|
133
|
+
}
|
|
134
|
+
<a class="lab-stat" href={hrefs.assets}>
|
|
135
|
+
<span class="lab-stat__head"><Icon name="assets" />Assets</span>
|
|
136
|
+
<span class="lab-stat__value">{assets.length}</span>
|
|
137
|
+
<span class="lab-stat__foot">
|
|
138
|
+
{unusedAssets > 0 ? <span class="lab-pill lab-pill--unresponsive">{unusedAssets} unused</span> : <span class="lab-pill">all referenced</span>}
|
|
139
|
+
</span>
|
|
140
|
+
</a>
|
|
141
|
+
{
|
|
142
|
+
tasks && tasksBase && (
|
|
143
|
+
<a class="lab-stat" href={tasksBase}>
|
|
144
|
+
<span class="lab-stat__head"><Icon name="tasks" />Tasks</span>
|
|
145
|
+
<span class="lab-stat__value">{tasks.open}</span>
|
|
146
|
+
<span class="lab-stat__foot">
|
|
147
|
+
{tasks.review > 0 ? <span class="lab-pill lab-pill--brand">{tasks.review} ready for review</span> : <span class="lab-pill">open</span>}
|
|
148
|
+
</span>
|
|
149
|
+
</a>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
</div>
|
|
84
153
|
|
|
85
|
-
<section class="lab-home__tiers">
|
|
86
154
|
{
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
155
|
+
onSite.map(({ page, slots }) => (
|
|
156
|
+
<section>
|
|
157
|
+
<h3 class="lab-section-title">
|
|
158
|
+
<a href={page.href}>{page.label}</a>
|
|
159
|
+
<small>{page.route} · {slots.length} mounted</small>
|
|
160
|
+
</h3>
|
|
161
|
+
<ol class="lab-slots">
|
|
162
|
+
{slots.map(({ slot, file, component }) => (
|
|
163
|
+
<li class="lab-slots__row">
|
|
164
|
+
<span class="lab-slots__n">{slot}</span>
|
|
165
|
+
{component ? (
|
|
166
|
+
<a class="lab-slots__name" href={component.stories[0]!.dashboardUrl}>
|
|
167
|
+
{component.moduleName}
|
|
168
|
+
{component.version && <span class="lab-card__version">{component.version}</span>}
|
|
169
|
+
</a>
|
|
170
|
+
) : (
|
|
171
|
+
<span class="lab-slots__name lab-slots__name--bare" title="Not in the lab — no stories file for it">
|
|
172
|
+
{file.split("/").pop()}
|
|
173
|
+
</span>
|
|
174
|
+
)}
|
|
175
|
+
<span class="lab-slots__file lab-mono">{file}</span>
|
|
176
|
+
</li>
|
|
177
|
+
))}
|
|
178
|
+
</ol>
|
|
179
|
+
</section>
|
|
98
180
|
))
|
|
99
181
|
}
|
|
100
|
-
</section>
|
|
101
|
-
|
|
102
|
-
{
|
|
103
|
-
branches.length > 0 && (
|
|
104
|
-
<section class="lab-home__branches">
|
|
105
|
-
<h2 class="lab-home__group-title">Branches</h2>
|
|
106
|
-
<ul>
|
|
107
|
-
{branches.map((branch) => (
|
|
108
|
-
<li>
|
|
109
|
-
<code>{branch}</code>
|
|
110
|
-
<a href={previewAlias(branch)} target="_blank" rel="noopener">
|
|
111
|
-
branch preview
|
|
112
|
-
</a>
|
|
113
|
-
</li>
|
|
114
|
-
))}
|
|
115
|
-
</ul>
|
|
116
|
-
</section>
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
{
|
|
121
|
-
tiers.map((tier) => (
|
|
122
|
-
<section class="lab-home__group">
|
|
123
|
-
<h2 class="lab-home__group-title">
|
|
124
|
-
<a href={tier.href}>{tier.label}</a>
|
|
125
|
-
</h2>
|
|
126
|
-
<CardGrid items={tier.items} />
|
|
127
|
-
</section>
|
|
128
|
-
))
|
|
129
|
-
}
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<style>
|
|
133
|
-
.lab-home {
|
|
134
|
-
background-color: var(--lab-color-bg);
|
|
135
|
-
color: var(--lab-color-text);
|
|
136
|
-
font-family: var(--lab-font-body);
|
|
137
|
-
min-height: 100%;
|
|
138
|
-
padding: 3em 2.5em 5em;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.lab-home__eyebrow {
|
|
142
|
-
font-family: var(--lab-font-mono);
|
|
143
|
-
font-weight: 500;
|
|
144
|
-
color: var(--lab-color-text-muted);
|
|
145
|
-
margin-bottom: 0.75em;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.lab-home__title {
|
|
149
|
-
font-family: var(--lab-font-heading);
|
|
150
|
-
color: var(--lab-color-text-strong);
|
|
151
|
-
font-size: 2.25em;
|
|
152
|
-
font-weight: 500;
|
|
153
|
-
letter-spacing: -0.5px;
|
|
154
|
-
margin-bottom: 0.4em;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.lab-home__intro {
|
|
158
|
-
max-width: 42em;
|
|
159
|
-
color: var(--lab-color-text-faint);
|
|
160
|
-
line-height: 1.55;
|
|
161
|
-
margin-bottom: 1.5em;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.lab-home__staging {
|
|
165
|
-
display: inline-block;
|
|
166
|
-
font-family: var(--lab-font-mono);
|
|
167
|
-
font-weight: 500;
|
|
168
|
-
color: var(--lab-color-text-strong);
|
|
169
|
-
text-decoration: none;
|
|
170
|
-
border: 1px solid var(--lab-color-border);
|
|
171
|
-
border-left: 1px solid var(--lab-color-text-strong);
|
|
172
|
-
padding: 0.75em 1.25em;
|
|
173
|
-
transition: background-color var(--lab-transition), color var(--lab-transition);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.lab-home__staging:hover,
|
|
177
|
-
.lab-home__staging:focus-visible {
|
|
178
|
-
background-color: var(--lab-color-text-strong);
|
|
179
|
-
color: var(--lab-color-accent-text);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
.lab-home__tiers {
|
|
183
|
-
display: grid;
|
|
184
|
-
grid-template-columns: repeat(auto-fit, minmax(14em, 1fr));
|
|
185
|
-
gap: 1em;
|
|
186
|
-
margin-top: 2.5em;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.tier-card {
|
|
190
|
-
display: flex;
|
|
191
|
-
flex-direction: column;
|
|
192
|
-
gap: 0.5em;
|
|
193
|
-
text-decoration: none;
|
|
194
|
-
border: 1px solid var(--lab-color-border);
|
|
195
|
-
background-color: var(--lab-color-surface);
|
|
196
|
-
padding: 1.25em 1.5em;
|
|
197
|
-
transition: border-color var(--lab-transition);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
.tier-card:hover,
|
|
201
|
-
.tier-card:focus-visible {
|
|
202
|
-
border-color: var(--lab-color-border-strong);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.tier-card__eyebrow {
|
|
206
|
-
font-family: var(--lab-font-mono);
|
|
207
|
-
font-weight: 500;
|
|
208
|
-
font-size: 0.75em;
|
|
209
|
-
color: var(--lab-color-text-faint);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.tier-card__name {
|
|
213
|
-
font-family: var(--lab-font-heading);
|
|
214
|
-
font-size: 1.4em;
|
|
215
|
-
font-weight: 500;
|
|
216
|
-
color: var(--lab-color-text-strong);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.tier-card__meta {
|
|
220
|
-
display: flex;
|
|
221
|
-
align-items: baseline;
|
|
222
|
-
justify-content: space-between;
|
|
223
|
-
gap: 1em;
|
|
224
|
-
font-family: var(--lab-font-mono);
|
|
225
|
-
font-size: 0.8em;
|
|
226
|
-
color: var(--lab-color-text-faint);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
.tier-card__live {
|
|
230
|
-
font-weight: 500;
|
|
231
|
-
line-height: 1;
|
|
232
|
-
padding: 0.45em 0.85em;
|
|
233
|
-
border-radius: var(--lab-radius-pill);
|
|
234
|
-
background-color: var(--lab-color-accent);
|
|
235
|
-
color: var(--lab-color-accent-text);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
.lab-home__branches {
|
|
239
|
-
margin-top: 2.5em;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
.lab-home__branches ul {
|
|
243
|
-
list-style: none;
|
|
244
|
-
padding: 0;
|
|
245
|
-
margin: 0;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
.lab-home__branches li {
|
|
249
|
-
display: flex;
|
|
250
|
-
align-items: baseline;
|
|
251
|
-
gap: 1em;
|
|
252
|
-
padding: 0.5em 0;
|
|
253
|
-
border-bottom: 1px solid var(--lab-color-border);
|
|
254
|
-
}
|
|
255
182
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
margin-top: 3em;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
.lab-home__group-title {
|
|
279
|
-
font-family: var(--lab-font-mono);
|
|
280
|
-
font-weight: 500;
|
|
281
|
-
font-size: 1em;
|
|
282
|
-
color: var(--lab-color-text-muted);
|
|
283
|
-
margin-bottom: 1em;
|
|
284
|
-
padding-bottom: 0.5em;
|
|
285
|
-
border-bottom: 1px solid var(--lab-color-border);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
.lab-home__group-title a {
|
|
289
|
-
color: inherit;
|
|
290
|
-
text-decoration: none;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
.lab-home__group-title a:hover,
|
|
294
|
-
.lab-home__group-title a:focus-visible {
|
|
295
|
-
color: var(--lab-color-text-strong);
|
|
296
|
-
text-decoration: underline;
|
|
297
|
-
}
|
|
298
|
-
</style>
|
|
183
|
+
{
|
|
184
|
+
branches.length > 0 && (
|
|
185
|
+
<section>
|
|
186
|
+
<h3 class="lab-section-title">Branches</h3>
|
|
187
|
+
<ol class="lab-slots">
|
|
188
|
+
{branches.map((branch) => (
|
|
189
|
+
<li class="lab-slots__row">
|
|
190
|
+
<span class="lab-slots__name lab-mono">{branch}</span>
|
|
191
|
+
<a class="lab-slots__file" href={previewAlias(branch)} target="_blank" rel="noopener">
|
|
192
|
+
branch preview ↗
|
|
193
|
+
</a>
|
|
194
|
+
</li>
|
|
195
|
+
))}
|
|
196
|
+
</ol>
|
|
197
|
+
</section>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
</div>
|
|
201
|
+
</Shell>
|