@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
package/index.mjs CHANGED
@@ -11,9 +11,10 @@
11
11
  //
12
12
  // ── the pin board (./src/pin/, moved in 2026-09-22) ─────────────────────────────────────────────
13
13
  // Decided 2026-09-22: the lab and the pin board are ONE app, bundled and working as one — not two
14
- // packages a site installs separately. So `/lab` and `/pin` come from one call, one install, one
15
- // dev server. `pin: false` omits it entirely — a site may want the lab alone — and `pin: {…}` passes
16
- // options straight through (ops/standalone-plan.md › The pin board merges in; docs/PIN.md).
14
+ // packages a site installs separately. So the lab (`<subpath>`) and its Tasks board
15
+ // (`<subpath>/tasks`) come from one call, one install, one dev server. `pin: false` omits it
16
+ // entirely — a site may want the lab alone — and `pin: {…}` passes options straight through
17
+ // (ops/standalone-plan.md › The pin board merges in; docs/PIN.md).
17
18
  //
18
19
  // The pin half is DEV ONLY and gates itself four times over; it is registered in staging builds
19
20
  // along with the lab and does nothing there, because its first assertion is `command === "dev"`.
@@ -25,22 +26,21 @@
25
26
  // components (which render the consumer's head inside them).
26
27
  // 2. inject the shell's routes, prerendered like every Astrobook route:
27
28
  // <subpath>/browse/[...path] folder pages → ./src/shell/Browse.astro
28
- // <subpath>/viewport/[...id] viewport configurator → ./src/shell/Viewport.astro
29
+ // <subpath>/pages/[...path] the site's pages → ./src/chrome/views/Pages.astro
30
+ // <subpath>/assets the site's images → ./src/chrome/views/Assets.astro
31
+ // <subpath>/[section] support, settings → ./src/chrome/views/Placeholder.astro
32
+ // <subpath>/viewport/[...id] a redirect, kept for old links → ./src/shell/Viewport.astro
29
33
  // <subpath>/index.json the lab index → ./src/shell/index.json.ts
34
+ // (the task views, <subpath>/tasks and /tasks/all, are the pin half's, dev only)
30
35
  // and define `virtual:orbytes-lab/*` so those pages can read the resolved options, the
31
36
  // consumer's head component and the consumer's CSS without importing anything by path.
32
- // 3. serve the two mark APIs on the Vite dev server only (astro:server:setup never runs in a
33
- // build, so a deployed lab has no write path and the switches hide themselves):
34
- // GET /__lab/cull { marked: string[], updated: string | null }
35
- // PUT /__lab/cull body { marked: string[] } — every entry must be an existing
36
- // stories file in the CULLABLE tier AND not live on any page;
37
- // otherwise 400 with the offenders. Writes <directory>/cull.json.
37
+ // 3. serve the responsive marks API on the Vite dev server only (astro:server:setup never runs
38
+ // in a build, so a deployed lab has no write path and the switches hide themselves):
38
39
  // GET /__lab/responsive { done: string[], approved: string[], updated: string | null }
39
40
  // PUT /__lab/responsive body { done: string[], approved?: string[] } — every entry must
40
41
  // be an existing stories file in the RESPONSIVE tier; otherwise 400
41
42
  // with the offenders. Writes <directory>/responsive.json.
42
- // Marks only. NOTHING HERE DELETES A FILE — that is `orbytes-lab-cull`, a separate command,
43
- // run by hand and with a dry run first.
43
+ // Marks only. NOTHING HERE DELETES A FILE.
44
44
  //
45
45
  // Gotcha (2026-09-06): Astro re-imports astro.config.mjs on a config change, but this module and
46
46
  // its ./src/shell/*.mjs imports stay in Node's ESM cache for the life of the process — so an edit
@@ -55,15 +55,7 @@ import { fileURLToPath } from "node:url";
55
55
  import { createAstrobookIntegration } from "./dist/core/index.js";
56
56
  import { resolveLabOptions } from "./defaults.mjs";
57
57
  import orbytesPin from "./src/pin/index.mjs";
58
- import { defaultImports, liveComponentFiles, storyComponentFile } from "./src/shell/live-files.mjs";
59
- import {
60
- cullOffence,
61
- readCull,
62
- readResponsive,
63
- responsiveOffence,
64
- writeCull,
65
- writeResponsive,
66
- } from "./src/shell/marks.mjs";
58
+ import { readResponsive, responsiveOffence, writeResponsive } from "./src/shell/marks.mjs";
67
59
 
68
60
  const file = (relative) => fileURLToPath(new URL(relative, import.meta.url));
69
61
 
@@ -80,22 +72,6 @@ const RESOLVED = {
80
72
  [VIRTUAL.css]: "__virtual_orbytes_lab_user_css__.mjs",
81
73
  };
82
74
 
83
- /**
84
- * Why an entry may not be marked for deletion, or null when it may. `importedByLive` maps a
85
- * component file to the live section that imports it, so an exploration a live section uses is
86
- * refused here with the same reason the cull script would give later.
87
- */
88
- const cullEntryOffence = (rootDir, config, entry, liveByFile, importedByLive) => {
89
- const bad = cullOffence(rootDir, config, entry);
90
- if (bad) return bad;
91
- const component = storyComponentFile(rootDir, entry);
92
- const mount = component ? liveByFile.get(component) : undefined;
93
- if (mount) return `live on ${mount.page} (slot ${mount.slot}: ${component})`;
94
- const user = component ? importedByLive.get(component) : undefined;
95
- if (user) return `used by live section ${user}`;
96
- return null;
97
- };
98
-
99
75
  const readBody = (req) =>
100
76
  new Promise((resolve, reject) => {
101
77
  let body = "";
@@ -117,14 +93,23 @@ const send = (res, status, payload) => {
117
93
  res.end(JSON.stringify(payload));
118
94
  };
119
95
 
96
+ /**
97
+ * What the lab and the pin half tell each other at setup. One object, created per `orbytesLab()`
98
+ * call and handed to both. The pin half fills `tasks` in its own `astro:config:setup` — only in dev,
99
+ * only when it found a repository — and the lab reads it when its config module LOADS, which Vite
100
+ * does on first request, after every integration's setup hook has run. So the lab's Tasks group
101
+ * exists exactly when the board does, and neither half decides for the other.
102
+ * @typedef {{ tasks: import("./defaults.mjs").LabConfig["tasks"] | { base: string; api: string; assets: string; repoRoot: string; backlogDir: string } }} Shared
103
+ */
104
+
120
105
  /** The virtual modules the package's own pages read. Everything in them is JSON-serialisable. */
121
- const virtualModulesPlugin = (config, headEntrypoint) => ({
106
+ const virtualModulesPlugin = (config, headEntrypoint, shared) => ({
122
107
  name: "orbytes-lab/virtual",
123
108
  resolveId: (id) => RESOLVED[id],
124
109
  load(id) {
125
110
  switch (id) {
126
111
  case RESOLVED[VIRTUAL.config]:
127
- return `export default ${JSON.stringify(config)};`;
112
+ return `export default ${JSON.stringify({ ...config, tasks: shared.tasks ?? null })};`;
128
113
  case RESOLVED[VIRTUAL.head]:
129
114
  return `export { default } from ${JSON.stringify(headEntrypoint)};`;
130
115
  case RESOLVED[VIRTUAL.css]:
@@ -133,8 +118,8 @@ const virtualModulesPlugin = (config, headEntrypoint) => ({
133
118
  },
134
119
  });
135
120
 
136
- /** @param {Record<string, unknown>} [options] */
137
- function labShell(options) {
121
+ /** @param {Record<string, unknown>} [options] @param {Shared} [shared] */
122
+ function labShell(options, shared = { tasks: null }) {
138
123
  const config = resolveLabOptions(options);
139
124
  let rootDir = process.cwd();
140
125
  return {
@@ -153,6 +138,9 @@ function labShell(options) {
153
138
 
154
139
  for (const [pattern, entrypoint] of [
155
140
  [`${config.subpath}/browse/[...path]`, "./src/shell/Browse.astro"],
141
+ [`${config.subpath}/pages/[...path]`, "./src/chrome/views/Pages.astro"],
142
+ [`${config.subpath}/assets`, "./src/chrome/views/Assets.astro"],
143
+ [`${config.subpath}/[section]`, "./src/chrome/views/Placeholder.astro"],
156
144
  [`${config.subpath}/viewport/[...id]`, "./src/shell/Viewport.astro"],
157
145
  [`${config.subpath}/index.json`, "./src/shell/index.json.ts"],
158
146
  ]) {
@@ -161,66 +149,27 @@ function labShell(options) {
161
149
 
162
150
  updateConfig({
163
151
  vite: {
164
- plugins: [virtualModulesPlugin(config, head)],
152
+ plugins: [virtualModulesPlugin(config, head, shared)],
165
153
  // This package ships SOURCE — .astro and .ts compiled by the consumer's Vite, the way
166
154
  // Starlight ships its components. Vite externalises node_modules for SSR by default,
167
155
  // and an externalised .astro file is handed to Node, which cannot read it. The
168
156
  // consumer should need one line in its config and nothing more, so the exemption is
169
157
  // declared here rather than there.
170
158
  ssr: { noExternal: ["@orbytes/astrolab"] },
171
- // Marking writes JSON files under src/; keep Vite from treating those as source
172
- // changes and reloading the lab mid-triage.
159
+ // Marking writes a JSON file under src/; keep Vite from treating it as a source
160
+ // change and reloading the lab mid-triage.
173
161
  server: {
174
- watch: { ignored: [`**/${config.cullFile}`, `**/${config.responsiveFile}`] },
162
+ watch: { ignored: [`**/${config.responsiveFile}`] },
175
163
  },
176
164
  },
177
165
  });
178
166
  logger.info(
179
- `folder pages at ${config.subpath}/browse, viewport at ${config.subpath}/viewport, index at ${config.subpath}/index.json`,
167
+ `pages at ${config.subpath}/pages, folders at ${config.subpath}/browse, assets at ${config.subpath}/assets, index at ${config.subpath}/index.json`,
180
168
  );
181
169
  },
182
170
  "astro:server:setup": ({ server, logger }) => {
183
- server.middlewares.use(async (req, res, next) => {
184
- const url = new URL(req.url ?? "/", "http://x");
185
- if (url.pathname !== "/__lab/cull") return next();
186
- try {
187
- if (req.method === "GET") return send(res, 200, readCull(rootDir, config));
188
- if (req.method === "PUT") {
189
- const body = await readBody(req);
190
- if (!Array.isArray(body.marked))
191
- return send(res, 400, { error: "body must be { marked: string[] }" });
192
- const live = liveComponentFiles(rootDir);
193
- // First mount wins, so the refusal names the page a reader would look at first.
194
- const liveByFile = new Map();
195
- for (const l of live) if (!liveByFile.has(l.file)) liveByFile.set(l.file, l);
196
- const importedByLive = new Map();
197
- for (const l of live) {
198
- for (const dep of defaultImports(rootDir, l.file)) {
199
- if (!importedByLive.has(dep.file)) importedByLive.set(dep.file, l.file);
200
- }
201
- }
202
- const offenders = body.marked
203
- .map((entry) => ({
204
- path: entry,
205
- reason: cullEntryOffence(rootDir, config, entry, liveByFile, importedByLive),
206
- }))
207
- .filter((o) => o.reason !== null);
208
- if (offenders.length > 0) {
209
- return send(res, 400, { error: "some entries cannot be marked", offenders });
210
- }
211
- const data = writeCull(rootDir, config, body.marked);
212
- logger.info(`cull marks saved: ${data.marked.length} → ${config.cullFile}`);
213
- return send(res, 200, { ok: true, file: config.cullFile, ...data });
214
- }
215
- return send(res, 405, { error: "GET or PUT" });
216
- } catch (e) {
217
- return send(res, 500, { error: String(e?.message ?? e) });
218
- }
219
- });
220
-
221
- // The responsive marks — same shape of API as the cull one above, and deliberately its
222
- // sibling rather than a second mechanism: a section version is marked responsive (and,
223
- // separately, approved for that work) by hand, because neither fact is in the code.
171
+ // The responsive marks: a section version is marked responsive (and, separately, approved
172
+ // for that work) by hand, because neither fact is in the code.
224
173
  server.middlewares.use(async (req, res, next) => {
225
174
  const url = new URL(req.url ?? "/", "http://x");
226
175
  if (url.pathname !== "/__lab/responsive") return next();
@@ -253,7 +202,7 @@ function labShell(options) {
253
202
  }
254
203
  });
255
204
 
256
- logger.info("mark APIs at /__lab/cull and /__lab/responsive (dev only)");
205
+ logger.info("mark API at /__lab/responsive (dev only)");
257
206
  },
258
207
  },
259
208
  };
@@ -277,7 +226,7 @@ function labShell(options) {
277
226
  * @param {Record<string, unknown>} [options] the lab's options
278
227
  * @param {import("./defaults.mjs").LabConfig} config the RESOLVED lab config
279
228
  */
280
- const pinHalf = (options, config) => {
229
+ const pinHalf = (options, config, shared) => {
281
230
  const pin = options?.pin;
282
231
  if (pin === false || pin === null) return [];
283
232
  const given = typeof pin === "object" ? pin : {};
@@ -289,7 +238,11 @@ const pinHalf = (options, config) => {
289
238
  { href: "/", label: "Site" },
290
239
  { href: config.subpath || "/", label: "Lab" },
291
240
  ],
241
+ // ONE URL (decided 2026-09-24): the board lives under the lab, at <subpath>/tasks, drawn in
242
+ // the lab's own chrome. `/pin` still answers, with a redirect. A consumer's `route` wins.
243
+ route: `${config.subpath}/tasks`,
292
244
  ...given,
245
+ lab: { shared },
293
246
  }),
294
247
  ];
295
248
  };
@@ -302,6 +255,8 @@ const pinHalf = (options, config) => {
302
255
  */
303
256
  export default function orbytesLab(options = {}) {
304
257
  const config = resolveLabOptions(options);
258
+ /** @type {Shared} */
259
+ const shared = { tasks: null };
305
260
  return [
306
261
  createAstrobookIntegration({
307
262
  directory: config.directory,
@@ -313,8 +268,8 @@ export default function orbytesLab(options = {}) {
313
268
  head: "@orbytes/astrolab/head",
314
269
  home: "@orbytes/astrolab/home",
315
270
  }),
316
- labShell(options),
317
- ...pinHalf(options, config),
271
+ labShell(options, shared),
272
+ ...pinHalf(options, config, shared),
318
273
  ];
319
274
  }
320
275
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@orbytes/astrolab",
3
- "version": "0.3.0",
3
+ "version": "0.4.0-next.2",
4
4
  "type": "module",
5
- "description": "The orbytes component lab and pin board — an Astrobook shell for Astro sites (live/used-by pills, a viewport configurator, responsive and cull marks, a parameters panel) plus a dev-only click-to-ticket feedback board at /pin.",
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",
@@ -28,10 +28,10 @@
28
28
  "preview": "npm --workspace testbed-orbytes-v2 run preview",
29
29
  "build:staging": "npm --workspace testbed-orbytes-v2 run build:staging",
30
30
  "check:isolation": "npm --workspace testbed-orbytes-v2 run check:isolation",
31
+ "check:lab-index": "npm --workspace testbed-orbytes-v2 run check:lab-index",
31
32
  "verify": "npm --workspace testbed-orbytes-v2 run verify"
32
33
  },
33
34
  "bin": {
34
- "orbytes-lab-cull": "bin/lab-cull.mjs",
35
35
  "orbytes-pin-gallery": "bin/pin-gallery.mjs"
36
36
  },
37
37
  "main": "./index.mjs",
@@ -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",