@calliopelabs/ui 0.7.0 → 0.11.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.
Files changed (120) hide show
  1. package/README.md +253 -3
  2. package/dist/appearance/AppearancePicker.js +4 -2
  3. package/dist/appearance/AppearanceProvider.d.ts +10 -2
  4. package/dist/appearance/AppearanceProvider.js +6 -2
  5. package/dist/appearance/AppearanceScript.d.ts +15 -4
  6. package/dist/appearance/AppearanceScript.js +11 -5
  7. package/dist/appearance/appearance.js +1 -1
  8. package/dist/appearance/boot.d.ts +13 -2
  9. package/dist/appearance/boot.js +35 -5
  10. package/dist/appearance/product.d.ts +13 -0
  11. package/dist/appearance/product.js +2 -2
  12. package/dist/appearance/runtime-brand.d.ts +106 -0
  13. package/dist/appearance/runtime-brand.js +420 -0
  14. package/dist/css/appearance.js +147 -0
  15. package/dist/css/calliope.css +1221 -0
  16. package/dist/css/components.css +729 -0
  17. package/dist/css/tokens.css +491 -0
  18. package/dist/data/DefinitionList.d.ts +16 -0
  19. package/dist/data/DefinitionList.js +9 -0
  20. package/dist/data/Stat.d.ts +25 -0
  21. package/dist/data/Stat.js +19 -0
  22. package/dist/data/Tag.d.ts +21 -0
  23. package/dist/data/Tag.js +18 -0
  24. package/dist/data/types.d.ts +3 -2
  25. package/dist/data/use-local-table.d.ts +36 -0
  26. package/dist/data/use-local-table.js +127 -0
  27. package/dist/index.d.ts +42 -3
  28. package/dist/index.js +39 -1
  29. package/dist/nav/Nav.d.ts +2 -1
  30. package/dist/nav/Nav.js +34 -15
  31. package/dist/nav/model.d.ts +21 -0
  32. package/dist/page/PageHeader.d.ts +5 -3
  33. package/dist/page/PageHeader.js +5 -4
  34. package/dist/page/Panel.d.ts +19 -0
  35. package/dist/page/Panel.js +12 -0
  36. package/dist/primitives/Avatar.d.ts +37 -0
  37. package/dist/primitives/Avatar.js +43 -0
  38. package/dist/primitives/Field.d.ts +33 -0
  39. package/dist/primitives/Field.js +30 -0
  40. package/dist/primitives/Segmented.d.ts +30 -0
  41. package/dist/primitives/Segmented.js +20 -0
  42. package/dist/primitives/Select.d.ts +40 -0
  43. package/dist/primitives/Select.js +47 -0
  44. package/dist/primitives/Switch.d.ts +7 -0
  45. package/dist/primitives/Switch.js +8 -0
  46. package/dist/primitives/Textarea.d.ts +3 -0
  47. package/dist/primitives/Textarea.js +6 -0
  48. package/dist/provider/messages.d.ts +412 -0
  49. package/dist/provider/messages.js +633 -0
  50. package/dist/shell/BrandMark.d.ts +25 -3
  51. package/dist/shell/BrandMark.js +34 -4
  52. package/dist/shell/RailPanel.js +2 -2
  53. package/dist/shell/RailTree.js +2 -2
  54. package/dist/shell/Sidebar.d.ts +9 -2
  55. package/dist/shell/Sidebar.js +10 -4
  56. package/dist/shell/UserMenu.d.ts +3 -1
  57. package/dist/shell/UserMenu.js +3 -11
  58. package/dist/shell/rail-boot.d.ts +28 -0
  59. package/dist/shell/rail-boot.js +45 -0
  60. package/dist/shell/rail-state.d.ts +6 -1
  61. package/dist/shell/rail-state.js +18 -2
  62. package/dist/styles/preset.css +4 -0
  63. package/dist/styles/theme.css +36 -13
  64. package/dist/theme/css-layer.d.ts +19 -0
  65. package/dist/theme/css-layer.js +171 -0
  66. package/dist/theme/css.d.ts +1 -0
  67. package/dist/theme/css.js +45 -9
  68. package/dist/workflow/ArtifactCard.d.ts +47 -0
  69. package/dist/workflow/ArtifactCard.js +45 -0
  70. package/dist/workflow/DocumentReview.d.ts +74 -0
  71. package/dist/workflow/DocumentReview.js +54 -0
  72. package/dist/workflow/FormReview.d.ts +53 -0
  73. package/dist/workflow/FormReview.js +83 -0
  74. package/dist/workflow/GateBadge.d.ts +11 -0
  75. package/dist/workflow/GateBadge.js +14 -0
  76. package/dist/workflow/MediaReview.d.ts +63 -0
  77. package/dist/workflow/MediaReview.js +166 -0
  78. package/dist/workflow/ReviewPanel.d.ts +41 -0
  79. package/dist/workflow/ReviewPanel.js +59 -0
  80. package/dist/workflow/RunTimeline.d.ts +25 -0
  81. package/dist/workflow/RunTimeline.js +56 -0
  82. package/dist/workflow/WorkflowCard.d.ts +39 -0
  83. package/dist/workflow/WorkflowCard.js +56 -0
  84. package/dist/workflow/WorkflowGallery.d.ts +43 -0
  85. package/dist/workflow/WorkflowGallery.js +69 -0
  86. package/dist/workflow/canvas/WorkflowCanvas.d.ts +39 -0
  87. package/dist/workflow/canvas/WorkflowCanvas.js +648 -0
  88. package/dist/workflow/definition.d.ts +408 -0
  89. package/dist/workflow/definition.js +411 -0
  90. package/dist/workflow/editor/WorkflowDiff.d.ts +17 -0
  91. package/dist/workflow/editor/WorkflowDiff.js +25 -0
  92. package/dist/workflow/editor/WorkflowImportPreview.d.ts +19 -0
  93. package/dist/workflow/editor/WorkflowImportPreview.js +50 -0
  94. package/dist/workflow/editor/WorkflowInspector.d.ts +40 -0
  95. package/dist/workflow/editor/WorkflowInspector.js +349 -0
  96. package/dist/workflow/editor/WorkflowJsonView.d.ts +16 -0
  97. package/dist/workflow/editor/WorkflowJsonView.js +40 -0
  98. package/dist/workflow/editor/WorkflowPalette.d.ts +16 -0
  99. package/dist/workflow/editor/WorkflowPalette.js +36 -0
  100. package/dist/workflow/editor/WorkflowProblems.d.ts +20 -0
  101. package/dist/workflow/editor/WorkflowProblems.js +51 -0
  102. package/dist/workflow/editor/WorkflowWorkspace.d.ts +49 -0
  103. package/dist/workflow/editor/WorkflowWorkspace.js +154 -0
  104. package/dist/workflow/gallery.d.ts +21 -0
  105. package/dist/workflow/gallery.js +29 -0
  106. package/dist/workflow/layout.d.ts +49 -0
  107. package/dist/workflow/layout.js +162 -0
  108. package/dist/workflow/model.d.ts +50 -0
  109. package/dist/workflow/model.js +7 -0
  110. package/dist/workflow/palette.d.ts +34 -0
  111. package/dist/workflow/palette.js +70 -0
  112. package/dist/workflow/portable.d.ts +162 -0
  113. package/dist/workflow/portable.js +547 -0
  114. package/dist/workflow/problems.d.ts +38 -0
  115. package/dist/workflow/problems.js +409 -0
  116. package/dist/workflow/run.d.ts +94 -0
  117. package/dist/workflow/run.js +31 -0
  118. package/dist/workflow/workspace.d.ts +33 -0
  119. package/dist/workflow/workspace.js +17 -0
  120. package/package.json +6 -1
@@ -0,0 +1,411 @@
1
+ /** The definition format this package reads and writes. */
2
+ export const WORKFLOW_DEFINITION_VERSION = 1;
3
+ /** Every kind of node a flow is built from. */
4
+ export const WORKFLOW_NODE_KINDS = [
5
+ "step",
6
+ "subflow",
7
+ "gate",
8
+ "condition",
9
+ "goto",
10
+ "fanout",
11
+ "join",
12
+ "variants",
13
+ "evaluator",
14
+ "classifier",
15
+ "check",
16
+ ];
17
+ export const WORKFLOW_SELECTORS = [
18
+ "evaluator",
19
+ "first-passing",
20
+ "cheapest-passing",
21
+ "person",
22
+ ];
23
+ // ---------------------------------------------------------------- lookups
24
+ export function findPreset(registry, ref) {
25
+ if (!ref)
26
+ return null;
27
+ const module = registry.modules.find((m) => m.id === ref.module);
28
+ const preset = module?.presets.find((p) => p.id === ref.id);
29
+ return module && preset ? { module, preset } : null;
30
+ }
31
+ export function findFact(registry, id) {
32
+ return registry.facts.find((f) => f.id === id);
33
+ }
34
+ export function findGateKind(registry, id) {
35
+ return registry.gateKinds.find((g) => g.id === id);
36
+ }
37
+ /** The operators that fit a fact's type. */
38
+ export function operatorsFor(registry, fact) {
39
+ if (!fact)
40
+ return [];
41
+ return registry.operators.filter((o) => o.types.includes(fact.type));
42
+ }
43
+ /** Every data type the registry names: the modules' own, then the host's. */
44
+ export function dataTypes(registry) {
45
+ return [...registry.modules.flatMap((m) => m.types ?? []), ...(registry.types ?? [])];
46
+ }
47
+ /** Whether a value of type `given` can go where `wanted` is asked for. */
48
+ export function typeAccepts(registry, given, wanted) {
49
+ if (wanted === "any" || given === wanted)
50
+ return true;
51
+ const types = dataTypes(registry);
52
+ const seen = new Set();
53
+ const stack = [given];
54
+ while (stack.length) {
55
+ const id = stack.pop();
56
+ if (id === wanted)
57
+ return true;
58
+ if (seen.has(id))
59
+ continue;
60
+ seen.add(id);
61
+ for (const t of types)
62
+ if (t.id === id)
63
+ stack.push(...(t.extends ?? []));
64
+ }
65
+ return false;
66
+ }
67
+ export const isJudge = (n) => n.kind === "evaluator" || n.kind === "classifier" || n.kind === "check";
68
+ const SUBJECT = { id: "subject", label: "Subject", type: "any", optional: true };
69
+ /**
70
+ * A node's typed inputs: a preset's, a custom step's own, a sub-flow's
71
+ * flow's, and a judge's subject (what it reads; the latest output when unbound).
72
+ */
73
+ export function inputsOf(node, registry, definition) {
74
+ switch (node.kind) {
75
+ case "step":
76
+ return findPreset(registry, node.preset)?.preset.inputs ?? node.inputs ?? [];
77
+ case "variants":
78
+ return findPreset(registry, node.preset)?.preset.inputs ?? [];
79
+ case "subflow":
80
+ return definition?.flows[node.flow]?.inputs ?? [];
81
+ case "evaluator":
82
+ case "classifier":
83
+ case "check":
84
+ return [SUBJECT];
85
+ default:
86
+ return [];
87
+ }
88
+ }
89
+ /** A node's typed outputs. Gates, logic and judges pass work on but make nothing. */
90
+ export function outputsOf(node, registry, definition) {
91
+ switch (node.kind) {
92
+ case "step":
93
+ return findPreset(registry, node.preset)?.preset.outputs ?? node.outputs ?? [];
94
+ case "variants":
95
+ return findPreset(registry, node.preset)?.preset.outputs ?? [];
96
+ case "subflow":
97
+ return definition?.flows[node.flow]?.outputs ?? [];
98
+ default:
99
+ return [];
100
+ }
101
+ }
102
+ // ---------------------------------------------------------------- exits and edges
103
+ /** The way on from a node with one exit. */
104
+ export const OUT = "out";
105
+ /** The handle a loop leaves from, and the one it comes back into. */
106
+ export const LOOP_OUT = "back";
107
+ export const LOOP_IN = "back-in";
108
+ /** The handle work comes into. */
109
+ export const IN = "in";
110
+ /**
111
+ * The exits a node offers for `next` edges, in order: a condition's cases and
112
+ * else, a check's yes and no, a classifier's labels, other and else, a routing
113
+ * evaluator's pass and fail. A go-to has none: it jumps.
114
+ */
115
+ export function exitsOf(node) {
116
+ switch (node.kind) {
117
+ case "goto":
118
+ return [];
119
+ case "condition":
120
+ return [...node.cases.map((c) => `case:${c.id}`), "else"];
121
+ case "check":
122
+ return ["yes", "no"];
123
+ case "classifier":
124
+ return [
125
+ ...node.options.map((o) => `option:${o.id}`),
126
+ ...(node.other ? ["other"] : []),
127
+ "else",
128
+ ];
129
+ case "evaluator":
130
+ return node.routes ? ["pass", "fail"] : [OUT];
131
+ default:
132
+ return [OUT];
133
+ }
134
+ }
135
+ /**
136
+ * An exit as the node names it: a case's or a label's own words; the fixed
137
+ * exits (`else`, `yes`, `no`, `other`, `pass`, `fail`, `out`) come back as
138
+ * themselves for the caller's copy to name.
139
+ */
140
+ export function exitLabel(node, exit) {
141
+ if (node.kind === "condition" && exit.startsWith("case:"))
142
+ return node.cases.find((c) => `case:${c.id}` === exit)?.label ?? exit;
143
+ if (node.kind === "classifier" && exit.startsWith("option:"))
144
+ return node.options.find((o) => `option:${o.id}` === exit)?.label ?? exit;
145
+ return exit;
146
+ }
147
+ export const portOf = (e) => e.port ?? (e.kind === "loop" ? LOOP_OUT : OUT);
148
+ /** A stable id for an edge: its kind, ends and ports. */
149
+ export const edgeId = (e) => `${e.kind}:${e.from}:${portOf(e)}>${e.to}${e.kind === "data" && e.input ? `:${e.input}` : ""}`;
150
+ /** Ids reachable from `from` over next edges. */
151
+ export function downstream(flow, from) {
152
+ return walk(flow, from, "down");
153
+ }
154
+ /** Ids with a path of next edges to `to`. */
155
+ export function upstream(flow, to) {
156
+ return walk(flow, to, "up");
157
+ }
158
+ function walk(flow, start, dir) {
159
+ const seen = new Set();
160
+ const stack = [start];
161
+ while (stack.length) {
162
+ const id = stack.pop();
163
+ for (const e of flow.edges) {
164
+ if (e.kind !== "next")
165
+ continue;
166
+ const [a, b] = dir === "down" ? [e.from, e.to] : [e.to, e.from];
167
+ if (a === id && !seen.has(b)) {
168
+ seen.add(b);
169
+ stack.push(b);
170
+ }
171
+ }
172
+ }
173
+ return seen;
174
+ }
175
+ /** The node a flow starts at: the first with no next edge coming in. */
176
+ export function startOf(flow) {
177
+ const into = new Set(flow.edges.filter((e) => e.kind === "next").map((e) => e.to));
178
+ return flow.nodes.find((n) => !into.has(n.id))?.id ?? null;
179
+ }
180
+ /** The default exit a new loop tests: the first boolean fact of the node that loops. */
181
+ function defaultUntil(registry, from) {
182
+ const fact = registry.facts.find((f) => f.type === "boolean" && f.node && (f.node === true || f.node.includes(from.kind)));
183
+ const op = operatorsFor(registry, fact).find((o) => !o.unary);
184
+ return fact && op ? { fact: fact.id, node: from.id, op: op.id, value: true } : undefined;
185
+ }
186
+ /**
187
+ * Whether a connection makes a legal edge, and the edge when it does. Next
188
+ * edges go from an exit into `in`, one per exit (a fan-out excepted), and
189
+ * never close a cycle. Loops go from `back` into an earlier node's `back-in`.
190
+ * Data goes from an output to an input of a type it fits, on a node that runs
191
+ * after it, one source per input. The canvas only lands what this accepts.
192
+ */
193
+ export function canConnect(flow, c, registry, definition) {
194
+ const a = flow.nodes.find((n) => n.id === c.from);
195
+ const b = flow.nodes.find((n) => n.id === c.to);
196
+ const refuse = (reason, values = {}) => ({ ok: false, reason, values });
197
+ if (!a || !b)
198
+ return refuse("missing-node");
199
+ const names = { from: a.label, to: b.label };
200
+ if (a.id === b.id)
201
+ return refuse("self", names);
202
+ if (c.source.startsWith("output:")) {
203
+ const out = outputsOf(a, registry, definition).find((p) => `output:${p.id}` === c.source);
204
+ if (!out)
205
+ return refuse("no-output", names);
206
+ if (!c.target.startsWith("input:"))
207
+ return refuse("data-target", names);
208
+ const input = inputsOf(b, registry, definition).find((p) => `input:${p.id}` === c.target);
209
+ if (!input)
210
+ return refuse("no-input", names);
211
+ const types = {
212
+ ...names,
213
+ output: out.label,
214
+ input: input.label,
215
+ given: out.type,
216
+ wanted: input.type,
217
+ };
218
+ if (!typeAccepts(registry, out.type, input.type))
219
+ return refuse("type-mismatch", types);
220
+ if (!downstream(flow, a.id).has(b.id))
221
+ return refuse("data-order", types);
222
+ if (flow.edges.some((e) => e.kind === "data" && e.to === b.id && e.input === input.id))
223
+ return refuse("input-taken", types);
224
+ return {
225
+ ok: true,
226
+ edge: { kind: "data", from: a.id, to: b.id, port: out.id, input: input.id },
227
+ };
228
+ }
229
+ if (c.source === LOOP_OUT) {
230
+ if (c.target !== LOOP_IN)
231
+ return refuse("loop-target", names);
232
+ if (!downstream(flow, b.id).has(a.id))
233
+ return refuse("loop-not-back", names);
234
+ if (flow.edges.some((e) => e.kind === "loop" && e.from === a.id && e.to === b.id))
235
+ return refuse("loop-exists", names);
236
+ const until = defaultUntil(registry, a);
237
+ return {
238
+ ok: true,
239
+ edge: { kind: "loop", from: a.id, to: b.id, max: 3, ...(until ? { until } : {}) },
240
+ };
241
+ }
242
+ if (c.target !== IN)
243
+ return refuse("next-target", names);
244
+ if (!exitsOf(a).includes(c.source))
245
+ return refuse("no-exit", { ...names, exit: c.source });
246
+ const sameExit = (e) => e.kind === "next" && e.from === a.id && portOf(e) === c.source;
247
+ if (flow.edges.some((e) => sameExit(e) && e.to === b.id))
248
+ return refuse("exists", names);
249
+ if (a.kind !== "fanout" && flow.edges.some(sameExit))
250
+ return refuse("exit-taken", { ...names, exit: exitLabel(a, c.source) });
251
+ if (downstream(flow, b.id).has(a.id))
252
+ return refuse("cycle", names);
253
+ return {
254
+ ok: true,
255
+ edge: { kind: "next", from: a.id, to: b.id, ...(c.source === OUT ? {} : { port: c.source }) },
256
+ };
257
+ }
258
+ // ---------------------------------------------------------------- edits
259
+ /** A fresh node id from a label, not taken. */
260
+ export function freshId(label, taken) {
261
+ const set = new Set(taken);
262
+ const base = label
263
+ .toLowerCase()
264
+ .replace(/[^a-z0-9]+/g, "-")
265
+ .replace(/^-+|-+$/g, "")
266
+ .slice(0, 24) || "node";
267
+ let id = base;
268
+ for (let i = 2; set.has(id); i++)
269
+ id = `${base}-${i}`;
270
+ return id;
271
+ }
272
+ /**
273
+ * A new node with workable defaults. Labels come from the preset or gate kind
274
+ * when there is one, else from `label` (the caller's copy for the kind).
275
+ */
276
+ export function createNode(template, id, registry, label) {
277
+ const named = template.label ?? label;
278
+ switch (template.kind) {
279
+ case "step": {
280
+ const found = findPreset(registry, template.preset);
281
+ return found
282
+ ? { id, kind: "step", label: template.label ?? found.preset.label, preset: template.preset }
283
+ : { id, kind: "step", label: named, inputs: [], outputs: [] };
284
+ }
285
+ case "subflow":
286
+ return { id, kind: "subflow", label: named, flow: "" };
287
+ case "gate": {
288
+ const kind = findGateKind(registry, template.gate ?? "") ?? registry.gateKinds[0];
289
+ const fields = kind?.fields ?? [];
290
+ return {
291
+ id,
292
+ kind: "gate",
293
+ label: template.label ?? kind?.label ?? named,
294
+ gate: kind?.id ?? "",
295
+ ...(fields.includes("approvers") || fields.includes("timeout")
296
+ ? {
297
+ policy: fields.includes("approvers")
298
+ ? { roles: registry.roles?.[0] ? [registry.roles[0].id] : [], quorum: 1 }
299
+ : {},
300
+ }
301
+ : {}),
302
+ ...(fields.includes("threshold") ? { threshold: 0.7 } : {}),
303
+ ...(fields.includes("rule") ? { rule: "" } : {}),
304
+ };
305
+ }
306
+ case "condition": {
307
+ const fact = registry.facts[0];
308
+ const op = operatorsFor(registry, fact)[0];
309
+ return {
310
+ id,
311
+ kind: "condition",
312
+ label: named,
313
+ cases: [
314
+ {
315
+ id: "c1",
316
+ label: "case 1",
317
+ when: { fact: fact?.id ?? "", op: op?.id ?? "", ...(op?.unary ? {} : { value: "" }) },
318
+ },
319
+ ],
320
+ };
321
+ }
322
+ case "goto":
323
+ return { id, kind: "goto", label: named, target: "", max: 2 };
324
+ case "fanout":
325
+ return { id, kind: "fanout", label: named };
326
+ case "join":
327
+ return { id, kind: "join", label: named, wait: "all" };
328
+ case "variants":
329
+ return {
330
+ id,
331
+ kind: "variants",
332
+ label: named,
333
+ ...(template.preset ? { preset: template.preset } : {}),
334
+ variants: [
335
+ { id: "a", label: "A" },
336
+ { id: "b", label: "B" },
337
+ ],
338
+ select: "first-passing",
339
+ };
340
+ case "evaluator":
341
+ return { id, kind: "evaluator", label: named, levels: [], threshold: 0.7, routes: true };
342
+ case "classifier":
343
+ return { id, kind: "classifier", label: named, options: [], other: true };
344
+ case "check":
345
+ return { id, kind: "check", label: named, question: "", threshold: 0.7 };
346
+ }
347
+ }
348
+ /** Adds a node, chained after `after` (or the last node) when an exit there is free. */
349
+ export function addNode(flow, node, after) {
350
+ const from = flow.nodes.find((n) => n.id === after) ?? flow.nodes.at(-1);
351
+ const nodes = [...flow.nodes, node];
352
+ if (!from)
353
+ return { ...flow, nodes };
354
+ const exit = exitsOf(from).find((p) => from.kind === "fanout" ||
355
+ !flow.edges.some((e) => e.kind === "next" && e.from === from.id && portOf(e) === p));
356
+ if (!exit)
357
+ return { ...flow, nodes };
358
+ const edge = {
359
+ kind: "next",
360
+ from: from.id,
361
+ to: node.id,
362
+ ...(exit === OUT ? {} : { port: exit }),
363
+ };
364
+ return { ...flow, nodes, edges: [...flow.edges, edge] };
365
+ }
366
+ /** Removes a node and every edge touching it; a go-to or a condition pointing at it loses the reference. */
367
+ export function removeNode(flow, id) {
368
+ return {
369
+ ...flow,
370
+ nodes: flow.nodes
371
+ .filter((n) => n.id !== id)
372
+ .map((n) => {
373
+ if (n.kind === "goto" && n.target === id)
374
+ return { ...n, target: "" };
375
+ if (n.kind === "variants" && n.evaluator === id)
376
+ return { ...n, evaluator: undefined };
377
+ return n;
378
+ }),
379
+ edges: flow.edges.filter((e) => e.from !== id && e.to !== id),
380
+ };
381
+ }
382
+ /** Replaces a node's fields, then drops next edges from exits it no longer has. */
383
+ export function updateNode(flow, id, patch) {
384
+ const nodes = flow.nodes.map((n) => (n.id === id ? { ...n, ...patch } : n));
385
+ const node = nodes.find((n) => n.id === id);
386
+ if (!node)
387
+ return flow;
388
+ const exits = new Set(exitsOf(node));
389
+ return {
390
+ ...flow,
391
+ nodes,
392
+ edges: flow.edges.filter((e) => e.kind !== "next" || e.from !== id || exits.has(portOf(e))),
393
+ };
394
+ }
395
+ /** Moves a node; positions are part of the document. */
396
+ export function moveNode(flow, id, at) {
397
+ return {
398
+ ...flow,
399
+ nodes: flow.nodes.map((n) => n.id === id ? { ...n, at: { x: Math.round(at.x), y: Math.round(at.y) } } : n),
400
+ };
401
+ }
402
+ export function removeEdge(flow, id) {
403
+ return { ...flow, edges: flow.edges.filter((e) => edgeId(e) !== id) };
404
+ }
405
+ export function updateEdge(flow, id, patch) {
406
+ return { ...flow, edges: flow.edges.map((e) => (edgeId(e) === id ? { ...e, ...patch } : e)) };
407
+ }
408
+ /** The definition with one flow replaced. */
409
+ export function withFlow(definition, name, flow) {
410
+ return { ...definition, flows: { ...definition.flows, [name]: flow } };
411
+ }
@@ -0,0 +1,17 @@
1
+ import type { WorkflowFlow } from "../definition.js";
2
+ import type { WorkflowDefinitionDiff } from "../portable.js";
3
+ export interface WorkflowDiffProps {
4
+ /** From `diffDefinitions(before, after)`. */
5
+ diff: WorkflowDefinitionDiff;
6
+ /** The flows, to name them by their labels. */
7
+ flows?: Readonly<Record<string, Pick<WorkflowFlow, "label">>>;
8
+ /** Names the comparison ("Draft against live"). */
9
+ label?: string;
10
+ className?: string;
11
+ }
12
+ /**
13
+ * What a change does to what runs: details, flows,
14
+ * nodes and connections added, removed or changed, flow by flow. Moved nodes
15
+ * are counted apart, since a move does not change a run.
16
+ */
17
+ export declare function WorkflowDiff({ diff: d, flows, label, className }: WorkflowDiffProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { cn } from "../../lib/cn.js";
4
+ import { useUI } from "../../provider/UIProvider.js";
5
+ /**
6
+ * What a change does to what runs: details, flows,
7
+ * nodes and connections added, removed or changed, flow by flow. Moved nodes
8
+ * are counted apart, since a move does not change a run.
9
+ */
10
+ export function WorkflowDiff({ diff: d, flows, label, className }) {
11
+ const { messages } = useUI();
12
+ const m = messages.workspace;
13
+ const flowName = (name) => flows?.[name]?.label ?? name;
14
+ const edge = (e) => messages.editor.edgeName(e.kind, e.from, e.to);
15
+ const moved = d.flows.reduce((n, f) => n + f.moved.length, 0);
16
+ const row = (change, what, items) => items.length ? (_jsxs("li", { className: "flex gap-2", children: [_jsx("span", { "aria-hidden": true, className: cn("w-3 shrink-0 text-center font-mono", change === "added" && "text-success-fg", change === "removed" && "text-danger-fg", change === "changed" && "text-warning-fg"), children: change === "added" ? "+" : change === "removed" ? "-" : "~" }), _jsxs("span", { children: [m.change(change, what), ": ", items.join(", ")] })] })) : null;
17
+ return (_jsx("section", { "aria-label": label ?? m.diff, "data-slot": "workflow-diff", className: cn("flex flex-col gap-3 text-sm", className), children: d.same ? (_jsx("p", { className: "text-muted-foreground", children: moved ? m.onlyMoved(moved) : m.noChanges })) : (_jsxs(_Fragment, { children: [d.details.length || d.flowsAdded.length || d.flowsRemoved.length ? (_jsxs("ul", { className: "flex flex-col gap-1", children: [row("changed", m.details, d.details.map((k) => m.detail(k))), row("added", m.flows, d.flowsAdded.map(flowName)), row("removed", m.flows, d.flowsRemoved.map(flowName))] })) : null, d.flows.map((f) => [
18
+ f.nodesAdded,
19
+ f.nodesRemoved,
20
+ f.nodesChanged,
21
+ f.edgesAdded,
22
+ f.edgesRemoved,
23
+ f.edgesChanged,
24
+ ].some((l) => l.length) ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("h3", { className: "text-xs font-medium text-muted-foreground", children: flowName(f.flow) }), _jsxs("ul", { className: "flex flex-col gap-1", children: [row("added", m.nodes, f.nodesAdded.map((n) => n.label)), row("removed", m.nodes, f.nodesRemoved.map((n) => n.label)), row("changed", m.nodes, f.nodesChanged.map((n) => n.label)), row("added", m.connections, f.edgesAdded.map(edge)), row("removed", m.connections, f.edgesRemoved.map(edge)), row("changed", m.connections, f.edgesChanged.map(edge))] })] }, f.flow)) : null), moved ? _jsx("p", { className: "text-muted-foreground", children: m.moved(moved) }) : null] })) }));
25
+ }
@@ -0,0 +1,19 @@
1
+ import type { WorkflowImportSummary as Preview } from "../portable.js";
2
+ export interface WorkflowImportPreviewProps {
3
+ /** From `previewWorkflowImport`, against this destination's registry. */
4
+ preview: Preview;
5
+ /** Imports it as the host decides (a new draft, or over the definition it replaces). Omitted: preview only. */
6
+ onConfirm?: () => void;
7
+ onCancel?: () => void;
8
+ /** While the host imports. */
9
+ busy?: boolean;
10
+ className?: string;
11
+ }
12
+ /**
13
+ * What importing a package would do here, before it does it: whether this
14
+ * destination could run it, each module it needs as installed here, the
15
+ * capabilities and vocabulary missing, the nodes that would come in disabled,
16
+ * the gates that need approvers here (named people never travel), the
17
+ * problems, and, when it replaces a definition, the diff.
18
+ */
19
+ export declare function WorkflowImportPreview({ preview, onConfirm, onCancel, busy, className, }: WorkflowImportPreviewProps): import("react").JSX.Element;
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../../lib/cn.js";
4
+ import { Button } from "../../primitives/Button.js";
5
+ import { useUI } from "../../provider/UIProvider.js";
6
+ import { StatusBadge } from "../../status/StatusBadge.js";
7
+ import { blockingProblems } from "../problems.js";
8
+ import { WorkflowDiff } from "./WorkflowDiff.js";
9
+ const MODULE_TONE = {
10
+ available: "ok",
11
+ version: "warn",
12
+ unavailable: "warn",
13
+ missing: "error",
14
+ };
15
+ const AVAILABILITY_TONE = {
16
+ available: "ok",
17
+ "missing-capabilities": "warn",
18
+ unsupported: "error",
19
+ };
20
+ /**
21
+ * What importing a package would do here, before it does it: whether this
22
+ * destination could run it, each module it needs as installed here, the
23
+ * capabilities and vocabulary missing, the nodes that would come in disabled,
24
+ * the gates that need approvers here (named people never travel), the
25
+ * problems, and, when it replaces a definition, the diff.
26
+ */
27
+ export function WorkflowImportPreview({ preview, onConfirm, onCancel, busy = false, className, }) {
28
+ const { messages } = useUI();
29
+ const m = messages.workspace;
30
+ const blocking = blockingProblems(preview.problems).length;
31
+ const unknown = [
32
+ ...preview.unknownGateKinds.map((g) => m.unknownGateKind(g)),
33
+ ...preview.unknownFacts.map((f) => m.unknownFact(f)),
34
+ ...preview.unknownOperators.map((o) => m.unknownOperator(o)),
35
+ ];
36
+ const section = (title, children) => (_jsxs("section", { "aria-label": title, className: "flex flex-col gap-1.5", children: [_jsx("h3", { className: "text-xs font-medium text-muted-foreground", children: title }), children] }));
37
+ return (_jsxs("div", { "data-slot": "workflow-import-preview", className: cn("flex flex-col gap-4 text-sm", className), children: [_jsxs("p", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "font-medium", children: preview.definition.name }), " ", _jsx(StatusBadge, { tone: AVAILABILITY_TONE[preview.availability.status], children: m.importAvailability(preview.availability.status) })] }), section(m.modules, preview.modules.length ? (_jsx("ul", { className: "flex flex-col gap-1", children: preview.modules.map((mod) => (_jsxs("li", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { children: messages.workflow.module(mod.label, mod.version) }), " ", _jsx(StatusBadge, { tone: MODULE_TONE[mod.status], children: m.moduleStatus(mod.status, mod.installed) })] }, mod.id))) })) : (_jsx("p", { className: "text-muted-foreground", children: m.noModules }))), preview.missingCapabilities.length
38
+ ? section(m.missingCapabilities, _jsx("p", { children: preview.missingCapabilities.join(", ") }))
39
+ : null, unknown.length
40
+ ? section(m.unknownVocabulary, _jsx("ul", { className: "flex flex-col gap-1", children: unknown.map((u) => (_jsx("li", { children: u }, u))) }))
41
+ : null, preview.disabled.length
42
+ ? section(m.disabledNodes(preview.disabled.length), _jsx("ul", { className: "flex flex-col gap-1", children: preview.disabled.map((n) => (_jsxs("li", { children: [n.label, _jsxs("span", { className: "text-muted-foreground", children: [", ", n.availability.status === "missing-capabilities"
43
+ ? messages.workflow.missingCapabilities(n.availability.missing ?? [])
44
+ : (n.availability.reason ?? messages.workflow.unsupported)] })] }, `${n.flow}/${n.id}`))) }))
45
+ : null, preview.gatesNeedingApprovers.length
46
+ ? section(m.needsApprovers, _jsx("p", { children: preview.gatesNeedingApprovers.map((g) => g.label).join(", ") }))
47
+ : null, section(messages.editor.problems, _jsx("p", { children: blocking ? m.importBlocking(blocking) : messages.editor.noProblems })), preview.diff
48
+ ? section(m.replaces, _jsx(WorkflowDiff, { diff: preview.diff, flows: preview.definition.flows, label: m.replaces }))
49
+ : null, onConfirm || onCancel ? (_jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [onCancel ? (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: busy, children: messages.editor.cancel })) : null, onConfirm ? (_jsx(Button, { type: "button", onClick: onConfirm, disabled: busy, children: busy ? m.importing : m.importAction })) : null] })) : null] }));
50
+ }
@@ -0,0 +1,40 @@
1
+ import * as React from "react";
2
+ import { type WorkflowCondition, type WorkflowDefinition, type WorkflowFlow, type WorkflowGatePolicy, type WorkflowRegistry, type WorkflowSelection } from "../definition.js";
3
+ export interface WorkflowInspectorProps {
4
+ definition: WorkflowDefinition;
5
+ registry: WorkflowRegistry;
6
+ selection?: WorkflowSelection | null;
7
+ /** Every edit, as the whole next definition. Omitted, or `readOnly`: the fields only show. */
8
+ onChange?: (definition: WorkflowDefinition) => void;
9
+ /** After a delete from the inspector, the selection is cleared through this. */
10
+ onSelectionChange?: (selection: WorkflowSelection | null) => void;
11
+ readOnly?: boolean;
12
+ className?: string;
13
+ }
14
+ /** One comparison on a run fact: the fact, its node when it is a node's, the operator and the value. */
15
+ export declare function ConditionEditor({ condition, onChange, flow, registry, label, disabled, }: {
16
+ condition: WorkflowCondition;
17
+ onChange: (condition: WorkflowCondition) => void;
18
+ flow: WorkflowFlow;
19
+ registry: WorkflowRegistry;
20
+ /** Names the group, so several on a page are told apart. */
21
+ label: string;
22
+ disabled?: boolean;
23
+ }): React.JSX.Element;
24
+ /** Who approves a gate: roles, named members, a quorum, and a timeout with what happens then. */
25
+ export declare function GatePolicyEditor({ policy, onChange, registry, approvers, timeout, disabled, }: {
26
+ policy: WorkflowGatePolicy;
27
+ onChange: (policy: WorkflowGatePolicy) => void;
28
+ registry: WorkflowRegistry;
29
+ approvers?: boolean;
30
+ timeout?: boolean;
31
+ disabled?: boolean;
32
+ }): React.JSX.Element;
33
+ /**
34
+ * The selected node's or connection's fields, edited in place: per-kind
35
+ * fields, a condition editor over the host's facts and operators, a gate
36
+ * policy editor (roles, members, quorum, timeout), and the host's model,
37
+ * harness and effort pickers. With nothing selected, the workflow's name and
38
+ * description. Every edit goes out as the whole next definition.
39
+ */
40
+ export declare function WorkflowInspector({ definition, registry, selection, onChange, onSelectionChange, readOnly, className, }: WorkflowInspectorProps): React.JSX.Element;