@nanobpm/urban 0.26.0 → 0.27.0

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.
@@ -323,46 +323,82 @@ function rendererShell(homePage) {
323
323
  </body>
324
324
  </html>`;
325
325
  }
326
+ // The app's stylesheet resolves every colour through the Nano console's
327
+ // semantic `--nano-*` token contract (see the Magikcraft/nano-bpm repo's
328
+ // `console/src/theme/tokens.css` — MIRROR: keep the palettes below in sync). When the app is embedded in the
329
+ // console it inherits nothing across the iframe boundary, so the console
330
+ // postMessages its resolved tokens + appearance and the runtime lays them onto
331
+ // this document's :root (see RENDERER_JS). Standalone (CLI on :3000) there is no
332
+ // console, so the defaults below stand on their own: dark by default, light when
333
+ // the OS asks for it and nothing has themed us. `--pc-*` are kept as thin
334
+ // aliases so existing class rules re-resolve through the shared tokens.
326
335
  const RENDERER_CSS = `
327
- :root { color-scheme: light dark; --pc-edge:#d0d0d8; --pc-accent:#3b5bdb; }
336
+ :root {
337
+ color-scheme: dark;
338
+ --nano-app:#0b0b10; --nano-panel:#10101a; --nano-raised:#16161f; --nano-inset:#08080c; --nano-hover:#1e1e2a;
339
+ --nano-edge:#24242f; --nano-edge-strong:#383848;
340
+ --nano-text:#f2f2f7; --nano-text-muted:#a3a3b2; --nano-text-faint:#6e6e80;
341
+ --nano-accent:#8b5cf6; --nano-accent-strong:#a78bfa; --nano-accent-2:#22d3ee; --nano-on-accent:#ffffff;
342
+ --nano-ok:#34d399; --nano-warn:#fbbf24; --nano-danger:#fb7185; --nano-info:#38bdf8;
343
+ }
344
+ :root[data-appearance="light"] {
345
+ color-scheme: light;
346
+ --nano-app:#f5f5f9; --nano-panel:#fdfdfe; --nano-raised:#ffffff; --nano-inset:#ededf3; --nano-hover:#e8e8f0;
347
+ --nano-edge:#e2e2ea; --nano-edge-strong:#c5c5d4;
348
+ --nano-text:#1a1a22; --nano-text-muted:#565664; --nano-text-faint:#8c8c9c;
349
+ --nano-accent:#7c3aed; --nano-accent-strong:#6d28d9; --nano-accent-2:#0891b2; --nano-on-accent:#ffffff;
350
+ --nano-ok:#059669; --nano-warn:#b45309; --nano-danger:#e11d48; --nano-info:#0369a1;
351
+ }
352
+ @media (prefers-color-scheme: light) {
353
+ /* Standalone only: follow the OS until the console (which sets data-appearance) themes us. */
354
+ :root:not([data-appearance]) {
355
+ color-scheme: light;
356
+ --nano-app:#f5f5f9; --nano-panel:#fdfdfe; --nano-raised:#ffffff; --nano-inset:#ededf3; --nano-hover:#e8e8f0;
357
+ --nano-edge:#e2e2ea; --nano-edge-strong:#c5c5d4;
358
+ --nano-text:#1a1a22; --nano-text-muted:#565664; --nano-text-faint:#8c8c9c;
359
+ --nano-accent:#7c3aed; --nano-accent-strong:#6d28d9; --nano-accent-2:#0891b2; --nano-on-accent:#ffffff;
360
+ --nano-ok:#059669; --nano-warn:#b45309; --nano-danger:#e11d48; --nano-info:#0369a1;
361
+ }
362
+ }
363
+ :root { --pc-edge:var(--nano-edge); --pc-accent:var(--nano-accent); }
328
364
  * { box-sizing: border-box; }
329
- body { margin:0; font:15px/1.5 system-ui,sans-serif; padding:2rem; max-width:64rem; margin-inline:auto; }
330
- .pc-empty { opacity:.6; }
365
+ body { margin:0; font:15px/1.5 system-ui,sans-serif; padding:2rem; max-width:64rem; margin-inline:auto; background:var(--nano-app); color:var(--nano-text); }
366
+ .pc-empty { color:var(--nano-text-faint); }
331
367
  .pc-heading { font-size:1.6rem; font-weight:650; margin:0 0 .25rem; }
332
- .pc-sub { opacity:.7; margin:.25rem 0 1rem; }
368
+ .pc-sub { color:var(--nano-text-muted); margin:.25rem 0 1rem; }
333
369
  .pc-body { margin:.5rem 0; }
334
- .pc-card { border:1px solid var(--pc-edge); border-radius:.6rem; padding:1rem 1.15rem; margin:1rem 0; }
370
+ .pc-card { border:1px solid var(--nano-edge); border-radius:.6rem; padding:1rem 1.15rem; margin:1rem 0; background:var(--nano-panel); }
335
371
  .pc-card h2 { font-size:1rem; margin:0 0 .75rem; }
336
372
  .pc-field { display:flex; flex-direction:column; gap:.25rem; margin-bottom:.6rem; }
337
- .pc-field label { font-size:.8rem; opacity:.75; }
338
- .pc-field input { padding:.5rem .6rem; border:1px solid var(--pc-edge); border-radius:.4rem; font:inherit; }
339
- .pc-btn { padding:.5rem .9rem; border:0; border-radius:.4rem; background:var(--pc-accent); color:#fff; font:inherit; cursor:pointer; }
373
+ .pc-field label { font-size:.8rem; color:var(--nano-text-muted); }
374
+ .pc-field input { padding:.5rem .6rem; border:1px solid var(--nano-edge); border-radius:.4rem; font:inherit; background:var(--nano-inset); color:var(--nano-text); }
375
+ .pc-btn { padding:.5rem .9rem; border:0; border-radius:.4rem; background:var(--nano-accent); color:var(--nano-on-accent); font:inherit; cursor:pointer; }
340
376
  .pc-btn:disabled { opacity:.5; cursor:default; }
341
377
  .pc-msg { font-size:.85rem; margin-top:.5rem; min-height:1.2em; }
342
- .pc-msg.err { color:#c0392b; }
343
- .pc-msg.ok { color:#2b8a3e; }
378
+ .pc-msg.err { color:var(--nano-danger); }
379
+ .pc-msg.ok { color:var(--nano-ok); }
344
380
  table.pc-grid { width:100%; border-collapse:collapse; font-size:.9rem; }
345
- table.pc-grid th, table.pc-grid td { text-align:left; padding:.4rem .6rem; border-bottom:1px solid var(--pc-edge); }
346
- table.pc-grid th { font-weight:600; opacity:.75; }
381
+ table.pc-grid th, table.pc-grid td { text-align:left; padding:.4rem .6rem; border-bottom:1px solid var(--nano-edge); }
382
+ table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
347
383
  .pc-tabs { display:flex; gap:.5rem; margin-bottom:.75rem; }
348
- .pc-tab { padding:.35rem .8rem; border:1px solid var(--pc-edge); border-radius:.4rem; background:transparent; color:inherit; font:inherit; cursor:pointer; }
349
- .pc-tab.active { background:var(--pc-accent); color:#fff; border-color:var(--pc-accent); }
384
+ .pc-tab { padding:.35rem .8rem; border:1px solid var(--nano-edge); border-radius:.4rem; background:transparent; color:inherit; font:inherit; cursor:pointer; }
385
+ .pc-tab.active { background:var(--nano-accent); color:var(--nano-on-accent); border-color:var(--nano-accent); }
350
386
  .pc-btn-sm { padding:.25rem .55rem; font-size:.8rem; margin-right:.3rem; }
351
- .pc-chevron { background:transparent; color:inherit; border:1px solid var(--pc-edge); }
387
+ .pc-chevron { background:transparent; color:inherit; border:1px solid var(--nano-edge); }
352
388
  .pc-row-actions { white-space:nowrap; text-align:right; }
353
389
  .pc-detail { padding:.75rem .25rem; }
354
390
  .pc-detail-field { display:flex; gap:.5rem; font-size:.85rem; margin:.15rem 0; }
355
- .pc-detail-label { opacity:.7; min-width:8rem; }
356
- .pc-link { color:var(--pc-accent); }
391
+ .pc-detail-label { color:var(--nano-text-muted); min-width:8rem; }
392
+ .pc-link { color:var(--nano-accent); }
357
393
  .pc-child { margin:.6rem 0; }
358
- .pc-child-title { font-size:.8rem; font-weight:600; opacity:.7; margin-bottom:.25rem; }
359
- .pc-transcript { white-space:pre-wrap; max-height:22rem; overflow:auto; background:rgba(120,120,160,.08); padding:.5rem; border-radius:.4rem; font-size:.8rem; margin-top:.4rem; }
360
- .pc-subform { margin-top:.75rem; padding:.6rem; border:1px dashed var(--pc-edge); border-radius:.5rem; }
394
+ .pc-child-title { font-size:.8rem; font-weight:600; color:var(--nano-text-muted); margin-bottom:.25rem; }
395
+ .pc-transcript { white-space:pre-wrap; max-height:22rem; overflow:auto; background:var(--nano-inset); padding:.5rem; border-radius:.4rem; font-size:.8rem; margin-top:.4rem; }
396
+ .pc-subform { margin-top:.75rem; padding:.6rem; border:1px dashed var(--nano-edge); border-radius:.5rem; }
361
397
  .pc-subform-title { font-weight:600; font-size:.85rem; margin-bottom:.4rem; }
362
- .pc-prompt { font-size:.85rem; opacity:.8; margin-bottom:.4rem; white-space:pre-wrap; }
363
- .pc-textarea { width:100%; min-height:4rem; padding:.5rem; border:1px solid var(--pc-edge); border-radius:.4rem; font:inherit; }
398
+ .pc-prompt { font-size:.85rem; color:var(--nano-text-muted); margin-bottom:.4rem; white-space:pre-wrap; }
399
+ .pc-textarea { width:100%; min-height:4rem; padding:.5rem; border:1px solid var(--nano-edge); border-radius:.4rem; font:inherit; background:var(--nano-inset); color:var(--nano-text); }
364
400
  .pc-collapse-header { display:flex; align-items:center; gap:.5rem; width:100%; margin:0 0 .75rem; padding:0; background:transparent; border:0; color:inherit; font:inherit; font-size:1rem; font-weight:600; cursor:pointer; text-align:left; }
365
- .pc-chevron-inline { opacity:.6; font-size:.75rem; width:1em; }
401
+ .pc-chevron-inline { color:var(--nano-text-faint); font-size:.75rem; width:1em; }
366
402
  .pc-card-body[hidden] { display:none; }
367
403
  `;
368
404
  // The schema-driven browser renderer (ADR 0042 §3). Plain ES module string served at
@@ -373,6 +409,38 @@ const RENDERER_JS = String.raw `
373
409
  const root = document.getElementById("page");
374
410
  const HOME = root.dataset.home || "home";
375
411
 
412
+ // Theme bridge for the Nano console embed. The app runs in a sandboxed,
413
+ // same-origin iframe, so the console's --nano-* custom properties and its
414
+ // data-appearance never cascade in. Instead the console postMessages its
415
+ // resolved tokens; we lay them onto this document's :root and mirror the
416
+ // appearance. Standalone (no console parent) nothing is posted, so the CSS
417
+ // defaults stand. We announce readiness so the console can push the theme even
418
+ // if its first message raced our listener install.
419
+ function applyTheme(msg) {
420
+ if (!msg || msg.type !== "nano-theme") return;
421
+ const el = document.documentElement;
422
+ if (msg.appearance === "light" || msg.appearance === "dark") el.dataset.appearance = msg.appearance;
423
+ const vars = msg.vars;
424
+ if (vars && typeof vars === "object") {
425
+ for (const [k, v] of Object.entries(vars)) {
426
+ // Only accept the shared token namespace; ignore anything else.
427
+ if (typeof k === "string" && k.startsWith("--nano-") && typeof v === "string") {
428
+ el.style.setProperty(k, v);
429
+ }
430
+ }
431
+ }
432
+ }
433
+ if (window.parent && window.parent !== window) {
434
+ window.addEventListener("message", (ev) => {
435
+ // Same-origin proxy (posture A): only trust a message from the framing
436
+ // parent AND from our own origin. ev.source alone is insufficient — a
437
+ // cross-origin page that frames a standalone run becomes window.parent, so
438
+ // an origin check is what actually pins the trust boundary.
439
+ if (ev.origin === window.location.origin && ev.source === window.parent) applyTheme(ev.data);
440
+ });
441
+ window.parent.postMessage({ type: "nano-app-ready" }, window.location.origin);
442
+ }
443
+
376
444
  // Resolve every app endpoint against where THIS module was actually served from,
377
445
  // so absolute-looking "/app/…" paths work both at the origin root (direct run,
378
446
  // e.g. the CLI on :3000) and under a path-prefixed reverse proxy (the Nano
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "git+https://github.com/jwulf/nano-ide.git",
22
+ "url": "git+https://github.com/nanobpm/nano-ide.git",
23
23
  "directory": "packages/urban"
24
24
  },
25
25
  "engines": {
@@ -94,6 +94,20 @@ test("GET / serves the renderer shell with the home marker", async () => {
94
94
  assert.doesNotMatch(res.body ?? "", /src="\/app\/runtime\.js"/);
95
95
  });
96
96
 
97
+ test("the shell styles the app through the shared --nano-* token contract", async () => {
98
+ const res = await dispatch("GET", "/");
99
+ const html = res.body ?? "";
100
+ // Body colours resolve through the console's token vocabulary, not hardcoded hex.
101
+ assert.match(html, /background:var\(--nano-app\)/);
102
+ assert.match(html, /color:var\(--nano-text\)/);
103
+ assert.match(html, /--pc-accent:var\(--nano-accent\)/);
104
+ // Self-contained defaults for standalone (CLI) runs: dark base + a light
105
+ // palette gated on data-appearance, and prefers-color-scheme only until themed.
106
+ assert.match(html, /--nano-accent:#8b5cf6/);
107
+ assert.match(html, /\[data-appearance="light"\]/);
108
+ assert.match(html, /:root:not\(\[data-appearance\]\)/);
109
+ });
110
+
97
111
  test("GET /app/runtime.js serves the renderer module", async () => {
98
112
  const res = await dispatch("GET", "/app/runtime.js");
99
113
  assert.match(res.headers?.["content-type"] ?? "", /javascript/);
@@ -104,6 +118,24 @@ test("GET /app/runtime.js serves the renderer module", async () => {
104
118
  assert.match(res.body ?? "", /function apiUrl\(u\)/);
105
119
  });
106
120
 
121
+ test("the renderer bridges the console theme over postMessage", async () => {
122
+ const res = await dispatch("GET", "/app/runtime.js");
123
+ const js = res.body ?? "";
124
+ // Announces readiness to the framing console (same-origin, posture A).
125
+ assert.match(js, /postMessage\(\{ type: "nano-app-ready" \}, window\.location\.origin\)/);
126
+ // Only trusts the framing parent, and only accepts nano-theme messages.
127
+ assert.match(js, /ev\.source === window\.parent/);
128
+ // Same-origin posture A: a cross-origin framer is rejected before applyTheme.
129
+ assert.match(js, /ev\.origin === window\.location\.origin/);
130
+ assert.match(js, /msg\.type !== "nano-theme"/);
131
+ // Mirrors the console appearance + lays --nano-* tokens onto :root; ignores
132
+ // any property outside the shared token namespace.
133
+ assert.match(js, /el\.dataset\.appearance = msg\.appearance/);
134
+ assert.match(js, /k\.startsWith\("--nano-"\)/);
135
+ assert.match(js, /el\.style\.setProperty\(k, v\)/);
136
+ });
137
+
138
+
107
139
  test("renderer preserves grid row-detail expansion across refreshes", async () => {
108
140
  // Regression: the 5s poll rebuilds the whole tbody, which used to collapse an open
109
141
  // detail row (e.g. the escalation-answer form). The client now tracks open rowKeys
@@ -353,46 +353,82 @@ function rendererShell(homePage: string): string {
353
353
  </html>`;
354
354
  }
355
355
 
356
+ // The app's stylesheet resolves every colour through the Nano console's
357
+ // semantic `--nano-*` token contract (see the Magikcraft/nano-bpm repo's
358
+ // `console/src/theme/tokens.css` — MIRROR: keep the palettes below in sync). When the app is embedded in the
359
+ // console it inherits nothing across the iframe boundary, so the console
360
+ // postMessages its resolved tokens + appearance and the runtime lays them onto
361
+ // this document's :root (see RENDERER_JS). Standalone (CLI on :3000) there is no
362
+ // console, so the defaults below stand on their own: dark by default, light when
363
+ // the OS asks for it and nothing has themed us. `--pc-*` are kept as thin
364
+ // aliases so existing class rules re-resolve through the shared tokens.
356
365
  const RENDERER_CSS = `
357
- :root { color-scheme: light dark; --pc-edge:#d0d0d8; --pc-accent:#3b5bdb; }
366
+ :root {
367
+ color-scheme: dark;
368
+ --nano-app:#0b0b10; --nano-panel:#10101a; --nano-raised:#16161f; --nano-inset:#08080c; --nano-hover:#1e1e2a;
369
+ --nano-edge:#24242f; --nano-edge-strong:#383848;
370
+ --nano-text:#f2f2f7; --nano-text-muted:#a3a3b2; --nano-text-faint:#6e6e80;
371
+ --nano-accent:#8b5cf6; --nano-accent-strong:#a78bfa; --nano-accent-2:#22d3ee; --nano-on-accent:#ffffff;
372
+ --nano-ok:#34d399; --nano-warn:#fbbf24; --nano-danger:#fb7185; --nano-info:#38bdf8;
373
+ }
374
+ :root[data-appearance="light"] {
375
+ color-scheme: light;
376
+ --nano-app:#f5f5f9; --nano-panel:#fdfdfe; --nano-raised:#ffffff; --nano-inset:#ededf3; --nano-hover:#e8e8f0;
377
+ --nano-edge:#e2e2ea; --nano-edge-strong:#c5c5d4;
378
+ --nano-text:#1a1a22; --nano-text-muted:#565664; --nano-text-faint:#8c8c9c;
379
+ --nano-accent:#7c3aed; --nano-accent-strong:#6d28d9; --nano-accent-2:#0891b2; --nano-on-accent:#ffffff;
380
+ --nano-ok:#059669; --nano-warn:#b45309; --nano-danger:#e11d48; --nano-info:#0369a1;
381
+ }
382
+ @media (prefers-color-scheme: light) {
383
+ /* Standalone only: follow the OS until the console (which sets data-appearance) themes us. */
384
+ :root:not([data-appearance]) {
385
+ color-scheme: light;
386
+ --nano-app:#f5f5f9; --nano-panel:#fdfdfe; --nano-raised:#ffffff; --nano-inset:#ededf3; --nano-hover:#e8e8f0;
387
+ --nano-edge:#e2e2ea; --nano-edge-strong:#c5c5d4;
388
+ --nano-text:#1a1a22; --nano-text-muted:#565664; --nano-text-faint:#8c8c9c;
389
+ --nano-accent:#7c3aed; --nano-accent-strong:#6d28d9; --nano-accent-2:#0891b2; --nano-on-accent:#ffffff;
390
+ --nano-ok:#059669; --nano-warn:#b45309; --nano-danger:#e11d48; --nano-info:#0369a1;
391
+ }
392
+ }
393
+ :root { --pc-edge:var(--nano-edge); --pc-accent:var(--nano-accent); }
358
394
  * { box-sizing: border-box; }
359
- body { margin:0; font:15px/1.5 system-ui,sans-serif; padding:2rem; max-width:64rem; margin-inline:auto; }
360
- .pc-empty { opacity:.6; }
395
+ body { margin:0; font:15px/1.5 system-ui,sans-serif; padding:2rem; max-width:64rem; margin-inline:auto; background:var(--nano-app); color:var(--nano-text); }
396
+ .pc-empty { color:var(--nano-text-faint); }
361
397
  .pc-heading { font-size:1.6rem; font-weight:650; margin:0 0 .25rem; }
362
- .pc-sub { opacity:.7; margin:.25rem 0 1rem; }
398
+ .pc-sub { color:var(--nano-text-muted); margin:.25rem 0 1rem; }
363
399
  .pc-body { margin:.5rem 0; }
364
- .pc-card { border:1px solid var(--pc-edge); border-radius:.6rem; padding:1rem 1.15rem; margin:1rem 0; }
400
+ .pc-card { border:1px solid var(--nano-edge); border-radius:.6rem; padding:1rem 1.15rem; margin:1rem 0; background:var(--nano-panel); }
365
401
  .pc-card h2 { font-size:1rem; margin:0 0 .75rem; }
366
402
  .pc-field { display:flex; flex-direction:column; gap:.25rem; margin-bottom:.6rem; }
367
- .pc-field label { font-size:.8rem; opacity:.75; }
368
- .pc-field input { padding:.5rem .6rem; border:1px solid var(--pc-edge); border-radius:.4rem; font:inherit; }
369
- .pc-btn { padding:.5rem .9rem; border:0; border-radius:.4rem; background:var(--pc-accent); color:#fff; font:inherit; cursor:pointer; }
403
+ .pc-field label { font-size:.8rem; color:var(--nano-text-muted); }
404
+ .pc-field input { padding:.5rem .6rem; border:1px solid var(--nano-edge); border-radius:.4rem; font:inherit; background:var(--nano-inset); color:var(--nano-text); }
405
+ .pc-btn { padding:.5rem .9rem; border:0; border-radius:.4rem; background:var(--nano-accent); color:var(--nano-on-accent); font:inherit; cursor:pointer; }
370
406
  .pc-btn:disabled { opacity:.5; cursor:default; }
371
407
  .pc-msg { font-size:.85rem; margin-top:.5rem; min-height:1.2em; }
372
- .pc-msg.err { color:#c0392b; }
373
- .pc-msg.ok { color:#2b8a3e; }
408
+ .pc-msg.err { color:var(--nano-danger); }
409
+ .pc-msg.ok { color:var(--nano-ok); }
374
410
  table.pc-grid { width:100%; border-collapse:collapse; font-size:.9rem; }
375
- table.pc-grid th, table.pc-grid td { text-align:left; padding:.4rem .6rem; border-bottom:1px solid var(--pc-edge); }
376
- table.pc-grid th { font-weight:600; opacity:.75; }
411
+ table.pc-grid th, table.pc-grid td { text-align:left; padding:.4rem .6rem; border-bottom:1px solid var(--nano-edge); }
412
+ table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
377
413
  .pc-tabs { display:flex; gap:.5rem; margin-bottom:.75rem; }
378
- .pc-tab { padding:.35rem .8rem; border:1px solid var(--pc-edge); border-radius:.4rem; background:transparent; color:inherit; font:inherit; cursor:pointer; }
379
- .pc-tab.active { background:var(--pc-accent); color:#fff; border-color:var(--pc-accent); }
414
+ .pc-tab { padding:.35rem .8rem; border:1px solid var(--nano-edge); border-radius:.4rem; background:transparent; color:inherit; font:inherit; cursor:pointer; }
415
+ .pc-tab.active { background:var(--nano-accent); color:var(--nano-on-accent); border-color:var(--nano-accent); }
380
416
  .pc-btn-sm { padding:.25rem .55rem; font-size:.8rem; margin-right:.3rem; }
381
- .pc-chevron { background:transparent; color:inherit; border:1px solid var(--pc-edge); }
417
+ .pc-chevron { background:transparent; color:inherit; border:1px solid var(--nano-edge); }
382
418
  .pc-row-actions { white-space:nowrap; text-align:right; }
383
419
  .pc-detail { padding:.75rem .25rem; }
384
420
  .pc-detail-field { display:flex; gap:.5rem; font-size:.85rem; margin:.15rem 0; }
385
- .pc-detail-label { opacity:.7; min-width:8rem; }
386
- .pc-link { color:var(--pc-accent); }
421
+ .pc-detail-label { color:var(--nano-text-muted); min-width:8rem; }
422
+ .pc-link { color:var(--nano-accent); }
387
423
  .pc-child { margin:.6rem 0; }
388
- .pc-child-title { font-size:.8rem; font-weight:600; opacity:.7; margin-bottom:.25rem; }
389
- .pc-transcript { white-space:pre-wrap; max-height:22rem; overflow:auto; background:rgba(120,120,160,.08); padding:.5rem; border-radius:.4rem; font-size:.8rem; margin-top:.4rem; }
390
- .pc-subform { margin-top:.75rem; padding:.6rem; border:1px dashed var(--pc-edge); border-radius:.5rem; }
424
+ .pc-child-title { font-size:.8rem; font-weight:600; color:var(--nano-text-muted); margin-bottom:.25rem; }
425
+ .pc-transcript { white-space:pre-wrap; max-height:22rem; overflow:auto; background:var(--nano-inset); padding:.5rem; border-radius:.4rem; font-size:.8rem; margin-top:.4rem; }
426
+ .pc-subform { margin-top:.75rem; padding:.6rem; border:1px dashed var(--nano-edge); border-radius:.5rem; }
391
427
  .pc-subform-title { font-weight:600; font-size:.85rem; margin-bottom:.4rem; }
392
- .pc-prompt { font-size:.85rem; opacity:.8; margin-bottom:.4rem; white-space:pre-wrap; }
393
- .pc-textarea { width:100%; min-height:4rem; padding:.5rem; border:1px solid var(--pc-edge); border-radius:.4rem; font:inherit; }
428
+ .pc-prompt { font-size:.85rem; color:var(--nano-text-muted); margin-bottom:.4rem; white-space:pre-wrap; }
429
+ .pc-textarea { width:100%; min-height:4rem; padding:.5rem; border:1px solid var(--nano-edge); border-radius:.4rem; font:inherit; background:var(--nano-inset); color:var(--nano-text); }
394
430
  .pc-collapse-header { display:flex; align-items:center; gap:.5rem; width:100%; margin:0 0 .75rem; padding:0; background:transparent; border:0; color:inherit; font:inherit; font-size:1rem; font-weight:600; cursor:pointer; text-align:left; }
395
- .pc-chevron-inline { opacity:.6; font-size:.75rem; width:1em; }
431
+ .pc-chevron-inline { color:var(--nano-text-faint); font-size:.75rem; width:1em; }
396
432
  .pc-card-body[hidden] { display:none; }
397
433
  `;
398
434
 
@@ -404,6 +440,38 @@ const RENDERER_JS = String.raw`
404
440
  const root = document.getElementById("page");
405
441
  const HOME = root.dataset.home || "home";
406
442
 
443
+ // Theme bridge for the Nano console embed. The app runs in a sandboxed,
444
+ // same-origin iframe, so the console's --nano-* custom properties and its
445
+ // data-appearance never cascade in. Instead the console postMessages its
446
+ // resolved tokens; we lay them onto this document's :root and mirror the
447
+ // appearance. Standalone (no console parent) nothing is posted, so the CSS
448
+ // defaults stand. We announce readiness so the console can push the theme even
449
+ // if its first message raced our listener install.
450
+ function applyTheme(msg) {
451
+ if (!msg || msg.type !== "nano-theme") return;
452
+ const el = document.documentElement;
453
+ if (msg.appearance === "light" || msg.appearance === "dark") el.dataset.appearance = msg.appearance;
454
+ const vars = msg.vars;
455
+ if (vars && typeof vars === "object") {
456
+ for (const [k, v] of Object.entries(vars)) {
457
+ // Only accept the shared token namespace; ignore anything else.
458
+ if (typeof k === "string" && k.startsWith("--nano-") && typeof v === "string") {
459
+ el.style.setProperty(k, v);
460
+ }
461
+ }
462
+ }
463
+ }
464
+ if (window.parent && window.parent !== window) {
465
+ window.addEventListener("message", (ev) => {
466
+ // Same-origin proxy (posture A): only trust a message from the framing
467
+ // parent AND from our own origin. ev.source alone is insufficient — a
468
+ // cross-origin page that frames a standalone run becomes window.parent, so
469
+ // an origin check is what actually pins the trust boundary.
470
+ if (ev.origin === window.location.origin && ev.source === window.parent) applyTheme(ev.data);
471
+ });
472
+ window.parent.postMessage({ type: "nano-app-ready" }, window.location.origin);
473
+ }
474
+
407
475
  // Resolve every app endpoint against where THIS module was actually served from,
408
476
  // so absolute-looking "/app/…" paths work both at the origin root (direct run,
409
477
  // e.g. the CLI on :3000) and under a path-prefixed reverse proxy (the Nano