@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 {
|
|
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 {
|
|
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 {
|
|
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(--
|
|
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;
|
|
338
|
-
.pc-field input { padding:.5rem .6rem; border:1px solid var(--
|
|
339
|
-
.pc-btn { padding:.5rem .9rem; border:0; border-radius:.4rem; background:var(--
|
|
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
|
|
343
|
-
.pc-msg.ok { color
|
|
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(--
|
|
346
|
-
table.pc-grid th { font-weight:600;
|
|
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(--
|
|
349
|
-
.pc-tab.active { background:var(--
|
|
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(--
|
|
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 {
|
|
356
|
-
.pc-link { color:var(--
|
|
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;
|
|
359
|
-
.pc-transcript { white-space:pre-wrap; max-height:22rem; overflow:auto; background:
|
|
360
|
-
.pc-subform { margin-top:.75rem; padding:.6rem; border:1px dashed var(--
|
|
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;
|
|
363
|
-
.pc-textarea { width:100%; min-height:4rem; padding:.5rem; border:1px solid var(--
|
|
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 {
|
|
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.
|
|
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/
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(--
|
|
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;
|
|
368
|
-
.pc-field input { padding:.5rem .6rem; border:1px solid var(--
|
|
369
|
-
.pc-btn { padding:.5rem .9rem; border:0; border-radius:.4rem; background:var(--
|
|
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
|
|
373
|
-
.pc-msg.ok { color
|
|
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(--
|
|
376
|
-
table.pc-grid th { font-weight:600;
|
|
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(--
|
|
379
|
-
.pc-tab.active { background:var(--
|
|
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(--
|
|
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 {
|
|
386
|
-
.pc-link { color:var(--
|
|
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;
|
|
389
|
-
.pc-transcript { white-space:pre-wrap; max-height:22rem; overflow:auto; background:
|
|
390
|
-
.pc-subform { margin-top:.75rem; padding:.6rem; border:1px dashed var(--
|
|
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;
|
|
393
|
-
.pc-textarea { width:100%; min-height:4rem; padding:.5rem; border:1px solid var(--
|
|
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 {
|
|
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
|