@calliopelabs/ui 0.6.1 → 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 +6 -4
  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 +415 -2
  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,409 @@
1
+ /**
2
+ * What is wrong with a workflow definition, at three levels, and whether this
3
+ * destination can run each node. `blocking` problems stop a publish,
4
+ * `warning`s do not, and `not-run` marks a node this destination does not run
5
+ * (yet): its module, a capability or its kind is missing here. Problems carry
6
+ * a code and values, never English; the editor's copy names them. Pure.
7
+ */
8
+ import { downstream, edgeId, exitLabel, exitsOf, findFact, findGateKind, findPreset, inputsOf, operatorsFor, outputsOf, portOf, startOf, typeAccepts, upstream, } from "./definition.js";
9
+ export const WORKFLOW_PROBLEM_CODES = [
10
+ // the definition
11
+ "no-main",
12
+ "flow-empty",
13
+ // nodes and edges
14
+ "duplicate-id",
15
+ "unreachable",
16
+ "dangling-edge",
17
+ "missing-exit",
18
+ "cycle",
19
+ "loop-no-until",
20
+ "loop-no-max",
21
+ "loop-not-back",
22
+ "exit-unwired",
23
+ // data
24
+ "input-unbound",
25
+ "data-port-missing",
26
+ "data-type",
27
+ "data-order",
28
+ // steps
29
+ "unknown-preset",
30
+ "unknown-model",
31
+ "unknown-harness",
32
+ "unknown-effort",
33
+ // sub-flows
34
+ "subflow-none",
35
+ "subflow-missing",
36
+ "subflow-recursive",
37
+ // gates
38
+ "unknown-gate-kind",
39
+ "gate-no-approver",
40
+ "gate-quorum",
41
+ "gate-no-threshold",
42
+ "gate-no-rule",
43
+ "gate-no-timeout-action",
44
+ // conditions
45
+ "no-cases",
46
+ "unknown-fact",
47
+ "unknown-operator",
48
+ "operator-mismatch",
49
+ "value-missing",
50
+ "value-type",
51
+ "fact-node-missing",
52
+ // logic
53
+ "goto-no-target",
54
+ "goto-missing-target",
55
+ "goto-no-max",
56
+ "fanout-few",
57
+ "join-few",
58
+ "join-no-n",
59
+ "variants-few",
60
+ "variants-no-select",
61
+ "variants-no-evaluator",
62
+ // judges
63
+ "rubric-short",
64
+ "no-threshold",
65
+ "no-question",
66
+ "options-few",
67
+ "option-duplicate",
68
+ // executability
69
+ "not-runnable",
70
+ ];
71
+ const AVAILABLE = { status: "available" };
72
+ function merge(parts) {
73
+ const unsupported = parts.find((p) => p.status === "unsupported");
74
+ if (unsupported)
75
+ return unsupported;
76
+ const missing = parts.filter((p) => p.status === "missing-capabilities");
77
+ if (!missing.length)
78
+ return AVAILABLE;
79
+ const names = [...new Set(missing.flatMap((p) => p.missing ?? []))];
80
+ const reason = missing.length === 1 ? missing[0]?.reason : undefined;
81
+ return { status: "missing-capabilities", missing: names, ...(reason ? { reason } : {}) };
82
+ }
83
+ function capabilities(registry, needs) {
84
+ if (!registry.capabilities || !needs?.length)
85
+ return AVAILABLE;
86
+ const missing = needs.filter((c) => !registry.capabilities?.includes(c));
87
+ return missing.length ? { status: "missing-capabilities", missing } : AVAILABLE;
88
+ }
89
+ /**
90
+ * Whether this destination can run a node, reusing the gallery's
91
+ * availability: `unsupported` when its kind, module, preset or gate kind is
92
+ * not here, `missing-capabilities` with the names when the module is but a
93
+ * capability it needs is not.
94
+ */
95
+ export function nodeAvailability(node, registry) {
96
+ const parts = [registry.nodeKinds?.[node.kind] ?? AVAILABLE];
97
+ if ((node.kind === "step" || node.kind === "variants") && node.preset) {
98
+ const module = registry.modules.find((m) => m.id === node.preset?.module);
99
+ const found = findPreset(registry, node.preset);
100
+ if (!module || !found)
101
+ parts.push({ status: "unsupported" });
102
+ else {
103
+ parts.push(module.availability ?? AVAILABLE);
104
+ parts.push(capabilities(registry, found.preset.capabilities));
105
+ }
106
+ }
107
+ if (node.kind === "gate") {
108
+ const kind = findGateKind(registry, node.gate);
109
+ if (!kind)
110
+ parts.push({ status: "unsupported" });
111
+ else
112
+ parts.push(kind.availability ?? AVAILABLE, capabilities(registry, kind.capabilities));
113
+ }
114
+ return merge(parts);
115
+ }
116
+ /** Whether this destination can run the whole definition: every node of every flow. */
117
+ export function definitionAvailability(definition, registry) {
118
+ return merge(Object.values(definition.flows).flatMap((f) => f.nodes.map((n) => nodeAvailability(n, registry))));
119
+ }
120
+ function checkCondition(flow, c, registry, push) {
121
+ const fact = findFact(registry, c.fact);
122
+ if (!fact)
123
+ return push("unknown-fact", { fact: c.fact });
124
+ const op = registry.operators.find((o) => o.id === c.op);
125
+ if (!op)
126
+ return push("unknown-operator", { fact: fact.label, op: c.op });
127
+ if (!operatorsFor(registry, fact).includes(op))
128
+ push("operator-mismatch", { fact: fact.label, op: op.label });
129
+ if (fact.node) {
130
+ const n = flow.nodes.find((x) => x.id === c.node);
131
+ if (!n || (fact.node !== true && !fact.node.includes(n.kind)))
132
+ push("fact-node-missing", { fact: fact.label });
133
+ }
134
+ if (op.unary)
135
+ return;
136
+ if (c.value === undefined || c.value === "")
137
+ return push("value-missing", { fact: fact.label });
138
+ const ok = fact.type === "number"
139
+ ? typeof c.value === "number"
140
+ : fact.type === "boolean"
141
+ ? typeof c.value === "boolean"
142
+ : fact.type === "enum"
143
+ ? !fact.values || fact.values.some((v) => v.id === c.value)
144
+ : typeof c.value === "string";
145
+ if (!ok)
146
+ push("value-type", { fact: fact.label });
147
+ }
148
+ function checkFlow(name, flow, definition, registry, push) {
149
+ const ids = new Set();
150
+ const label = (id) => flow.nodes.find((n) => n.id === id)?.label ?? id;
151
+ for (const n of flow.nodes) {
152
+ if (ids.has(n.id))
153
+ push("blocking", "duplicate-id", { node: n.id }, { id: n.id });
154
+ ids.add(n.id);
155
+ }
156
+ if (!flow.nodes.length)
157
+ push("warning", "flow-empty", {}, { flow: flow.label ?? name });
158
+ for (const e of flow.edges) {
159
+ const at = { edge: edgeId(e) };
160
+ const a = flow.nodes.find((n) => n.id === e.from);
161
+ const b = flow.nodes.find((n) => n.id === e.to);
162
+ if (!a || !b) {
163
+ push("blocking", "dangling-edge", at, { from: label(e.from), to: label(e.to) });
164
+ continue;
165
+ }
166
+ const names = { from: a.label, to: b.label };
167
+ if (e.kind === "next" && !exitsOf(a).includes(portOf(e)))
168
+ push("blocking", "missing-exit", at, { ...names, exit: portOf(e) });
169
+ if (e.kind === "loop") {
170
+ if (!e.until)
171
+ push("blocking", "loop-no-until", at, names);
172
+ else
173
+ checkCondition(flow, e.until, registry, (code, v) => push("blocking", code, at, { ...names, ...v }));
174
+ if (!e.max)
175
+ push("blocking", "loop-no-max", at, names);
176
+ if (!downstream(flow, b.id).has(a.id))
177
+ push("blocking", "loop-not-back", at, names);
178
+ }
179
+ if (e.kind === "data") {
180
+ const out = outputsOf(a, registry, definition).find((p) => p.id === e.port);
181
+ const input = inputsOf(b, registry, definition).find((p) => p.id === e.input);
182
+ if (!out || !input)
183
+ push("blocking", "data-port-missing", at, {
184
+ ...names,
185
+ output: e.port ?? "",
186
+ input: e.input ?? "",
187
+ });
188
+ else {
189
+ const v = {
190
+ ...names,
191
+ output: out.label,
192
+ input: input.label,
193
+ given: out.type,
194
+ wanted: input.type,
195
+ };
196
+ if (!typeAccepts(registry, out.type, input.type))
197
+ push("blocking", "data-type", at, v);
198
+ if (!downstream(flow, a.id).has(b.id))
199
+ push("blocking", "data-order", at, v);
200
+ }
201
+ }
202
+ }
203
+ // Next edges never cycle: going back is a loop edge, with a count and an exit.
204
+ const colour = new Map();
205
+ const visit = (id) => {
206
+ if (colour.get(id) === 2)
207
+ return false;
208
+ if (colour.get(id) === 1)
209
+ return true;
210
+ colour.set(id, 1);
211
+ const cyc = flow.edges.some((e) => e.kind === "next" && e.from === id && visit(e.to));
212
+ colour.set(id, 2);
213
+ return cyc;
214
+ };
215
+ if (flow.nodes.some((n) => visit(n.id)))
216
+ push("blocking", "cycle", {}, { flow: flow.label ?? name });
217
+ const start = startOf(flow);
218
+ if (start) {
219
+ const seen = new Set([start]);
220
+ const stack = [start];
221
+ while (stack.length) {
222
+ const id = stack.pop();
223
+ const n = flow.nodes.find((x) => x.id === id);
224
+ const nexts = flow.edges.filter((e) => e.kind === "next" && e.from === id).map((e) => e.to);
225
+ if (n?.kind === "goto" && n.target)
226
+ nexts.push(n.target);
227
+ for (const t of nexts)
228
+ if (!seen.has(t)) {
229
+ seen.add(t);
230
+ stack.push(t);
231
+ }
232
+ }
233
+ for (const n of flow.nodes)
234
+ if (!seen.has(n.id))
235
+ push("warning", "unreachable", { node: n.id }, { node: n.label });
236
+ }
237
+ const nextOut = (id) => flow.edges.filter((e) => e.kind === "next" && e.from === id);
238
+ const nextIn = (id) => flow.edges.filter((e) => e.kind === "next" && e.to === id);
239
+ const picks = (list, value) => !list || !value || list.some((o) => o.id === value);
240
+ for (const n of flow.nodes) {
241
+ const at = { node: n.id };
242
+ const v = { node: n.label };
243
+ const block = (code, more = {}) => push("blocking", code, at, { ...v, ...more });
244
+ const warn = (code, more = {}) => push("warning", code, at, { ...v, ...more });
245
+ // Inputs a node needs: bound by a data edge, or by type to something before it.
246
+ const required = inputsOf(n, registry, definition).filter((p) => !p.optional);
247
+ if (required.length) {
248
+ const before = upstream(flow, n.id);
249
+ const available = [
250
+ ...(flow.inputs ?? []),
251
+ ...flow.nodes
252
+ .filter((x) => before.has(x.id))
253
+ .flatMap((x) => outputsOf(x, registry, definition)),
254
+ ];
255
+ for (const p of required) {
256
+ const bound = flow.edges.some((e) => e.kind === "data" && e.to === n.id && e.input === p.id);
257
+ if (!bound && !available.some((o) => typeAccepts(registry, o.type, p.type)))
258
+ block("input-unbound", { input: p.label, type: p.type });
259
+ }
260
+ }
261
+ switch (n.kind) {
262
+ case "step":
263
+ case "variants": {
264
+ if (n.preset && !findPreset(registry, n.preset))
265
+ warn("unknown-preset", { module: n.preset.module, preset: n.preset.id });
266
+ const runs = n.kind === "step" ? [n] : n.variants;
267
+ for (const r of runs) {
268
+ if (!picks(registry.models, r.model))
269
+ warn("unknown-model", { value: r.model ?? "" });
270
+ if (!picks(registry.harnesses, r.harness))
271
+ warn("unknown-harness", { value: r.harness ?? "" });
272
+ if (!picks(registry.efforts, r.effort))
273
+ warn("unknown-effort", { value: r.effort ?? "" });
274
+ }
275
+ if (n.kind === "variants") {
276
+ if (n.variants.length < 2)
277
+ block("variants-few");
278
+ if (!n.select)
279
+ block("variants-no-select");
280
+ if (n.select === "evaluator" &&
281
+ flow.nodes.find((x) => x.id === n.evaluator)?.kind !== "evaluator")
282
+ block("variants-no-evaluator");
283
+ }
284
+ break;
285
+ }
286
+ case "subflow":
287
+ if (!n.flow)
288
+ block("subflow-none");
289
+ else if (!definition.flows[n.flow])
290
+ block("subflow-missing", { flow: n.flow });
291
+ else if (n.flow === name || callsFlow(definition, n.flow, name))
292
+ block("subflow-recursive", { flow: n.flow });
293
+ break;
294
+ case "gate": {
295
+ const kind = findGateKind(registry, n.gate);
296
+ if (!kind) {
297
+ block("unknown-gate-kind", { gate: n.gate });
298
+ break;
299
+ }
300
+ const p = n.policy ?? {};
301
+ if (kind.fields.includes("approvers")) {
302
+ const people = (p.roles?.length ?? 0) + (p.members?.length ?? 0);
303
+ if (!people)
304
+ block("gate-no-approver");
305
+ else if (!p.roles?.length && (p.quorum ?? 1) > (p.members?.length ?? 0))
306
+ block("gate-quorum", { quorum: p.quorum ?? 1, members: p.members?.length ?? 0 });
307
+ }
308
+ if (kind.fields.includes("threshold") && n.threshold === undefined)
309
+ block("gate-no-threshold");
310
+ if (kind.fields.includes("rule") && !n.rule?.trim())
311
+ block("gate-no-rule");
312
+ if (p.timeoutHours && !p.onTimeout)
313
+ warn("gate-no-timeout-action");
314
+ break;
315
+ }
316
+ case "condition":
317
+ if (!n.cases.length)
318
+ block("no-cases");
319
+ for (const c of n.cases)
320
+ checkCondition(flow, c.when, registry, (code, more) => block(code, { case: c.label, ...more }));
321
+ break;
322
+ case "goto":
323
+ if (!n.target)
324
+ block("goto-no-target");
325
+ else if (!ids.has(n.target) || n.target === n.id)
326
+ block("goto-missing-target", { target: n.target });
327
+ if (!n.max)
328
+ block("goto-no-max");
329
+ break;
330
+ case "fanout":
331
+ if (nextOut(n.id).length < 2)
332
+ warn("fanout-few");
333
+ break;
334
+ case "join":
335
+ if (nextIn(n.id).length < 2)
336
+ warn("join-few");
337
+ if (n.wait === "n" && !n.n)
338
+ block("join-no-n");
339
+ break;
340
+ case "evaluator":
341
+ if (n.levels.length < 2)
342
+ block("rubric-short");
343
+ if (n.routes && n.threshold === undefined)
344
+ block("no-threshold");
345
+ if (!n.question?.trim())
346
+ warn("no-question");
347
+ break;
348
+ case "classifier": {
349
+ if (n.options.length < 2)
350
+ block("options-few");
351
+ const labels = n.options.map((o) => o.label);
352
+ const twice = labels.find((l, i) => labels.indexOf(l) !== i);
353
+ if (twice !== undefined)
354
+ block("option-duplicate", { option: twice });
355
+ if (!n.question?.trim())
356
+ warn("no-question");
357
+ break;
358
+ }
359
+ case "check":
360
+ if (!n.question.trim())
361
+ block("no-question");
362
+ break;
363
+ }
364
+ // A routing node whose exit goes nowhere stops the run there.
365
+ if (exitsOf(n).length > 1)
366
+ for (const exit of exitsOf(n))
367
+ if (!nextOut(n.id).some((e) => portOf(e) === exit))
368
+ warn("exit-unwired", { exit: exitLabel(n, exit) });
369
+ const availability = nodeAvailability(n, registry);
370
+ if (availability.status !== "available")
371
+ push("not-run", "not-runnable", at, v, availability);
372
+ }
373
+ }
374
+ /** Whether flow `from` calls flow `target`, directly or through other sub-flows. */
375
+ function callsFlow(definition, from, target) {
376
+ const seen = new Set();
377
+ const stack = [from];
378
+ while (stack.length) {
379
+ const name = stack.pop();
380
+ if (seen.has(name))
381
+ continue;
382
+ seen.add(name);
383
+ for (const n of definition.flows[name]?.nodes ?? [])
384
+ if (n.kind === "subflow" && n.flow) {
385
+ if (n.flow === target)
386
+ return true;
387
+ stack.push(n.flow);
388
+ }
389
+ }
390
+ return false;
391
+ }
392
+ /** Every problem in a definition, against what this destination's registry says. */
393
+ export function checkDefinition(definition, registry) {
394
+ const out = [];
395
+ if (!definition.flows[definition.main])
396
+ out.push({ level: "blocking", code: "no-main", values: { flow: definition.main } });
397
+ for (const [name, flow] of Object.entries(definition.flows))
398
+ checkFlow(name, flow, definition, registry, (level, code, at = {}, values = {}, availability) => out.push({
399
+ level,
400
+ code,
401
+ flow: name,
402
+ ...at,
403
+ values,
404
+ ...(availability ? { availability } : {}),
405
+ }));
406
+ return out;
407
+ }
408
+ /** The problems that stop a publish. */
409
+ export const blockingProblems = (problems) => problems.filter((p) => p.level === "blocking");
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The run and review model the workflow run components read: stages,
3
+ * artifacts, gates, comments and decisions. Module-neutral: a stage is any
4
+ * unit of work, an artifact any typed output (a video, a contract, a filled
5
+ * form, a report), a gate any point where people or checks must say go.
6
+ * Times and sizes arrive formatted by the host, in its locale. Pure.
7
+ */
8
+ import type { StatusTone } from "../status/tones.js";
9
+ export type RunStageStatus = "pending" | "running" | "waiting" | "blocked" | "passed" | "failed" | "skipped" | "cancelled";
10
+ /** Each stage status's tone; `running` also pulses. */
11
+ export declare const RUN_STAGE_TONE: Record<RunStageStatus, StatusTone>;
12
+ /** One named value a stage took in or gave out, summarized for display. */
13
+ export interface RunValue {
14
+ label: string;
15
+ value: string;
16
+ }
17
+ /** One unit of work in a run. */
18
+ export interface RunStage {
19
+ id: string;
20
+ label: string;
21
+ status: RunStageStatus;
22
+ /** When it started, formatted by the host ("14:02", "Nov 14, 14:02"). */
23
+ startedAt?: string;
24
+ /** How long it took or has taken, formatted by the host ("2m 14s"). */
25
+ duration?: string;
26
+ /** Which attempt this is, from 1. Shown when above 1 or when `maxAttempts` is set. */
27
+ attempt?: number;
28
+ maxAttempts?: number;
29
+ inputs?: readonly RunValue[];
30
+ outputs?: readonly RunValue[];
31
+ /** One line: why it failed, what it waits for. */
32
+ detail?: string;
33
+ /** Set when the stage is a gate. */
34
+ gate?: Gate;
35
+ }
36
+ export type GateStatus = "waiting" | "approved" | "rejected" | "expired" | "skipped";
37
+ export declare const GATE_TONE: Record<GateStatus, StatusTone>;
38
+ /** A point where people or checks must say go. */
39
+ export interface Gate {
40
+ id: string;
41
+ /** What it asks, in the host's words ("Counsel sign-off", "Two approvers"). */
42
+ label: string;
43
+ status: GateStatus;
44
+ /** Approvals so far and needed, for a quorum. */
45
+ approvals?: number;
46
+ required?: number;
47
+ /** The decision that closed it. */
48
+ decision?: ReviewDecision;
49
+ }
50
+ /** A gate's or a review's closing decision. */
51
+ export interface ReviewDecision {
52
+ verdict: "approved" | "rejected";
53
+ /** Why, as the person wrote it. Required for a rejection. */
54
+ reason?: string;
55
+ by?: string;
56
+ /** Formatted by the host. */
57
+ at?: string;
58
+ }
59
+ /** Where a review stands. */
60
+ export type ReviewVerdict = "pending" | "approved" | "changes-requested" | "rejected";
61
+ export declare const VERDICT_TONE: Record<ReviewVerdict, StatusTone>;
62
+ export interface ReviewComment {
63
+ id: string;
64
+ author: string;
65
+ body: string;
66
+ /** Formatted by the host. */
67
+ at?: string;
68
+ /**
69
+ * Where in the artifact it points, as a module viewer labels it: a timecode
70
+ * ("00:01:12"), a region, a page and clause ("p. 3, 4.2"), a field.
71
+ */
72
+ anchor?: string;
73
+ resolved?: boolean;
74
+ }
75
+ export type ArtifactStatus = "draft" | "in-review" | "approved" | "rejected" | "superseded";
76
+ export declare const ARTIFACT_TONE: Record<ArtifactStatus, StatusTone>;
77
+ /** A typed output of a run. */
78
+ export interface Artifact {
79
+ id: string;
80
+ name: string;
81
+ /** The host's type key ("video", "contract", "form", "report"); viewers switch on it. */
82
+ kind: string;
83
+ /** The type's display name ("Video", "Contract"). */
84
+ kindLabel: string;
85
+ /** The MIME type, when there is one. */
86
+ mediaType?: string;
87
+ /** Formatted by the host ("4.2 MB", "12 pages", "0:30"). */
88
+ size?: string;
89
+ version?: string;
90
+ status?: ArtifactStatus;
91
+ /** The stage that produced it, by label. */
92
+ producedBy?: string;
93
+ summary?: string;
94
+ }
@@ -0,0 +1,31 @@
1
+ /** Each stage status's tone; `running` also pulses. */
2
+ export const RUN_STAGE_TONE = {
3
+ pending: "muted",
4
+ running: "info",
5
+ waiting: "warn",
6
+ blocked: "warn",
7
+ passed: "ok",
8
+ failed: "error",
9
+ skipped: "muted",
10
+ cancelled: "muted",
11
+ };
12
+ export const GATE_TONE = {
13
+ waiting: "warn",
14
+ approved: "ok",
15
+ rejected: "error",
16
+ expired: "muted",
17
+ skipped: "muted",
18
+ };
19
+ export const VERDICT_TONE = {
20
+ pending: "muted",
21
+ approved: "ok",
22
+ "changes-requested": "warn",
23
+ rejected: "error",
24
+ };
25
+ export const ARTIFACT_TONE = {
26
+ draft: "muted",
27
+ "in-review": "warn",
28
+ approved: "ok",
29
+ rejected: "error",
30
+ superseded: "muted",
31
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Where a workflow document stands against the host's revisions: unsaved
3
+ * edits, a draft that differs from what runs, and a conflict when the saved
4
+ * revision moved under unsaved edits. Persistence and authority stay with the
5
+ * host; this only compares. Pure.
6
+ */
7
+ import type { WorkflowDefinition } from "./definition.js";
8
+ /** A revision as the host's authority holds it. */
9
+ export interface WorkflowRevision {
10
+ /** Changes whenever the stored definition changes. */
11
+ id: string;
12
+ /** As the host labels it ("12", "2026-11-14.3"). */
13
+ label?: string;
14
+ /** Formatted by the host. */
15
+ at?: string;
16
+ by?: string;
17
+ definition: WorkflowDefinition;
18
+ }
19
+ export interface WorkflowDocumentStatus {
20
+ /** The working copy differs from the revision it started from (positions count). */
21
+ dirty: boolean;
22
+ /** What would run differs from what runs today (positions do not count). */
23
+ unpublished: boolean;
24
+ /** The saved revision moved to one this working copy did not start from, while it had edits. */
25
+ conflict: boolean;
26
+ /** Nothing has been saved or published yet. */
27
+ fresh: boolean;
28
+ }
29
+ /**
30
+ * Compares the working copy with the revision it started from (`base`), the
31
+ * host's latest saved revision, and the published one.
32
+ */
33
+ export declare function documentStatus(draft: WorkflowDefinition, base: WorkflowRevision | null | undefined, saved: WorkflowRevision | null | undefined, published: WorkflowRevision | null | undefined): WorkflowDocumentStatus;
@@ -0,0 +1,17 @@
1
+ import { sameDefinition } from "./portable.js";
2
+ /**
3
+ * Compares the working copy with the revision it started from (`base`), the
4
+ * host's latest saved revision, and the published one.
5
+ */
6
+ export function documentStatus(draft, base, saved, published) {
7
+ const start = base ?? saved ?? published;
8
+ const dirty = start ? !sameDefinition(draft, start.definition) : true;
9
+ return {
10
+ dirty,
11
+ unpublished: published
12
+ ? !sameDefinition(draft, published.definition, { positions: false })
13
+ : true,
14
+ conflict: !!(dirty && base && saved && saved.id !== base.id),
15
+ fresh: !saved && !published,
16
+ };
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calliopelabs/ui",
3
- "version": "0.6.1",
3
+ "version": "0.11.0",
4
4
  "description": "Calliope AI shared operator UI: app shell, navigation, page frame, data table and list patterns, status colours, design tokens and appearance for Astrolift AI and Zentinelle AI.",
5
5
  "keywords": [
6
6
  "calliope-ai",
@@ -32,6 +32,10 @@
32
32
  },
33
33
  "./theme.css": "./dist/styles/theme.css",
34
34
  "./preset.css": "./dist/styles/preset.css",
35
+ "./css/tokens.css": "./dist/css/tokens.css",
36
+ "./css/appearance.js": "./dist/css/appearance.js",
37
+ "./css/components.css": "./dist/css/components.css",
38
+ "./css/calliope.css": "./dist/css/calliope.css",
35
39
  "./package.json": "./package.json"
36
40
  },
37
41
  "main": "./dist/index.js",
@@ -55,6 +59,7 @@
55
59
  "build-storybook": "storybook build --quiet"
56
60
  },
57
61
  "dependencies": {
62
+ "@xyflow/react": "^12.12.1",
58
63
  "class-variance-authority": "^0.7.1",
59
64
  "clsx": "^2.1.1",
60
65
  "lucide-react": "^1.58.0",