@nanobpm/urban 0.41.0 → 0.43.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.
- package/dist/runtime/core/modules/api.d.ts +27 -0
- package/dist/runtime/core/modules/api.js +67 -13
- package/dist/runtime/core/modules/cancel.d.ts +8 -4
- package/dist/runtime/core/modules/cancel.js +23 -11
- package/dist/runtime/core/modules/pages.js +229 -9
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.js +3 -0
- package/package.json +2 -2
- package/src/runtime/core/logger.test.ts +11 -0
- package/src/runtime/core/modules/api.test.ts +129 -4
- package/src/runtime/core/modules/api.ts +72 -13
- package/src/runtime/core/modules/cancel.test.ts +28 -0
- package/src/runtime/core/modules/cancel.ts +23 -11
- package/src/runtime/core/modules/pages.test.ts +120 -0
- package/src/runtime/core/modules/pages.ts +229 -9
- package/src/runtime/index.ts +4 -2
|
@@ -525,14 +525,22 @@ body { margin:0; font:15px/1.5 system-ui,sans-serif; padding:2rem; max-width:64r
|
|
|
525
525
|
.pc-card h2 { font-size:1rem; margin:0 0 .75rem; }
|
|
526
526
|
.pc-field { display:flex; flex-direction:column; gap:.25rem; margin-bottom:.6rem; }
|
|
527
527
|
.pc-field label { font-size:.8rem; color:var(--nano-text-muted); }
|
|
528
|
-
.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); }
|
|
528
|
+
.pc-field input:not([type=checkbox]) { padding:.5rem .6rem; border:1px solid var(--nano-edge); border-radius:.4rem; font:inherit; background:var(--nano-inset); color:var(--nano-text); }
|
|
529
|
+
.pc-field-check label { display:flex; flex-direction:row; align-items:center; gap:.5rem; font-size:.9rem; color:var(--nano-text); cursor:pointer; }
|
|
530
|
+
.pc-field-check input { width:auto; padding:0; accent-color:var(--nano-accent); cursor:pointer; }
|
|
529
531
|
.pc-btn { padding:.5rem .9rem; border:0; border-radius:.4rem; background:var(--nano-accent); color:var(--nano-on-accent); font:inherit; cursor:pointer; }
|
|
530
532
|
.pc-btn:disabled { opacity:.5; cursor:default; }
|
|
531
533
|
.pc-msg { font-size:.85rem; margin-top:.5rem; min-height:1.2em; }
|
|
532
534
|
.pc-msg.err { color:var(--nano-danger); }
|
|
533
535
|
.pc-msg.ok { color:var(--nano-ok); }
|
|
534
536
|
table.pc-grid { width:100%; border-collapse:collapse; font-size:.9rem; }
|
|
535
|
-
|
|
537
|
+
/* Wrap long, space-less cell values (e.g. a JSON blob or a 40-char SHA) so one
|
|
538
|
+
cell can't force its column wide and blow the whole table past the page.
|
|
539
|
+
overflow-wrap:anywhere reduces the cell's min-content width so the auto table
|
|
540
|
+
layout can shrink it to fit; max-width caps a single column when there is
|
|
541
|
+
ample room. vertical-align:top keeps a multi-line cell aligned with its
|
|
542
|
+
single-line neighbours. */
|
|
543
|
+
table.pc-grid th, table.pc-grid td { text-align:left; padding:.4rem .6rem; border-bottom:1px solid var(--nano-edge); overflow-wrap:anywhere; word-wrap:break-word; max-width:32rem; vertical-align:top; }
|
|
536
544
|
table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
|
|
537
545
|
.pc-tabs { display:flex; gap:.5rem; margin-bottom:.75rem; }
|
|
538
546
|
.pc-tab { padding:.35rem .8rem; border:1px solid var(--nano-edge); border-radius:.4rem; background:transparent; color:inherit; font:inherit; cursor:pointer; }
|
|
@@ -544,6 +552,13 @@ table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
|
|
|
544
552
|
.pc-detail-field { display:flex; gap:.5rem; font-size:.85rem; margin:.15rem 0; }
|
|
545
553
|
.pc-detail-label { color:var(--nano-text-muted); min-width:8rem; }
|
|
546
554
|
.pc-link { color:var(--nano-accent); }
|
|
555
|
+
.pc-badge { display:inline-flex; align-items:center; justify-content:center; min-width:1.35rem; height:1.35rem; box-sizing:border-box; padding:0 .4rem; border-radius:999px; font-size:.72rem; font-weight:700; line-height:1; color:#fff; background:var(--nano-danger); }
|
|
556
|
+
.pc-badge-danger { background:var(--nano-danger); }
|
|
557
|
+
.pc-badge-warn { background:var(--nano-warn); color:#3a2a00; }
|
|
558
|
+
.pc-badge-ok { background:var(--nano-ok); }
|
|
559
|
+
.pc-badge-info { background:var(--nano-info); }
|
|
560
|
+
:root[data-appearance="light"] .pc-badge-warn { color:#fff; }
|
|
561
|
+
@media (prefers-color-scheme: light) { :root:not([data-appearance]) .pc-badge-warn { color:#fff; } }
|
|
547
562
|
.pc-child { margin:.6rem 0; }
|
|
548
563
|
.pc-child-title { font-size:.8rem; font-weight:600; color:var(--nano-text-muted); margin-bottom:.25rem; }
|
|
549
564
|
.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; }
|
|
@@ -554,6 +569,16 @@ table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
|
|
|
554
569
|
.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; }
|
|
555
570
|
.pc-chevron-inline { color:var(--nano-text-faint); font-size:.75rem; width:1em; }
|
|
556
571
|
.pc-card-body[hidden] { display:none; }
|
|
572
|
+
/* A standalone button node + the modal it opens (e.g. a copy-pasteable prompt). */
|
|
573
|
+
.pc-buttonrow { margin:1rem 0; }
|
|
574
|
+
.pc-btn-ghost { background:transparent; color:var(--nano-text-muted); border:1px solid var(--nano-edge); }
|
|
575
|
+
.pc-btn-ghost:hover { color:var(--nano-text); border-color:var(--nano-accent); }
|
|
576
|
+
.pc-modal-overlay { position:fixed; inset:0; z-index:1000; display:flex; align-items:center; justify-content:center; padding:1.5rem; background:rgba(0,0,0,.55); }
|
|
577
|
+
.pc-modal { background:var(--nano-panel); border:1px solid var(--nano-edge-strong); border-radius:.6rem; padding:1.25rem 1.35rem; max-width:44rem; width:100%; max-height:85vh; overflow:auto; box-shadow:0 12px 40px rgba(0,0,0,.45); }
|
|
578
|
+
.pc-modal-title { font-size:1.15rem; margin:0 0 .5rem; }
|
|
579
|
+
.pc-modal-desc { color:var(--nano-text-muted); margin:0 0 .75rem; font-size:.9rem; white-space:pre-wrap; }
|
|
580
|
+
.pc-modal-code { white-space:pre-wrap; word-break:break-word; background:var(--nano-inset); border:1px solid var(--nano-edge); border-radius:.4rem; padding:.75rem; font:.82rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; max-height:45vh; overflow:auto; margin:0 0 .85rem; color:var(--nano-text); }
|
|
581
|
+
.pc-modal-actions { display:flex; gap:.5rem; justify-content:flex-end; }
|
|
557
582
|
/* Multi-page navigation: a horizontal menu bar or a vertical side rail. */
|
|
558
583
|
.pc-layout { display:flex; gap:1.5rem; align-items:flex-start; }
|
|
559
584
|
.pc-rail-wrap { flex:0 0 12rem; position:sticky; top:1rem; align-self:flex-start; }
|
|
@@ -611,6 +636,15 @@ function teardown() {
|
|
|
611
636
|
}
|
|
612
637
|
}
|
|
613
638
|
|
|
639
|
+
// Monotonic counter for per-modal element ids. aria-labelledby needs a unique
|
|
640
|
+
// title id: a fixed id would clash if the page already contains that id, and
|
|
641
|
+
// mis-associate the label for assistive tech.
|
|
642
|
+
let modalSeq = 0;
|
|
643
|
+
// At most one modal is open at a time. A double-click (or two rapid button
|
|
644
|
+
// clicks) would otherwise stack multiple overlays + document-level keydown
|
|
645
|
+
// listeners; this guard keeps a single dialog + handler live.
|
|
646
|
+
let modalOpen = false;
|
|
647
|
+
|
|
614
648
|
// Theme bridge for the Nano console embed. The app runs in a sandboxed,
|
|
615
649
|
// same-origin iframe, so the console's --nano-* custom properties and its
|
|
616
650
|
// data-appearance never cascade in. Instead the console postMessages its
|
|
@@ -764,11 +798,151 @@ function renderText(node) {
|
|
|
764
798
|
return el(v === "heading" ? "h1" : "p", { class: cls }, node.props.text || "");
|
|
765
799
|
}
|
|
766
800
|
|
|
767
|
-
//
|
|
768
|
-
//
|
|
801
|
+
// Copy text to the clipboard, resiliently. The async Clipboard API is tried
|
|
802
|
+
// first, but it can reject inside a sandboxed console iframe (no clipboard-write
|
|
803
|
+
// permission) — so fall back to a hidden <textarea> + execCommand("copy"), which
|
|
804
|
+
// works under a user gesture with only allow-scripts. Returns whether a copy
|
|
805
|
+
// succeeded so the caller can nudge the user to ⌘/Ctrl+C when both paths fail.
|
|
806
|
+
async function copyToClipboard(text) {
|
|
807
|
+
try {
|
|
808
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
809
|
+
await navigator.clipboard.writeText(text);
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
} catch (_e) { /* fall through to the execCommand path */ }
|
|
813
|
+
const ta = document.createElement("textarea");
|
|
814
|
+
try {
|
|
815
|
+
ta.value = text;
|
|
816
|
+
ta.setAttribute("readonly", "");
|
|
817
|
+
ta.style.position = "fixed";
|
|
818
|
+
ta.style.top = "-1000px";
|
|
819
|
+
ta.style.opacity = "0";
|
|
820
|
+
document.body.append(ta);
|
|
821
|
+
ta.select();
|
|
822
|
+
return document.execCommand("copy");
|
|
823
|
+
} catch (_e) { return false; }
|
|
824
|
+
// Always remove the hidden textarea, even if select()/execCommand threw, so a
|
|
825
|
+
// failed copy never leaves a detached node accreting in <body> on retry.
|
|
826
|
+
finally { ta.remove(); }
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Rebase the sole {{appBase}} token in author-supplied copy text onto the app's
|
|
830
|
+
// absolute mount root (always ends in "/"), so a prompt like
|
|
831
|
+
// "read {{appBase}}app/api/agent" resolves to a URL an external agent can fetch —
|
|
832
|
+
// the console proxy path when embedded, the app origin when standalone. No other
|
|
833
|
+
// interpolation happens: copy text is otherwise emitted verbatim.
|
|
834
|
+
function resolveCopyText(text) {
|
|
835
|
+
return typeof text === "string" ? text.split("{{appBase}}").join(APP_BASE.toString()) : "";
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Open a lightweight modal appended to <body>. Closes on the ✕/Close button, a
|
|
839
|
+
// backdrop click, Escape, or a page switch; the keydown listener is removed on
|
|
840
|
+
// close so it never leaks, and close() is registered with teardown() so
|
|
841
|
+
// navigating away (hashchange → renderPage → teardown) tears down any open modal
|
|
842
|
+
// instead of leaving a stale overlay + document-level listener alive. When it
|
|
843
|
+
// carries copyText it renders a scrollable code block plus a Copy button (see
|
|
844
|
+
// copyToClipboard for the sandbox-safe fallback).
|
|
845
|
+
function openModal(m) {
|
|
846
|
+
// Only one modal at a time — drop a second (e.g. double-click) open so we
|
|
847
|
+
// never stack overlays or register duplicate document-level keydown handlers.
|
|
848
|
+
if (modalOpen) return;
|
|
849
|
+
modalOpen = true;
|
|
850
|
+
const overlay = el("div", { class: "pc-modal-overlay" });
|
|
851
|
+
// role="dialog" + aria-modal need an accessible name: label by the visible
|
|
852
|
+
// title when present, else fall back to a generic aria-label so screen readers
|
|
853
|
+
// don't announce an unnamed dialog. The title id is unique per instance so it
|
|
854
|
+
// can't collide with a pre-existing page id.
|
|
855
|
+
const titleId = m.title ? "pc-modal-title-" + (++modalSeq) : null;
|
|
856
|
+
const dialog = el("div", titleId
|
|
857
|
+
? { class: "pc-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId }
|
|
858
|
+
: { class: "pc-modal", role: "dialog", "aria-modal": "true", "aria-label": "Dialog" });
|
|
859
|
+
if (m.title) dialog.append(el("h2", { class: "pc-modal-title", id: titleId }, m.title));
|
|
860
|
+
if (m.description) dialog.append(el("p", { class: "pc-modal-desc" }, m.description));
|
|
861
|
+
const copyText = resolveCopyText(m.copyText);
|
|
862
|
+
if (copyText) dialog.append(el("pre", { class: "pc-modal-code" }, copyText));
|
|
863
|
+
const actions = el("div", { class: "pc-modal-actions" });
|
|
864
|
+
// Remember what had focus so we can restore it on close — a keyboard/screen
|
|
865
|
+
// reader user returns to where they were instead of being dumped at <body>.
|
|
866
|
+
const prevFocus = document.activeElement;
|
|
867
|
+
let closed = false;
|
|
868
|
+
function close() {
|
|
869
|
+
if (closed) return;
|
|
870
|
+
closed = true;
|
|
871
|
+
modalOpen = false;
|
|
872
|
+
document.removeEventListener("keydown", onKey);
|
|
873
|
+
overlay.remove();
|
|
874
|
+
// A manual close (button / Escape / backdrop) never pops the disposers
|
|
875
|
+
// stack, so drop our own entry here — otherwise repeated open/close cycles
|
|
876
|
+
// accrete dead close() closures until the next page navigation runs teardown.
|
|
877
|
+
const i = disposers.indexOf(close);
|
|
878
|
+
if (i !== -1) disposers.splice(i, 1);
|
|
879
|
+
if (prevFocus && typeof prevFocus.focus === "function") prevFocus.focus();
|
|
880
|
+
}
|
|
881
|
+
// Focusable descendants of the dialog, in DOM order, for the Tab focus trap.
|
|
882
|
+
function focusables() {
|
|
883
|
+
return Array.prototype.slice.call(dialog.querySelectorAll(
|
|
884
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
885
|
+
)).filter((n) => !n.disabled);
|
|
886
|
+
}
|
|
887
|
+
function onKey(ev) {
|
|
888
|
+
if (ev.key === "Escape") { close(); return; }
|
|
889
|
+
if (ev.key !== "Tab") return;
|
|
890
|
+
// Trap Tab within the dialog so focus can't slip behind the overlay onto the
|
|
891
|
+
// inert page underneath — that would break aria-modal semantics.
|
|
892
|
+
const f = focusables();
|
|
893
|
+
if (!f.length) return;
|
|
894
|
+
const first = f[0];
|
|
895
|
+
const last = f[f.length - 1];
|
|
896
|
+
if (ev.shiftKey && document.activeElement === first) { ev.preventDefault(); last.focus(); }
|
|
897
|
+
else if (!ev.shiftKey && document.activeElement === last) { ev.preventDefault(); first.focus(); }
|
|
898
|
+
}
|
|
899
|
+
if (copyText) {
|
|
900
|
+
const label = m.copyLabel || "Copy";
|
|
901
|
+
const copyBtn = el("button", { class: "pc-btn pc-btn-sm", type: "button" }, label);
|
|
902
|
+
copyBtn.addEventListener("click", async () => {
|
|
903
|
+
const ok = await copyToClipboard(copyText);
|
|
904
|
+
copyBtn.textContent = ok ? "Copied ✓" : "Press ⌘/Ctrl+C";
|
|
905
|
+
setTimeout(() => { copyBtn.textContent = label; }, 2200);
|
|
906
|
+
});
|
|
907
|
+
actions.append(copyBtn);
|
|
908
|
+
}
|
|
909
|
+
const closeBtn = el("button", { class: "pc-btn pc-btn-sm pc-btn-ghost", type: "button" }, m.closeLabel || "Close");
|
|
910
|
+
closeBtn.addEventListener("click", close);
|
|
911
|
+
actions.append(closeBtn);
|
|
912
|
+
dialog.append(actions);
|
|
913
|
+
overlay.append(dialog);
|
|
914
|
+
// A click on the backdrop (never the dialog itself) dismisses.
|
|
915
|
+
overlay.addEventListener("click", (ev) => { if (ev.target === overlay) close(); });
|
|
916
|
+
document.addEventListener("keydown", onKey);
|
|
917
|
+
document.body.append(overlay);
|
|
918
|
+
// A page switch runs teardown() before the next render — dispose the modal
|
|
919
|
+
// there too so navigating away never leaves a stale overlay + keydown listener.
|
|
920
|
+
disposers.push(close);
|
|
921
|
+
closeBtn.focus();
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// A standalone button node. Clicking it opens props.modal — used e.g. to surface
|
|
925
|
+
// a copy-pasteable "point your agent here" prompt. props.variant === "ghost"
|
|
926
|
+
// renders the muted outline style. A button without a modal is inert.
|
|
927
|
+
function renderButton(node) {
|
|
928
|
+
const p = node.props || {};
|
|
929
|
+
const btn = el(
|
|
930
|
+
"button",
|
|
931
|
+
{ class: "pc-btn" + (p.variant === "ghost" ? " pc-btn-ghost" : ""), type: "button" },
|
|
932
|
+
p.label || "Open",
|
|
933
|
+
);
|
|
934
|
+
const m = p.modal && typeof p.modal === "object" ? p.modal : null;
|
|
935
|
+
if (m) btn.addEventListener("click", () => openModal(m));
|
|
936
|
+
return el("div", { class: "pc-buttonrow" }, btn);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// A grid td cell with three column-declared rendering modes, checked in order:
|
|
940
|
+
// 1. badge — a compact status indicator, gated on a non-empty field value
|
|
941
|
+
// (see the detailed note at the check below).
|
|
942
|
+
// 2. linkField — the cell text becomes a link to the URL held in that other
|
|
769
943
|
// field. Only http(s) hrefs are linked (a javascript:/other-scheme URL
|
|
770
944
|
// smuggled through row data falls back to plain text).
|
|
771
|
-
//
|
|
945
|
+
// 3. link: { kind: "processExplorer", keyField } — a structured, engine-aware
|
|
772
946
|
// link: the cell text links to the Nano console's explorer view for the
|
|
773
947
|
// process instance whose key is held in the row's keyField. The console
|
|
774
948
|
// path is constructed HERE (never taken from row data) and the key is
|
|
@@ -784,6 +958,27 @@ function renderText(node) {
|
|
|
784
958
|
// grid and child grids.
|
|
785
959
|
function gridCell(col, row) {
|
|
786
960
|
const text = row[col.field] == null ? "" : String(row[col.field]);
|
|
961
|
+
// 1. badge — a compact status indicator. When the row's field value is
|
|
962
|
+
// non-empty (truthy after trimming) render a small circular badge (e.g. a
|
|
963
|
+
// red "1" dot flagging an incident) whose tooltip is the full field text;
|
|
964
|
+
// when empty the cell stays blank so the column is unobtrusive until it
|
|
965
|
+
// matters. tone (danger|warn|ok|info, default danger) picks the color and
|
|
966
|
+
// label the glyph inside (default "1"). Label/tone come from the app
|
|
967
|
+
// schema (trusted); the tooltip is row data set via title/textContent
|
|
968
|
+
// (DOM-escaped by the platform, so no HTML/attribute injection). The full
|
|
969
|
+
// field text is also mirrored into aria-label so assistive tech announces
|
|
970
|
+
// the incident/status text (the visible "1" glyph alone is not meaningful).
|
|
971
|
+
if (col.badge) {
|
|
972
|
+
if (text.trim() === "") return el("td", {});
|
|
973
|
+
const t = col.badge.tone;
|
|
974
|
+
const tone = t === "warn" || t === "ok" || t === "info" ? t : "danger";
|
|
975
|
+
const label = col.badge.label == null ? "1" : String(col.badge.label);
|
|
976
|
+
return el(
|
|
977
|
+
"td",
|
|
978
|
+
{},
|
|
979
|
+
el("span", { class: "pc-badge pc-badge-" + tone, title: text, "aria-label": text }, label),
|
|
980
|
+
);
|
|
981
|
+
}
|
|
787
982
|
if (col.linkField) {
|
|
788
983
|
const href = row[col.linkField] == null ? "" : String(row[col.linkField]);
|
|
789
984
|
if (text !== "" && /^https?:\/\//i.test(href)) {
|
|
@@ -833,8 +1028,24 @@ function renderActionForm(node) {
|
|
|
833
1028
|
const inputs = new Map();
|
|
834
1029
|
const fieldTypes = new Map();
|
|
835
1030
|
for (const f of p.fields || []) {
|
|
836
|
-
const kind = f.type === "number" ? "number" : "text";
|
|
1031
|
+
const kind = f.type === "checkbox" ? "checkbox" : (f.type === "number" ? "number" : "text");
|
|
837
1032
|
fieldTypes.set(f.key, kind);
|
|
1033
|
+
if (kind === "checkbox") {
|
|
1034
|
+
// A boolean field renders a real checkbox; its default checked state comes
|
|
1035
|
+
// from default/checked on the field (unset -> unchecked). We record that on
|
|
1036
|
+
// the input's defaultChecked so a post-submit reset can restore it without a
|
|
1037
|
+
// side map that could drift from the actual input.
|
|
1038
|
+
const checked = f.default === true || f.checked === true;
|
|
1039
|
+
const input = el("input", { type: "checkbox" });
|
|
1040
|
+
input.defaultChecked = checked;
|
|
1041
|
+
input.checked = checked;
|
|
1042
|
+
inputs.set(f.key, input);
|
|
1043
|
+
// Input-first, wrapped in the label so the whole row is a click target.
|
|
1044
|
+
card.append(
|
|
1045
|
+
el("div", { class: "pc-field pc-field-check" }, el("label", {}, input, " " + (f.label || f.key))),
|
|
1046
|
+
);
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
838
1049
|
const attrs = { type: kind, placeholder: f.label || f.key };
|
|
839
1050
|
if (kind === "number") {
|
|
840
1051
|
attrs.inputmode = "numeric";
|
|
@@ -851,7 +1062,11 @@ function renderActionForm(node) {
|
|
|
851
1062
|
btn.addEventListener("click", async () => {
|
|
852
1063
|
const variables = Object.create(null);
|
|
853
1064
|
for (const [k, input] of inputs) {
|
|
854
|
-
if (fieldTypes.get(k) === "
|
|
1065
|
+
if (fieldTypes.get(k) === "checkbox") {
|
|
1066
|
+
// Always emit a real boolean (checked -> true / false), never a string,
|
|
1067
|
+
// so a strict equality check on the action side sees the intended value.
|
|
1068
|
+
variables[k] = input.checked === true;
|
|
1069
|
+
} else if (fieldTypes.get(k) === "number") {
|
|
855
1070
|
const t = String(input.value).trim();
|
|
856
1071
|
// Blank → omit so the action-side default applies; a non-finite parse
|
|
857
1072
|
// (NaN/Infinity) is also omitted rather than smuggling a raw string
|
|
@@ -874,7 +1089,12 @@ function renderActionForm(node) {
|
|
|
874
1089
|
msg.className = "pc-msg ok";
|
|
875
1090
|
msg.textContent = (p.action && p.action.successLabel) ||
|
|
876
1091
|
(res && res.processInstanceKey != null ? "Started (instance " + res.processInstanceKey + ")" : "Done");
|
|
877
|
-
for (const input of inputs
|
|
1092
|
+
for (const [k, input] of inputs) {
|
|
1093
|
+
// Text/number inputs clear; a checkbox resets to its declared default,
|
|
1094
|
+
// read straight off the input's defaultChecked (no side map to drift).
|
|
1095
|
+
if (fieldTypes.get(k) === "checkbox") input.checked = input.defaultChecked;
|
|
1096
|
+
else input.value = "";
|
|
1097
|
+
}
|
|
878
1098
|
document.dispatchEvent(new CustomEvent("pc:refresh"));
|
|
879
1099
|
} catch (e) {
|
|
880
1100
|
msg.className = "pc-msg err"; msg.textContent = String(e.message || e);
|
|
@@ -1253,7 +1473,7 @@ function renderNav(node) {
|
|
|
1253
1473
|
return nav;
|
|
1254
1474
|
}
|
|
1255
1475
|
|
|
1256
|
-
const RENDERERS = { text: renderText, actionForm: renderActionForm, dataGrid: renderDataGrid, nav: renderNav };
|
|
1476
|
+
const RENDERERS = { text: renderText, actionForm: renderActionForm, dataGrid: renderDataGrid, nav: renderNav, button: renderButton };
|
|
1257
1477
|
|
|
1258
1478
|
// Durable per-node UI state. localStorage is keyed by the home page id + node
|
|
1259
1479
|
// id so two grids on the same page (or the same grid across pages) don't clash,
|
package/src/runtime/index.ts
CHANGED
|
@@ -54,8 +54,10 @@ export type {
|
|
|
54
54
|
WorkerSubscription,
|
|
55
55
|
} from "./core/host.ts";
|
|
56
56
|
export type { AppApi, Mounted, RuntimeContext } from "./core/context.ts";
|
|
57
|
-
// Structured logging surface (see core/logger.ts): the shape of `AppApi.log
|
|
58
|
-
|
|
57
|
+
// Structured logging surface (see core/logger.ts): the shape of `AppApi.log`, plus `createLogger`
|
|
58
|
+
// so consumers can build a Logger for a custom sink or a no-op test double (`createLogger(() => {})`).
|
|
59
|
+
export { createLogger } from "./core/logger.ts";
|
|
60
|
+
export type { Logger, LogLevel, LogFields, LogSink } from "./core/logger.ts";
|
|
59
61
|
|
|
60
62
|
// BPMN error contract: a handler throws `BpmnError(code)` to raise a modelled
|
|
61
63
|
// error routed to an error boundary (ADR 0050), instead of a retryable failure.
|