@nanobpm/nano-workforce 0.197.0 → 0.198.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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.198.1](https://github.com/nanobpm/nano-workforce/compare/v0.198.0...v0.198.1) (2026-10-02)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **deps:** update dependency @nanobpm/agentic to ^0.15.0 ([#848](https://github.com/nanobpm/nano-workforce/issues/848)) ([26f731a](https://github.com/nanobpm/nano-workforce/commit/26f731a9d87a50a9a4852dc7dd5bc8b3e61c72c4))
6
+
7
+ ## [0.198.0](https://github.com/nanobpm/nano-workforce/compare/v0.197.0...v0.198.0) (2026-10-02)
8
+
9
+ ### Features
10
+
11
+ * **ui:** selectable themes (classic / soft / cartoon) ([#841](https://github.com/nanobpm/nano-workforce/issues/841)) ([5eb42e1](https://github.com/nanobpm/nano-workforce/commit/5eb42e17c0157311fde152f1d270dc6d7ff1610c)), closes [#840](https://github.com/nanobpm/nano-workforce/issues/840)
12
+
1
13
  ## [0.197.0](https://github.com/nanobpm/nano-workforce/compare/v0.196.0...v0.197.0) (2026-10-01)
2
14
 
3
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.197.0",
3
+ "version": "0.198.1",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",
@@ -64,8 +64,8 @@
64
64
  "lint:fix": "biome check --write app operations workers pages components scripts e2e main.ts"
65
65
  },
66
66
  "dependencies": {
67
- "@nanobpm/agentic": "^0.14.0",
68
- "@nanobpm/urban": "^0.94.1",
67
+ "@nanobpm/agentic": "^0.15.0",
68
+ "@nanobpm/urban": "^0.95.0",
69
69
  "bpmn-auto-layout": "^2.0.0-alpha.2"
70
70
  },
71
71
  "devDependencies": {
@@ -83,7 +83,7 @@
83
83
  "yaml": "^2.9.0"
84
84
  },
85
85
  "overrides": {
86
- "@nanobpm/engine-wasm": "^0.10.0",
86
+ "@nanobpm/engine-wasm": "^0.11.0",
87
87
  "@semantic-release/npm": "^13.1.5"
88
88
  }
89
89
  }
package/pages/app.css ADDED
@@ -0,0 +1,142 @@
1
+ /*
2
+ * nano-workforce themes (issue #840) — linked by the urban shell AFTER its built-in CSS
3
+ * (`pages/app.css`, nano-ide#578), so these rules win. Every rule is keyed on
4
+ * <html data-nwf-theme="…">, set by `pages/app.js` (which owns the theme list). "classic"
5
+ * has no rules: it is urban's stock look.
6
+ *
7
+ * Colours go through urban's `--nano-*` token contract. When embedded in the Nano console,
8
+ * the console writes its resolved tokens as INLINE styles on :root, which beat any stylesheet
9
+ * rule — hence `!important` on the token overrides, so the chosen theme still applies there.
10
+ */
11
+
12
+ /* ── the picker (all themes) ─────────────────────────────────────────── */
13
+ .nwf-theme-picker {
14
+ position: fixed; right: .75rem; bottom: .75rem; z-index: 50;
15
+ display: inline-flex; align-items: center; gap: .35rem;
16
+ padding: .25rem .5rem; border: 1px solid var(--nano-edge); border-radius: 999px;
17
+ background: var(--nano-panel); color: var(--nano-text-muted); font-size: .8rem;
18
+ box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
19
+ }
20
+ .nwf-theme-picker select { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; }
21
+
22
+ /* ══ SOFT — rounded, airy, gentle shadows; keeps light/dark ══════════════ */
23
+ :root[data-nwf-theme="soft"] {
24
+ --nwf-radius: 14px;
25
+ --nwf-shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 4px 14px rgba(16, 24, 40, .08);
26
+ --nwf-shadow-hover: 0 2px 4px rgba(16, 24, 40, .08), 0 10px 24px rgba(16, 24, 40, .12);
27
+ }
28
+ :root[data-nwf-theme="soft"][data-appearance="light"],
29
+ :root[data-nwf-theme="soft"]:not([data-appearance]) {
30
+ --nano-app: #f4f6fb !important; --nano-panel: #ffffff !important; --nano-raised: #ffffff !important;
31
+ --nano-inset: #eef1f7 !important; --nano-hover: #eef2ff !important;
32
+ --nano-edge: #e3e7f0 !important; --nano-edge-strong: #cdd3e1 !important;
33
+ --nano-text: #1d2433 !important; --nano-text-muted: #5b6478 !important; --nano-text-faint: #8a93a6 !important;
34
+ --nano-accent: #5b6cf0 !important; --nano-accent-strong: #4453d6 !important; --nano-on-accent: #ffffff !important;
35
+ --nano-ok: #12a873 !important; --nano-warn: #d98a0b !important; --nano-danger: #e5486a !important; --nano-info: #2f8fdc !important;
36
+ }
37
+ :root[data-nwf-theme="soft"][data-appearance="dark"] {
38
+ --nwf-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 6px 18px rgba(0, 0, 0, .35);
39
+ --nwf-shadow-hover: 0 2px 4px rgba(0, 0, 0, .35), 0 12px 28px rgba(0, 0, 0, .45);
40
+ }
41
+ :root[data-nwf-theme="soft"] body { font-family: Nunito, "Segoe UI", system-ui, sans-serif; letter-spacing: .005em; }
42
+ :root[data-nwf-theme="soft"] .pc-heading { font-weight: 800; letter-spacing: -.01em; }
43
+ :root[data-nwf-theme="soft"] .pc-card,
44
+ :root[data-nwf-theme="soft"] [data-grid-layout="cards"] table.pc-grid tr:has(> td[data-label]) {
45
+ border-color: transparent; border-radius: var(--nwf-radius); box-shadow: var(--nwf-shadow);
46
+ transition: box-shadow .15s ease, transform .15s ease;
47
+ }
48
+ :root[data-nwf-theme="soft"] [data-grid-layout="cards"] table.pc-grid tr:has(> td[data-label]):hover {
49
+ box-shadow: var(--nwf-shadow-hover); transform: translateY(-1px);
50
+ }
51
+ :root[data-nwf-theme="soft"] [data-grid-layout="cards"] table.pc-grid tr { padding: .8rem 1rem; margin: .8rem 0; }
52
+ :root[data-nwf-theme="soft"] [data-grid-layout="cards"] table.pc-grid td[data-label]::before {
53
+ text-transform: none; letter-spacing: 0; font-weight: 600; font-size: .78rem;
54
+ }
55
+ :root[data-nwf-theme="soft"] .pc-btn,
56
+ :root[data-nwf-theme="soft"] .njf-actions button { border-radius: 999px; padding: .45rem 1rem; font-weight: 600; }
57
+ :root[data-nwf-theme="soft"] .pc-tab,
58
+ :root[data-nwf-theme="soft"] .pc-nav-link { border-radius: 999px; }
59
+ :root[data-nwf-theme="soft"] .pc-bar { border-bottom: 0; }
60
+ :root[data-nwf-theme="soft"] .pc-bar.pc-sticky { box-shadow: 0 1px 0 var(--nano-edge); }
61
+ :root[data-nwf-theme="soft"] .pc-pipe-stage { padding: .15rem .6rem; }
62
+ :root[data-nwf-theme="soft"] .pc-input,
63
+ :root[data-nwf-theme="soft"] .pc-select,
64
+ :root[data-nwf-theme="soft"] .pc-textarea,
65
+ :root[data-nwf-theme="soft"] .pc-field input:not([type=checkbox]) { border-radius: 10px; }
66
+ :root[data-nwf-theme="soft"] .pc-modal { border-radius: 18px; box-shadow: 0 20px 50px rgba(0, 0, 0, .25); }
67
+
68
+ /* ══ CARTOON — cubefarm-style: chunky ink outlines, warm paper, Fredoka (light only) ══ */
69
+ :root[data-nwf-theme="cartoon"] {
70
+ color-scheme: light;
71
+ --nwf-ink: #1f1d2b;
72
+ --nano-app: #bfe3ff !important; --nano-panel: #fffdf6 !important; --nano-raised: #ffffff !important;
73
+ --nano-inset: #f3eedf !important; --nano-hover: #ffe8d6 !important;
74
+ --nano-edge: #1f1d2b !important; --nano-edge-strong: #1f1d2b !important;
75
+ --nano-text: #1f1d2b !important; --nano-text-muted: #6c7086 !important; --nano-text-faint: #9a9cb0 !important;
76
+ --nano-accent: #ff8a5b !important; --nano-accent-strong: #e8643a !important; --nano-accent-2: #3a56d4 !important;
77
+ --nano-on-accent: #1f1d2b !important;
78
+ --nano-ok: #2dc653 !important; --nano-warn: #ffb703 !important; --nano-danger: #ef476f !important; --nano-info: #3a9bf0 !important;
79
+ }
80
+ :root[data-nwf-theme="cartoon"] body {
81
+ font-family: Fredoka, "Segoe UI", system-ui, sans-serif;
82
+ background:
83
+ radial-gradient(circle at 12% 8%, rgba(255, 255, 255, .55) 0 6rem, transparent 6.1rem),
84
+ radial-gradient(circle at 88% 18%, rgba(255, 255, 255, .45) 0 4rem, transparent 4.1rem),
85
+ var(--nano-app);
86
+ background-attachment: fixed;
87
+ }
88
+ :root[data-nwf-theme="cartoon"] .pc-heading { font-weight: 700; font-size: 1.9rem; }
89
+ :root[data-nwf-theme="cartoon"] .pc-card,
90
+ :root[data-nwf-theme="cartoon"] [data-grid-layout="cards"] table.pc-grid tr:has(> td[data-label]),
91
+ :root[data-nwf-theme="cartoon"] .pc-modal,
92
+ :root[data-nwf-theme="cartoon"] .pc-appview-frame {
93
+ border: 2px solid var(--nwf-ink); border-bottom-width: 5px; border-radius: 16px;
94
+ box-shadow: 0 6px 0 rgba(31, 29, 43, .12);
95
+ }
96
+ :root[data-nwf-theme="cartoon"] [data-grid-layout="cards"] table.pc-grid tr { padding: .8rem 1rem; margin: .9rem 0; }
97
+ :root[data-nwf-theme="cartoon"] [data-grid-layout="cards"] table.pc-grid tr:has(> td[data-label]):hover {
98
+ transform: translateY(-2px) rotate(-.25deg); transition: transform .12s ease;
99
+ }
100
+ :root[data-nwf-theme="cartoon"] [data-grid-layout="cards"] table.pc-grid td[data-label]::before {
101
+ font-weight: 600; text-transform: none; letter-spacing: 0; font-size: .8rem; color: var(--nano-text-muted);
102
+ }
103
+ :root[data-nwf-theme="cartoon"] [data-grid-layout="cards"] table.pc-grid td.pc-mcell-primary { font-size: 1.1rem; font-weight: 700; }
104
+ :root[data-nwf-theme="cartoon"] table.pc-grid th,
105
+ :root[data-nwf-theme="cartoon"] table.pc-grid td { border-bottom-color: rgba(31, 29, 43, .15); }
106
+ /* key-cap buttons */
107
+ :root[data-nwf-theme="cartoon"] .pc-btn,
108
+ :root[data-nwf-theme="cartoon"] .njf-actions button,
109
+ :root[data-nwf-theme="cartoon"] .pc-tab,
110
+ :root[data-nwf-theme="cartoon"] .pc-chevron {
111
+ border: 2px solid var(--nwf-ink); border-bottom-width: 4px; border-radius: 10px;
112
+ font-weight: 600; transition: transform .06s ease, border-bottom-width .06s ease;
113
+ }
114
+ :root[data-nwf-theme="cartoon"] .pc-btn:active:not(:disabled),
115
+ :root[data-nwf-theme="cartoon"] .njf-actions button:active,
116
+ :root[data-nwf-theme="cartoon"] .pc-tab:active {
117
+ transform: translateY(2px); border-bottom-width: 2px;
118
+ }
119
+ :root[data-nwf-theme="cartoon"] .pc-btn-ghost { background: #fff; color: var(--nwf-ink); }
120
+ :root[data-nwf-theme="cartoon"] .pc-badge { border: 2px solid var(--nwf-ink); color: var(--nwf-ink); font-weight: 700; }
121
+ :root[data-nwf-theme="cartoon"] .pc-pipe-stage { border-width: 2px; background: #fff; }
122
+ :root[data-nwf-theme="cartoon"] .pc-pipe-done { background: #d9f7e1; }
123
+ :root[data-nwf-theme="cartoon"] .pc-pipe-active { background: #ffe8d6; }
124
+ :root[data-nwf-theme="cartoon"] .pc-pipe-conn { height: 3px; background: var(--nwf-ink); opacity: .25; }
125
+ :root[data-nwf-theme="cartoon"] .pc-pipe-conn.filled { opacity: 1; background: var(--nano-ok); }
126
+ /* the nav as a chunky toolbar */
127
+ :root[data-nwf-theme="cartoon"] .pc-bar {
128
+ border: 2px solid var(--nwf-ink); border-bottom-width: 5px; border-radius: 16px;
129
+ background: var(--nano-panel); padding: .5rem .75rem;
130
+ }
131
+ :root[data-nwf-theme="cartoon"] .pc-bar.pc-sticky { margin-inline: 0; padding-inline: .75rem; top: .5rem; }
132
+ :root[data-nwf-theme="cartoon"] .pc-nav-title { font-weight: 700; font-size: 1.1rem; }
133
+ :root[data-nwf-theme="cartoon"] .pc-nav-link { border-radius: 10px; color: var(--nwf-ink); font-weight: 500; }
134
+ :root[data-nwf-theme="cartoon"] .pc-nav-link.active { background: var(--nano-accent); color: var(--nwf-ink); box-shadow: inset 0 -3px 0 rgba(31, 29, 43, .25); }
135
+ :root[data-nwf-theme="cartoon"] .pc-input,
136
+ :root[data-nwf-theme="cartoon"] .pc-select,
137
+ :root[data-nwf-theme="cartoon"] .pc-textarea,
138
+ :root[data-nwf-theme="cartoon"] .pc-field input:not([type=checkbox]) { border: 2px solid var(--nwf-ink); border-radius: 10px; background: #fff; }
139
+ :root[data-nwf-theme="cartoon"] .pc-md-code,
140
+ :root[data-nwf-theme="cartoon"] .pc-md-pre code { font-family: "JetBrains Mono", Consolas, monospace; }
141
+ :root[data-nwf-theme="cartoon"] .pc-link { color: #3a56d4; font-weight: 500; }
142
+ :root[data-nwf-theme="cartoon"] .nwf-theme-picker { border: 2px solid var(--nwf-ink); border-bottom-width: 4px; }
package/pages/app.js ADDED
@@ -0,0 +1,89 @@
1
+ // nano-workforce selectable themes (issue #840).
2
+ //
3
+ // Loaded by the urban page shell by convention (`pages/app.js`, nano-ide#578), after the
4
+ // page runtime. It stamps the chosen theme onto <html data-nwf-theme="…"> — which every
5
+ // rule in `pages/app.css` keys on — and mounts a small floating picker. The choice is
6
+ // persisted per browser in localStorage.
7
+ //
8
+ // THEMES is the single source of truth for the theme list: the picker options, the
9
+ // validation of a stored value, and the guard test (test/themes.test.ts, which checks every
10
+ // non-default theme has rules in app.css) all derive from it.
11
+
12
+ /** @type {ReadonlyArray<{ id: string, label: string, font?: string }>} */
13
+ export const THEMES = Object.freeze([
14
+ { id: "classic", label: "Classic" },
15
+ { id: "soft", label: "Soft", font: "Nunito:wght@400;600;700" },
16
+ { id: "cartoon", label: "Cartoon", font: "Fredoka:wght@400;500;600;700" },
17
+ ]);
18
+
19
+ export const DEFAULT_THEME = "classic";
20
+ export const STORAGE_KEY = "nwf.theme";
21
+
22
+ /** A stored/requested value → a known theme id (anything unknown falls back to the default). */
23
+ export function resolveTheme(value) {
24
+ return THEMES.some((t) => t.id === value) ? value : DEFAULT_THEME;
25
+ }
26
+
27
+ function readStored() {
28
+ try {
29
+ return globalThis.localStorage?.getItem(STORAGE_KEY) ?? null;
30
+ } catch {
31
+ return null; // storage blocked (e.g. sandboxed iframe) → default
32
+ }
33
+ }
34
+
35
+ function writeStored(id) {
36
+ try {
37
+ globalThis.localStorage?.setItem(STORAGE_KEY, id);
38
+ } catch {
39
+ /* storage blocked → the choice just lasts for this page view */
40
+ }
41
+ }
42
+
43
+ function loadFont(doc, theme) {
44
+ if (!theme.font || doc.getElementById(`nwf-font-${theme.id}`)) return;
45
+ const link = doc.createElement("link");
46
+ link.id = `nwf-font-${theme.id}`;
47
+ link.rel = "stylesheet";
48
+ link.href = `https://fonts.googleapis.com/css2?family=${theme.font}&display=swap`;
49
+ doc.head.appendChild(link);
50
+ }
51
+
52
+ /** Apply a theme to the document (idempotent). */
53
+ export function applyTheme(doc, value) {
54
+ const id = resolveTheme(value);
55
+ doc.documentElement.dataset.nwfTheme = id;
56
+ const theme = THEMES.find((t) => t.id === id);
57
+ if (theme) loadFont(doc, theme);
58
+ return id;
59
+ }
60
+
61
+ function mountPicker(doc) {
62
+ if (doc.getElementById("nwf-theme-picker")) return;
63
+ const wrap = doc.createElement("label");
64
+ wrap.id = "nwf-theme-picker";
65
+ wrap.className = "nwf-theme-picker";
66
+ wrap.title = "Theme";
67
+ const icon = doc.createElement("span");
68
+ icon.textContent = "🎨";
69
+ icon.setAttribute("aria-hidden", "true");
70
+ const select = doc.createElement("select");
71
+ select.setAttribute("aria-label", "Theme");
72
+ for (const t of THEMES) {
73
+ const opt = doc.createElement("option");
74
+ opt.value = t.id;
75
+ opt.textContent = t.label;
76
+ select.appendChild(opt);
77
+ }
78
+ select.value = doc.documentElement.dataset.nwfTheme ?? DEFAULT_THEME;
79
+ select.addEventListener("change", () => {
80
+ writeStored(applyTheme(doc, select.value));
81
+ });
82
+ wrap.append(icon, select);
83
+ doc.body.appendChild(wrap);
84
+ }
85
+
86
+ if (typeof document !== "undefined") {
87
+ applyTheme(document, readStored());
88
+ mountPicker(document);
89
+ }
@@ -0,0 +1,75 @@
1
+ // #840 — selectable themes: `pages/app.js` (theme registry + picker) and `pages/app.css` (the rules),
2
+ // shipped by urban's app-asset convention (nano-ide#578).
3
+ import assert from "node:assert/strict";
4
+ import { readFileSync } from "node:fs";
5
+ import { test } from "node:test";
6
+
7
+ // Comments stripped: rules only (the header comment describes the attribute generically).
8
+ const css = readFileSync("pages/app.css", "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
9
+
10
+ async function themes() {
11
+ return await import("../pages/app.js");
12
+ }
13
+
14
+ test("#840: the theme registry offers classic (default), soft and cartoon", async () => {
15
+ const { THEMES, DEFAULT_THEME } = await themes();
16
+ assert.deepEqual(
17
+ THEMES.map((t: { id: string }) => t.id),
18
+ ["classic", "soft", "cartoon"],
19
+ );
20
+ assert.equal(DEFAULT_THEME, "classic");
21
+ });
22
+
23
+ test("#840: every non-default theme in the registry has rules in app.css (no phantom theme)", async () => {
24
+ const { THEMES, DEFAULT_THEME } = await themes();
25
+ for (const t of THEMES) {
26
+ if (t.id === DEFAULT_THEME) continue;
27
+ assert.ok(css.includes(`[data-nwf-theme="${t.id}"]`), `app.css has no rules for theme "${t.id}"`);
28
+ }
29
+ });
30
+
31
+ test("#840: app.css only styles themes the registry knows (no orphan rules)", async () => {
32
+ const { THEMES } = await themes();
33
+ const known = new Set(THEMES.map((t: { id: string }) => t.id));
34
+ for (const m of css.matchAll(/data-nwf-theme="([^"]+)"/g)) {
35
+ assert.ok(known.has(m[1]), `app.css styles unknown theme "${m[1]}"`);
36
+ }
37
+ });
38
+
39
+ test("#840: theme token overrides are !important so they survive the console's inline :root tokens", () => {
40
+ for (const decl of css.matchAll(/(--nano-[a-z0-9-]+):\s*([^;]+);/g)) {
41
+ assert.match(decl[2], /!important\s*$/, `${decl[1]} override must be !important`);
42
+ }
43
+ });
44
+
45
+ test("#840: resolveTheme falls back to the default for unknown/blank values", async () => {
46
+ const { resolveTheme } = await themes();
47
+ assert.equal(resolveTheme("cartoon"), "cartoon");
48
+ assert.equal(resolveTheme("soft"), "soft");
49
+ assert.equal(resolveTheme("nope"), "classic");
50
+ assert.equal(resolveTheme(null), "classic");
51
+ });
52
+
53
+ test("#840: applyTheme stamps <html data-nwf-theme> and loads the theme's font once", async () => {
54
+ const { applyTheme } = await themes();
55
+ const appended: Array<{ id: string; href: string }> = [];
56
+ const byId = new Map<string, unknown>();
57
+ const doc = {
58
+ documentElement: { dataset: {} as Record<string, string> },
59
+ getElementById: (id: string) => byId.get(id) ?? null,
60
+ createElement: () => ({ id: "", rel: "", href: "" }),
61
+ head: {
62
+ appendChild: (n: { id: string; href: string }) => {
63
+ appended.push(n);
64
+ byId.set(n.id, n);
65
+ },
66
+ },
67
+ };
68
+ assert.equal(applyTheme(doc, "cartoon"), "cartoon");
69
+ assert.equal(doc.documentElement.dataset.nwfTheme, "cartoon");
70
+ applyTheme(doc, "cartoon");
71
+ assert.equal(appended.length, 1, "font link is added once");
72
+ assert.match(appended[0].href, /Fredoka/);
73
+ assert.equal(applyTheme(doc, "bogus"), "classic");
74
+ assert.equal(doc.documentElement.dataset.nwfTheme, "classic");
75
+ });