@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,349 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { PlusIcon, Trash2Icon, XIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../../lib/cn.js";
6
+ import { Button } from "../../primitives/Button.js";
7
+ import { Checkbox } from "../../primitives/Checkbox.js";
8
+ import { Field } from "../../primitives/Field.js";
9
+ import { Input } from "../../primitives/Input.js";
10
+ import { Select } from "../../primitives/Select.js";
11
+ import { Switch } from "../../primitives/Switch.js";
12
+ import { Textarea } from "../../primitives/Textarea.js";
13
+ import { useUI } from "../../provider/UIProvider.js";
14
+ import { WORKFLOW_SELECTORS, dataTypes, edgeId, exitLabel, findFact, findGateKind, findPreset, freshId, inputsOf, operatorsFor, outputsOf, portOf, removeEdge, removeNode, updateEdge, updateNode, withFlow, } from "../definition.js";
15
+ const DEFAULT = "__default";
16
+ const NONE = "__none";
17
+ /** A number field that commits a number, or nothing when emptied. */
18
+ function NumberInput({ value, onValue, min, max, step, ...props }) {
19
+ return (_jsx(Input, { ...props, type: "number", inputMode: "decimal", min: min, max: max, step: step, value: value ?? "", onChange: (e) => {
20
+ const n = e.target.valueAsNumber;
21
+ onValue(e.target.value === "" || Number.isNaN(n) ? undefined : n);
22
+ } }));
23
+ }
24
+ /** A 0 to 1 value as a percentage. */
25
+ function PercentInput({ value, onValue, ...props }) {
26
+ return (_jsx(NumberInput, { ...props, min: 0, max: 100, step: 1, value: value === undefined ? undefined : Math.round(value * 100), onValue: (v) => onValue(v === undefined ? undefined : Math.min(1, Math.max(0, v / 100))) }));
27
+ }
28
+ /** A host picker: the host's options, with "the default" first. */
29
+ function HostPicker({ label, options, value, onValue, disabled, }) {
30
+ const { messages } = useUI();
31
+ if (!options?.length)
32
+ return null;
33
+ const known = !value || options.some((o) => o.id === value);
34
+ return (_jsx(Field, { label: label, children: _jsx(Select, { value: value ?? DEFAULT, disabled: disabled, onValueChange: (v) => onValue(v === DEFAULT ? undefined : v), options: [
35
+ { value: DEFAULT, label: messages.editor.hostDefault },
36
+ ...options.map((o) => ({ value: o.id, label: o.label })),
37
+ ...(known ? [] : [{ value: value, label: value }]),
38
+ ] }) }));
39
+ }
40
+ /** Ticks for a set of the host's options: roles, members. */
41
+ function OptionChecks({ legend, options, value, onValue, disabled, }) {
42
+ const id = React.useId();
43
+ return (_jsxs("fieldset", { className: "flex flex-col gap-1.5", children: [_jsx("legend", { className: "mb-1 text-sm font-medium", children: legend }), options.map((o) => (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { id: `${id}-${o.id}`, checked: value.includes(o.id), disabled: disabled, onCheckedChange: (checked) => onValue(checked === true ? [...value, o.id] : value.filter((v) => v !== o.id)) }), _jsx("label", { htmlFor: `${id}-${o.id}`, children: o.label })] }, o.id)))] }));
44
+ }
45
+ /** One comparison on a run fact: the fact, its node when it is a node's, the operator and the value. */
46
+ export function ConditionEditor({ condition, onChange, flow, registry, label, disabled = false, }) {
47
+ const { messages } = useUI();
48
+ const m = messages.editor;
49
+ const fact = findFact(registry, condition.fact);
50
+ const ops = operatorsFor(registry, fact);
51
+ const op = registry.operators.find((o) => o.id === condition.op);
52
+ const nodes = fact?.node
53
+ ? flow.nodes.filter((n) => fact.node === true || (Array.isArray(fact.node) && fact.node.includes(n.kind)))
54
+ : [];
55
+ const setFact = (id) => {
56
+ const next = findFact(registry, id);
57
+ const nextOp = operatorsFor(registry, next)[0];
58
+ onChange({
59
+ fact: id,
60
+ op: nextOp?.id ?? "",
61
+ ...(next?.node && condition.node ? { node: condition.node } : {}),
62
+ ...(nextOp?.unary
63
+ ? {}
64
+ : {
65
+ value: next?.type === "boolean"
66
+ ? true
67
+ : next?.type === "number"
68
+ ? 0
69
+ : next?.type === "enum"
70
+ ? (next.values?.[0]?.id ?? "")
71
+ : "",
72
+ }),
73
+ });
74
+ };
75
+ return (_jsxs("fieldset", { className: "flex flex-col gap-3 rounded-lg border border-border p-3", children: [_jsx("legend", { className: "px-1 text-sm font-medium", children: label }), _jsx(Field, { label: m.fact, children: _jsx(Select, { value: condition.fact || NONE, disabled: disabled, onValueChange: setFact, options: [
76
+ ...(fact
77
+ ? []
78
+ : [
79
+ {
80
+ value: condition.fact || NONE,
81
+ label: condition.fact || m.choose,
82
+ disabled: true,
83
+ },
84
+ ]),
85
+ ...registry.facts.map((f) => ({ value: f.id, label: f.label })),
86
+ ] }) }), fact?.node ? (_jsx(Field, { label: m.factNode, children: _jsx(Select, { value: condition.node ?? NONE, disabled: disabled, onValueChange: (v) => onChange({ ...condition, node: v }), options: [
87
+ ...(condition.node ? [] : [{ value: NONE, label: m.choose, disabled: true }]),
88
+ ...nodes.map((n) => ({ value: n.id, label: n.label })),
89
+ ] }) })) : null, _jsx(Field, { label: m.operator, children: _jsx(Select, { value: op && ops.includes(op) ? op.id : NONE, disabled: disabled, onValueChange: (v) => {
90
+ const next = registry.operators.find((o) => o.id === v);
91
+ const { value: _value, ...rest } = condition;
92
+ onChange(next?.unary ? { ...rest, op: v } : { ...condition, op: v });
93
+ }, options: [
94
+ ...(op && ops.includes(op)
95
+ ? []
96
+ : [{ value: NONE, label: condition.op || m.choose, disabled: true }]),
97
+ ...ops.map((o) => ({ value: o.id, label: o.label })),
98
+ ] }) }), op?.unary ? null : fact?.type === "number" ? (_jsx(Field, { label: m.value, children: _jsx(NumberInput, { value: typeof condition.value === "number" ? condition.value : undefined, disabled: disabled, onValue: (v) => onChange({ ...condition, value: v }) }) })) : fact?.type === "boolean" ? (_jsx(Field, { label: m.value, children: _jsx(Select, { value: String(condition.value === true), disabled: disabled, onValueChange: (v) => onChange({ ...condition, value: v === "true" }), options: [
99
+ { value: "true", label: m.boolean(true) },
100
+ { value: "false", label: m.boolean(false) },
101
+ ] }) })) : fact?.type === "enum" && fact.values ? (_jsx(Field, { label: m.value, children: _jsx(Select, { value: typeof condition.value === "string" && condition.value ? condition.value : NONE, disabled: disabled, onValueChange: (v) => onChange({ ...condition, value: v }), options: [
102
+ ...(fact.values.some((v) => v.id === condition.value)
103
+ ? []
104
+ : [{ value: NONE, label: m.choose, disabled: true }]),
105
+ ...fact.values.map((v) => ({ value: v.id, label: v.label })),
106
+ ] }) })) : (_jsx(Field, { label: m.value, children: _jsx(Input, { value: typeof condition.value === "string" ? condition.value : String(condition.value ?? ""), disabled: disabled, onChange: (e) => onChange({ ...condition, value: e.target.value }) }) }))] }));
107
+ }
108
+ /** Who approves a gate: roles, named members, a quorum, and a timeout with what happens then. */
109
+ export function GatePolicyEditor({ policy, onChange, registry, approvers = true, timeout = true, disabled = false, }) {
110
+ const { messages } = useUI();
111
+ const m = messages.editor;
112
+ const set = (patch) => {
113
+ const next = { ...policy, ...patch };
114
+ for (const k of Object.keys(next))
115
+ if (next[k] === undefined)
116
+ delete next[k];
117
+ onChange(next);
118
+ };
119
+ return (_jsxs("fieldset", { className: "flex flex-col gap-3 rounded-lg border border-border p-3", children: [_jsx("legend", { className: "px-1 text-sm font-medium", children: m.approvers }), approvers ? (_jsxs(_Fragment, { children: [registry.roles?.length ? (_jsx(OptionChecks, { legend: m.roles, options: registry.roles, value: policy.roles ?? [], onValue: (roles) => set({ roles }), disabled: disabled })) : null, registry.members?.length ? (_jsx(OptionChecks, { legend: m.members, options: [
120
+ ...registry.members,
121
+ ...(policy.members ?? [])
122
+ .filter((id) => !registry.members?.some((o) => o.id === id))
123
+ .map((id) => ({ id, label: id })),
124
+ ], value: policy.members ?? [], onValue: (members) => set({ members }), disabled: disabled })) : null, _jsx(Field, { label: m.quorum, hint: m.quorumHint, children: _jsx(NumberInput, { value: policy.quorum, min: 1, step: 1, disabled: disabled, onValue: (quorum) => set({ quorum }) }) })] })) : null, timeout ? (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.timeoutHours, children: _jsx(NumberInput, { value: policy.timeoutHours, min: 0, step: 1, disabled: disabled, onValue: (timeoutHours) => set({ timeoutHours }) }) }), policy.timeoutHours ? (_jsx(Field, { label: m.onTimeout, children: _jsx(Select, { value: policy.onTimeout ?? NONE, disabled: disabled, onValueChange: (v) => set({ onTimeout: v }), options: [
125
+ ...(policy.onTimeout ? [] : [{ value: NONE, label: m.choose, disabled: true }]),
126
+ { value: "fail", label: m.onTimeoutOption("fail") },
127
+ { value: "escalate", label: m.onTimeoutOption("escalate") },
128
+ ] }) })) : null] })) : null] }));
129
+ }
130
+ /** A list of rows with add and remove, for cases, labels, variants and ports. */
131
+ function RowList({ legend, rows, render, name, onAdd, onRemove, addLabel, disabled, }) {
132
+ const { messages } = useUI();
133
+ return (_jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "mb-1 text-sm font-medium", children: legend }), rows.map((row, i) => (_jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-border p-2", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: render(row, i) }), disabled ? null : (_jsx(Button, { type: "button", size: "icon-xs", variant: "ghost", "aria-label": messages.editor.remove(name(row)), title: messages.editor.remove(name(row)), onClick: () => onRemove(i), children: _jsx(XIcon, { "aria-hidden": true }) }))] }, i))), disabled ? null : (_jsxs(Button, { type: "button", size: "sm", variant: "outline", className: "self-start", onClick: onAdd, children: [_jsx(PlusIcon, { "aria-hidden": true }), addLabel] }))] }));
134
+ }
135
+ const replace = (rows, i, row) => rows.map((r, j) => (j === i ? row : r));
136
+ function PortsEditor({ legend, ports, onChange, registry, disabled, }) {
137
+ const { messages } = useUI();
138
+ const m = messages.editor;
139
+ const types = [{ id: "any", label: m.anyType }, ...dataTypes(registry)];
140
+ return (_jsx(RowList, { legend: legend, rows: ports, name: (p) => p.label, addLabel: m.addPort, disabled: disabled, onAdd: () => {
141
+ const id = freshId(m.newPort, ports.map((p) => p.id));
142
+ onChange([...ports, { id, label: m.newPort, type: "any" }]);
143
+ }, onRemove: (i) => onChange(ports.filter((_, j) => j !== i)), render: (p, i) => (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.portLabel, children: _jsx(Input, { value: p.label, disabled: disabled, onChange: (e) => onChange(replace(ports, i, { ...p, label: e.target.value })) }) }), _jsx(Field, { label: m.portType, children: _jsx(Select, { value: p.type, disabled: disabled, onValueChange: (type) => onChange(replace(ports, i, { ...p, type })), options: [
144
+ ...types.map((t) => ({ value: t.id, label: t.label })),
145
+ ...(types.some((t) => t.id === p.type) ? [] : [{ value: p.type, label: p.type }]),
146
+ ] }) })] })) }));
147
+ }
148
+ /**
149
+ * The selected node's or connection's fields, edited in place: per-kind
150
+ * fields, a condition editor over the host's facts and operators, a gate
151
+ * policy editor (roles, members, quorum, timeout), and the host's model,
152
+ * harness and effort pickers. With nothing selected, the workflow's name and
153
+ * description. Every edit goes out as the whole next definition.
154
+ */
155
+ export function WorkflowInspector({ definition, registry, selection = null, onChange, onSelectionChange, readOnly = false, className, }) {
156
+ const { messages } = useUI();
157
+ const m = messages.editor;
158
+ const disabled = readOnly || !onChange;
159
+ const flowName = selection?.flow && definition.flows[selection.flow] ? selection.flow : definition.main;
160
+ const flow = definition.flows[flowName] ?? { nodes: [], edges: [] };
161
+ const node = selection?.type === "node" ? flow.nodes.find((n) => n.id === selection.id) : undefined;
162
+ const edge = selection?.type === "edge" ? flow.edges.find((e) => edgeId(e) === selection.id) : undefined;
163
+ const setFlow = (next) => onChange?.(withFlow(definition, flowName, next));
164
+ let body;
165
+ let title;
166
+ if (node) {
167
+ title = m.inspectorFor(m.kind(node.kind), node.label);
168
+ body = (_jsx(NodeFields, { node: node, flow: flow, definition: definition, registry: registry, disabled: disabled, onPatch: (patch) => setFlow(updateNode(flow, node.id, patch)), onDelete: () => {
169
+ setFlow(removeNode(flow, node.id));
170
+ onSelectionChange?.(null);
171
+ } }));
172
+ }
173
+ else if (edge) {
174
+ const from = flow.nodes.find((n) => n.id === edge.from);
175
+ const to = flow.nodes.find((n) => n.id === edge.to);
176
+ title = m.edgeName(edge.kind, from?.label ?? edge.from, to?.label ?? edge.to);
177
+ body = (_jsx(EdgeFields, { edge: edge, flow: flow, definition: definition, registry: registry, disabled: disabled,
178
+ // A connection's id is its ends and ports, none of which the inspector changes.
179
+ onPatch: (patch) => setFlow(updateEdge(flow, edgeId(edge), patch)), onDelete: () => {
180
+ setFlow(removeEdge(flow, edgeId(edge)));
181
+ onSelectionChange?.(null);
182
+ } }));
183
+ }
184
+ else {
185
+ title = m.workflowDetails;
186
+ body = (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(Field, { label: m.name, children: _jsx(Input, { value: definition.name, disabled: disabled, onChange: (e) => onChange?.({ ...definition, name: e.target.value }) }) }), _jsx(Field, { label: m.description, children: _jsx(Textarea, { value: definition.description ?? "", disabled: disabled, onChange: (e) => onChange?.({ ...definition, description: e.target.value || undefined }) }) }), _jsx(PortsEditor, { legend: m.runInputs, ports: flow.inputs ?? [], registry: registry, disabled: disabled, onChange: (inputs) => setFlow({ ...flow, inputs }) }), _jsx("p", { className: "text-sm text-muted-foreground", children: m.inspectorEmpty })] }));
187
+ }
188
+ return (_jsxs("section", { "aria-label": m.inspector, "data-slot": "workflow-inspector", className: cn("flex min-h-0 flex-col gap-3", className), children: [_jsx("h2", { className: "truncate text-sm font-medium", children: title }), _jsx("div", { className: "flex min-h-0 flex-col gap-4 overflow-y-auto pb-2", children: body })] }));
189
+ }
190
+ function NodeFields({ node, flow, definition, registry, disabled, onPatch, onDelete, }) {
191
+ const { messages } = useUI();
192
+ const m = messages.editor;
193
+ const presetOptions = registry.modules.flatMap((mod) => mod.presets.map((p) => ({ value: `${mod.id}/${p.id}`, label: `${mod.label} · ${p.label}` })));
194
+ const presetValue = (ref) => ref ? `${ref.module}/${ref.id}` : NONE;
195
+ const toRef = (v) => {
196
+ if (v === NONE)
197
+ return undefined;
198
+ const [module, id] = v.split("/");
199
+ return { module, id };
200
+ };
201
+ const others = flow.nodes.filter((n) => n.id !== node.id);
202
+ const common = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.label, children: _jsx(Input, { value: node.label, disabled: disabled, onChange: (e) => onPatch({ label: e.target.value }) }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: m.nodeId(node.id) })] }));
203
+ const agent = (target, patch) => (_jsxs(_Fragment, { children: [_jsx(HostPicker, { label: m.model, options: registry.models, value: target.model, onValue: (model) => patch({ model }), disabled: disabled }), _jsx(HostPicker, { label: m.harness, options: registry.harnesses, value: target.harness, onValue: (harness) => patch({ harness }), disabled: disabled }), _jsx(HostPicker, { label: m.effort, options: registry.efforts, value: target.effort, onValue: (effort) => patch({ effort }), disabled: disabled })] }));
204
+ let fields = null;
205
+ switch (node.kind) {
206
+ case "step": {
207
+ const found = findPreset(registry, node.preset);
208
+ const unknown = node.preset && !found;
209
+ fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.preset, children: _jsx(Select, { value: presetValue(node.preset), disabled: disabled, onValueChange: (v) => {
210
+ const preset = toRef(v);
211
+ onPatch(preset
212
+ ? { preset, inputs: undefined, outputs: undefined }
213
+ : { preset: undefined, inputs: [], outputs: [] });
214
+ }, options: [
215
+ { value: NONE, label: m.customStep },
216
+ ...presetOptions,
217
+ ...(unknown && node.preset
218
+ ? [{ value: presetValue(node.preset), label: presetValue(node.preset) }]
219
+ : []),
220
+ ] }) }), found ? (_jsx(PortSummary, { node: node, definition: definition, registry: registry })) : (_jsxs(_Fragment, { children: [_jsx(PortsEditor, { legend: m.inputs, ports: node.inputs ?? [], registry: registry, disabled: disabled, onChange: (inputs) => onPatch({ inputs }) }), _jsx(PortsEditor, { legend: m.outputs, ports: node.outputs ?? [], registry: registry, disabled: disabled, onChange: (outputs) => onPatch({ outputs }) })] })), !found || found.preset.agent ? agent(node, (p) => onPatch(p)) : null] }));
221
+ break;
222
+ }
223
+ case "subflow":
224
+ fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.subflow, children: _jsx(Select, { value: node.flow || NONE, disabled: disabled, onValueChange: (flow) => onPatch({ flow }), options: [
225
+ ...(node.flow ? [] : [{ value: NONE, label: m.choose, disabled: true }]),
226
+ ...Object.entries(definition.flows)
227
+ .filter(([name]) => name !== definition.main)
228
+ .map(([name, f]) => ({ value: name, label: f.label ?? name })),
229
+ ] }) }), _jsx(Field, { label: m.each, hint: m.eachHint, children: _jsx(Input, { value: node.each ?? "", disabled: disabled, onChange: (e) => onPatch({ each: e.target.value || undefined }) }) })] }));
230
+ break;
231
+ case "gate": {
232
+ const kind = findGateKind(registry, node.gate);
233
+ const fieldsOf = kind?.fields ?? [];
234
+ fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.gateKind, hint: kind?.hint, children: _jsx(Select, { value: kind ? node.gate : NONE, disabled: disabled, onValueChange: (gate) => onPatch({ gate }), options: [
235
+ ...(kind ? [] : [{ value: NONE, label: node.gate || m.choose, disabled: true }]),
236
+ ...registry.gateKinds.map((g) => ({ value: g.id, label: g.label })),
237
+ ] }) }), fieldsOf.includes("approvers") || fieldsOf.includes("timeout") ? (_jsx(GatePolicyEditor, { policy: node.policy ?? {}, registry: registry, approvers: fieldsOf.includes("approvers"), timeout: fieldsOf.includes("timeout"), disabled: disabled, onChange: (policy) => onPatch({ policy }) })) : null, fieldsOf.includes("threshold") ? (_jsx(Field, { label: m.threshold, children: _jsx(PercentInput, { value: node.threshold, disabled: disabled, onValue: (threshold) => onPatch({ threshold }) }) })) : null, fieldsOf.includes("rule") ? (_jsx(Field, { label: m.rule, children: _jsx(Textarea, { value: node.rule ?? "", disabled: disabled, onChange: (e) => onPatch({ rule: e.target.value }) }) })) : null] }));
238
+ break;
239
+ }
240
+ case "condition": {
241
+ const n = node;
242
+ fields = (_jsx(RowList, { legend: m.cases, rows: n.cases, name: (c) => c.label, addLabel: m.addCase, disabled: disabled, onAdd: () => {
243
+ const id = freshId("case", n.cases.map((c) => c.id));
244
+ const fact = registry.facts[0];
245
+ const op = operatorsFor(registry, fact)[0];
246
+ onPatch({
247
+ cases: [
248
+ ...n.cases,
249
+ {
250
+ id,
251
+ label: m.newCase(n.cases.length + 1),
252
+ when: {
253
+ fact: fact?.id ?? "",
254
+ op: op?.id ?? "",
255
+ ...(op?.unary ? {} : { value: "" }),
256
+ },
257
+ },
258
+ ],
259
+ });
260
+ }, onRemove: (i) => onPatch({ cases: n.cases.filter((_, j) => j !== i) }), render: (c, i) => (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.caseLabel, children: _jsx(Input, { value: c.label, disabled: disabled, onChange: (e) => onPatch({ cases: replace(n.cases, i, { ...c, label: e.target.value }) }) }) }), _jsx(ConditionEditor, { label: m.when(c.label), condition: c.when, flow: flow, registry: registry, disabled: disabled, onChange: (when) => onPatch({ cases: replace(n.cases, i, { ...c, when }) }) })] })) }));
261
+ break;
262
+ }
263
+ case "goto":
264
+ fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.target, children: _jsx(Select, { value: node.target || NONE, disabled: disabled, onValueChange: (target) => onPatch({ target }), options: [
265
+ ...(others.some((o) => o.id === node.target)
266
+ ? []
267
+ : [
268
+ {
269
+ value: node.target || NONE,
270
+ label: node.target || m.choose,
271
+ disabled: true,
272
+ },
273
+ ]),
274
+ ...others.map((o) => ({ value: o.id, label: o.label })),
275
+ ] }) }), _jsx(Field, { label: m.limit, children: _jsx(NumberInput, { value: node.max, min: 1, step: 1, disabled: disabled, onValue: (max) => onPatch({ max }) }) })] }));
276
+ break;
277
+ case "fanout":
278
+ fields = _jsx("p", { className: "text-sm text-muted-foreground", children: m.kindHint("fanout") });
279
+ break;
280
+ case "join":
281
+ fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.wait, children: _jsx(Select, { value: node.wait, disabled: disabled, onValueChange: (wait) => onPatch({ wait: wait }), options: ["all", "any", "n"].map((w) => ({
282
+ value: w,
283
+ label: m.waitOption(w),
284
+ })) }) }), node.wait === "n" ? (_jsx(Field, { label: m.waitN, children: _jsx(NumberInput, { value: node.n, min: 1, step: 1, disabled: disabled, onValue: (n) => onPatch({ n }) }) })) : null] }));
285
+ break;
286
+ case "variants": {
287
+ const n = node;
288
+ const evaluators = others.filter((o) => o.kind === "evaluator");
289
+ fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.preset, children: _jsx(Select, { value: presetValue(n.preset), disabled: disabled, onValueChange: (v) => onPatch({ preset: toRef(v) }), options: [{ value: NONE, label: m.noPreset }, ...presetOptions] }) }), _jsx(Field, { label: m.winner, children: _jsx(Select, { value: n.select ?? NONE, disabled: disabled, onValueChange: (v) => onPatch({ select: v }), options: [
290
+ ...(n.select ? [] : [{ value: NONE, label: m.choose, disabled: true }]),
291
+ ...WORKFLOW_SELECTORS.map((s) => ({ value: s, label: m.selector(s) })),
292
+ ] }) }), n.select === "evaluator" ? (_jsx(Field, { label: m.evaluator, children: _jsx(Select, { value: n.evaluator ?? NONE, disabled: disabled, onValueChange: (evaluator) => onPatch({ evaluator }), options: [
293
+ ...(n.evaluator ? [] : [{ value: NONE, label: m.choose, disabled: true }]),
294
+ ...evaluators.map((o) => ({ value: o.id, label: o.label })),
295
+ ] }) })) : null, _jsx(RowList, { legend: m.variants, rows: n.variants, name: (v) => v.label, addLabel: m.addVariant, disabled: disabled, onAdd: () => {
296
+ const id = freshId("variant", n.variants.map((v) => v.id));
297
+ onPatch({
298
+ variants: [...n.variants, { id, label: m.newVariant(n.variants.length + 1) }],
299
+ });
300
+ }, onRemove: (i) => onPatch({ variants: n.variants.filter((_, j) => j !== i) }), render: (v, i) => (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.variantLabel, children: _jsx(Input, { value: v.label, disabled: disabled, onChange: (e) => onPatch({ variants: replace(n.variants, i, { ...v, label: e.target.value }) }) }) }), agent(v, (p) => onPatch({ variants: replace(n.variants, i, { ...v, ...p }) }))] })) })] }));
301
+ break;
302
+ }
303
+ case "evaluator":
304
+ fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.question, children: _jsx(Textarea, { value: node.question ?? "", disabled: disabled, onChange: (e) => onPatch({ question: e.target.value || undefined }) }) }), _jsx(Field, { label: m.levels, hint: m.levelsHint, children: _jsx(Textarea, { value: node.levels.join("\n"), disabled: disabled, onChange: (e) => onPatch({
305
+ levels: e.target.value
306
+ .split("\n")
307
+ .map((l) => l.trim())
308
+ .filter(Boolean),
309
+ }) }) }), _jsx(Field, { label: m.routes, orientation: "horizontal", children: _jsx(Switch, { checked: node.routes, disabled: disabled, onCheckedChange: (routes) => onPatch({ routes }) }) }), _jsx(Field, { label: m.threshold, children: _jsx(PercentInput, { value: node.threshold, disabled: disabled, onValue: (threshold) => onPatch({ threshold }) }) }), _jsx(Field, { label: m.minConfidence, hint: m.minConfidenceHint, children: _jsx(PercentInput, { value: node.minConfidence, disabled: disabled, onValue: (minConfidence) => onPatch({ minConfidence }) }) })] }));
310
+ break;
311
+ case "classifier": {
312
+ const n = node;
313
+ fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.question, children: _jsx(Textarea, { value: n.question ?? "", disabled: disabled, onChange: (e) => onPatch({ question: e.target.value || undefined }) }) }), _jsx(RowList, { legend: m.options, rows: n.options, name: (o) => o.label, addLabel: m.addOption, disabled: disabled, onAdd: () => {
314
+ const id = freshId("label", n.options.map((o) => o.id));
315
+ onPatch({
316
+ options: [...n.options, { id, label: m.newOption(n.options.length + 1) }],
317
+ });
318
+ }, onRemove: (i) => onPatch({ options: n.options.filter((_, j) => j !== i) }), render: (o, i) => (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.optionLabel, children: _jsx(Input, { value: o.label, disabled: disabled, onChange: (e) => onPatch({ options: replace(n.options, i, { ...o, label: e.target.value }) }) }) }), _jsx(Field, { label: m.optionDescription, children: _jsx(Input, { value: o.description ?? "", disabled: disabled, onChange: (e) => onPatch({
319
+ options: replace(n.options, i, {
320
+ ...o,
321
+ description: e.target.value || undefined,
322
+ }),
323
+ }) }) })] })) }), _jsx(Field, { label: m.otherExit, orientation: "horizontal", children: _jsx(Switch, { checked: n.other, disabled: disabled, onCheckedChange: (other) => onPatch({ other }) }) }), _jsx(Field, { label: m.minConfidence, hint: m.minConfidenceHint, children: _jsx(PercentInput, { value: n.minConfidence, disabled: disabled, onValue: (minConfidence) => onPatch({ minConfidence }) }) })] }));
324
+ break;
325
+ }
326
+ case "check":
327
+ fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.question, children: _jsx(Textarea, { value: node.question, disabled: disabled, onChange: (e) => onPatch({ question: e.target.value }) }) }), _jsx(Field, { label: m.yesAt, children: _jsx(PercentInput, { value: node.threshold, disabled: disabled, onValue: (threshold) => onPatch({ threshold: threshold ?? 0.5 }) }) })] }));
328
+ break;
329
+ }
330
+ return (_jsxs("div", { className: "flex flex-col gap-4", children: [common, fields, _jsx(Field, { label: m.note, children: _jsx(Textarea, { value: node.note ?? "", disabled: disabled, onChange: (e) => onPatch({ note: e.target.value || undefined }) }) }), disabled ? null : (_jsxs(Button, { type: "button", variant: "destructive", size: "sm", className: "self-start", onClick: onDelete, children: [_jsx(Trash2Icon, { "aria-hidden": true }), m.deleteNode(node.label)] }))] }));
331
+ }
332
+ /** A preset's contract, read from the registry. */
333
+ function PortSummary({ node, definition, registry, }) {
334
+ const { messages } = useUI();
335
+ const m = messages.editor;
336
+ const list = (title, ports) => (_jsxs("div", { children: [_jsx("h3", { className: "text-xs font-medium text-muted-foreground", children: title }), ports.length ? (_jsx("ul", { className: "text-sm", children: ports.map((p) => (_jsxs("li", { children: [m.port(p.label, p.type), p.optional ? _jsxs("span", { className: "text-muted-foreground", children: [", ", m.optional] }) : null] }, p.id))) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.run.none }))] }));
337
+ return (_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [list(m.inputs, inputsOf(node, registry, definition)), list(m.outputs, outputsOf(node, registry, definition))] }));
338
+ }
339
+ function EdgeFields({ edge, flow, definition, registry, disabled, onPatch, onDelete, }) {
340
+ const { messages } = useUI();
341
+ const m = messages.editor;
342
+ const from = flow.nodes.find((n) => n.id === edge.from);
343
+ const to = flow.nodes.find((n) => n.id === edge.to);
344
+ return (_jsxs("div", { className: "flex flex-col gap-4", children: [edge.kind === "next" && from ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm", children: m.exitIs(m.exit(exitLabel(from, portOf(edge)))) }), _jsx(Field, { label: m.edgeLabel, children: _jsx(Input, { value: edge.label ?? "", disabled: disabled, onChange: (e) => onPatch({ label: e.target.value || undefined }) }) })] })) : null, edge.kind === "data" && from && to ? (_jsx("p", { className: "text-sm", children: m.dataFlow(outputsOf(from, registry, definition).find((p) => p.id === edge.port)?.label ??
345
+ edge.port ??
346
+ "", inputsOf(to, registry, definition).find((p) => p.id === edge.input)?.label ??
347
+ edge.input ??
348
+ "") })) : null, edge.kind === "loop" ? (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.limit, children: _jsx(NumberInput, { value: edge.max, min: 1, step: 1, disabled: disabled, onValue: (max) => onPatch({ max }) }) }), _jsx(ConditionEditor, { label: m.until, condition: edge.until ?? { fact: "", op: "" }, flow: flow, registry: registry, disabled: disabled, onChange: (until) => onPatch({ until }) })] })) : null, disabled ? null : (_jsxs(Button, { type: "button", variant: "destructive", size: "sm", className: "self-start", onClick: onDelete, children: [_jsx(Trash2Icon, { "aria-hidden": true }), m.deleteEdge] }))] }));
349
+ }
@@ -0,0 +1,16 @@
1
+ import * as React from "react";
2
+ import type { WorkflowDefinition } from "../definition.js";
3
+ export interface WorkflowJsonViewProps {
4
+ definition: WorkflowDefinition;
5
+ /** A valid edit, read through the format. Omitted: read-only. */
6
+ onChange?: (definition: WorkflowDefinition) => void;
7
+ readOnly?: boolean;
8
+ className?: string;
9
+ }
10
+ /**
11
+ * The definition as JSON, kept in step with the canvas and the inspector:
12
+ * an edit elsewhere rewrites the text, and text that reads as a definition
13
+ * goes back as an edit. Text that does not read stays as typed, with the
14
+ * reason, until it does; nothing outside the format is ever kept.
15
+ */
16
+ export declare function WorkflowJsonView({ definition, onChange, readOnly, className, }: WorkflowJsonViewProps): React.JSX.Element;
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { Field } from "../../primitives/Field.js";
6
+ import { Textarea } from "../../primitives/Textarea.js";
7
+ import { useUI } from "../../provider/UIProvider.js";
8
+ import { readWorkflowDefinition } from "../portable.js";
9
+ const show = (d) => JSON.stringify(d, null, 2);
10
+ /**
11
+ * The definition as JSON, kept in step with the canvas and the inspector:
12
+ * an edit elsewhere rewrites the text, and text that reads as a definition
13
+ * goes back as an edit. Text that does not read stays as typed, with the
14
+ * reason, until it does; nothing outside the format is ever kept.
15
+ */
16
+ export function WorkflowJsonView({ definition, onChange, readOnly = false, className, }) {
17
+ const { messages } = useUI();
18
+ const m = messages.editor;
19
+ const editable = !readOnly && !!onChange;
20
+ // The text, and the definition it was last in step with.
21
+ const [state, setState] = React.useState(() => ({ text: show(definition), from: definition }));
22
+ const [error, setError] = React.useState(null);
23
+ if (state.from !== definition) {
24
+ setState({ text: show(definition), from: definition });
25
+ setError(null);
26
+ }
27
+ return (_jsx("div", { "data-slot": "workflow-json", className: cn("flex min-h-0 flex-col gap-2", className), children: _jsx(Field, { label: m.json, hint: editable ? m.jsonHint : undefined, error: error ? m.readError(error) : undefined, children: _jsx(Textarea, { value: state.text, readOnly: !editable, spellCheck: false, rows: 18, className: "min-h-64 font-mono text-xs", onChange: (e) => {
28
+ const text = e.target.value;
29
+ const read = readWorkflowDefinition(text);
30
+ if (!read.ok) {
31
+ setState((s) => ({ ...s, text }));
32
+ setError(read.error);
33
+ return;
34
+ }
35
+ setError(null);
36
+ // In step with what goes out, so the host handing it back keeps the person's text.
37
+ setState({ text, from: read.value });
38
+ onChange?.(read.value);
39
+ } }) }) }));
40
+ }
@@ -0,0 +1,16 @@
1
+ import * as React from "react";
2
+ import type { WorkflowRegistry } from "../definition.js";
3
+ import { type WorkflowPaletteItem } from "../palette.js";
4
+ export interface WorkflowPaletteProps {
5
+ registry: WorkflowRegistry;
6
+ /** Adds the item; the editor puts it after the selection. Omitted: read-only, nothing to add. */
7
+ onAdd?: (item: WorkflowPaletteItem) => void;
8
+ className?: string;
9
+ }
10
+ /**
11
+ * What a flow is built from, grouped by module: each installed module's step
12
+ * presets, then gates by kind, logic, judges, a custom step and a sub-flow.
13
+ * A click adds after the selection. An item this destination does not run is
14
+ * marked, never hidden: a definition can be written for another factory.
15
+ */
16
+ export declare function WorkflowPalette({ registry, onAdd, className }: WorkflowPaletteProps): React.JSX.Element;
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { PlusIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../../lib/cn.js";
6
+ import { Input } from "../../primitives/Input.js";
7
+ import { useUI } from "../../provider/UIProvider.js";
8
+ import { paletteOf } from "../palette.js";
9
+ /**
10
+ * What a flow is built from, grouped by module: each installed module's step
11
+ * presets, then gates by kind, logic, judges, a custom step and a sub-flow.
12
+ * A click adds after the selection. An item this destination does not run is
13
+ * marked, never hidden: a definition can be written for another factory.
14
+ */
15
+ export function WorkflowPalette({ registry, onAdd, className }) {
16
+ const { messages } = useUI();
17
+ const m = messages.editor;
18
+ const [query, setQuery] = React.useState("");
19
+ const terms = query.toLocaleLowerCase().split(/\s+/).filter(Boolean);
20
+ const name = (item) => item.label || (item.template.kind === "step" ? m.customStep : m.kind(item.template.kind));
21
+ const hint = (item) => item.hint ?? (item.label ? undefined : m.kindHint(item.template.kind));
22
+ const groups = paletteOf(registry)
23
+ .map((g) => ({
24
+ ...g,
25
+ title: g.module ? g.label : m.paletteGroup(g.key),
26
+ }))
27
+ .map((g) => ({
28
+ ...g,
29
+ items: g.items.filter((item) => {
30
+ const text = `${g.title} ${name(item)} ${hint(item) ?? ""}`.toLocaleLowerCase();
31
+ return terms.every((t) => text.includes(t));
32
+ }),
33
+ }))
34
+ .filter((g) => g.items.length);
35
+ return (_jsxs("nav", { "aria-label": m.palette, "data-slot": "workflow-palette", className: cn("flex min-h-0 flex-col gap-3", className), children: [_jsx(Input, { type: "search", "aria-label": m.paletteSearch, placeholder: m.paletteSearch, value: query, onChange: (e) => setQuery(e.target.value) }), _jsx("div", { className: "flex min-h-0 flex-col gap-4 overflow-y-auto", children: groups.length ? (groups.map((g) => (_jsxs("section", { "aria-label": g.title, className: "flex flex-col gap-1", children: [_jsx("h3", { className: "text-2xs font-medium tracking-wide text-muted-foreground uppercase", children: g.title }), _jsx("ul", { className: "flex flex-col gap-1", children: g.items.map((item) => (_jsx("li", { children: _jsxs("button", { type: "button", disabled: !onAdd, onClick: () => onAdd?.(item), className: "group flex w-full items-start gap-2 rounded-lg border border-border bg-card px-2.5 py-1.5 text-left text-sm hover:bg-muted disabled:cursor-default disabled:hover:bg-card", children: [_jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate", children: name(item) }), hint(item) ? (_jsx("span", { className: "line-clamp-2 text-xs text-muted-foreground", children: hint(item) })) : null, item.available ? null : (_jsx("span", { className: "text-xs text-warning-fg", children: m.notRun }))] }), onAdd ? (_jsx(PlusIcon, { "aria-hidden": true, className: "mt-0.5 size-4 shrink-0 text-muted-foreground group-hover:text-foreground" })) : null] }) }, item.key))) })] }, g.key)))) : (_jsx("p", { className: "text-sm text-muted-foreground", children: m.paletteEmpty })) })] }));
36
+ }
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+ import type { WorkflowDefinition, WorkflowSelection } from "../definition.js";
3
+ import type { WorkflowProblem } from "../problems.js";
4
+ export interface WorkflowProblemsProps {
5
+ problems: readonly WorkflowProblem[];
6
+ /** Names each problem's flow when the definition has more than one. */
7
+ definition?: WorkflowDefinition;
8
+ /** Selects what a problem points at. Omitted: the rows do not select. */
9
+ onSelect?: (selection: WorkflowSelection) => void;
10
+ className?: string;
11
+ }
12
+ /** Where a problem points, as a selection; null for one about a whole flow or the definition. */
13
+ export declare function problemTarget(problem: WorkflowProblem, main: string): WorkflowSelection | null;
14
+ /**
15
+ * Every problem in the definition at its three levels: blocking (stops a
16
+ * publish), warning, and not run here. Each row names its problem and selects
17
+ * what it points at. The not-run notes fold away, since a definition written
18
+ * for another factory has many.
19
+ */
20
+ export declare function WorkflowProblems({ problems, definition, onSelect, className, }: WorkflowProblemsProps): React.JSX.Element;
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { useUI } from "../../provider/UIProvider.js";
6
+ import { StatusBadge } from "../../status/StatusBadge.js";
7
+ const LEVELS = ["blocking", "warning", "not-run"];
8
+ const TONE = {
9
+ blocking: "error",
10
+ warning: "warn",
11
+ "not-run": "muted",
12
+ };
13
+ /** Where a problem points, as a selection; null for one about a whole flow or the definition. */
14
+ export function problemTarget(problem, main) {
15
+ const flow = problem.flow ?? main;
16
+ if (problem.node)
17
+ return { type: "node", flow, id: problem.node };
18
+ if (problem.edge)
19
+ return { type: "edge", flow, id: problem.edge };
20
+ return null;
21
+ }
22
+ /**
23
+ * Every problem in the definition at its three levels: blocking (stops a
24
+ * publish), warning, and not run here. Each row names its problem and selects
25
+ * what it points at. The not-run notes fold away, since a definition written
26
+ * for another factory has many.
27
+ */
28
+ export function WorkflowProblems({ problems, definition, onSelect, className, }) {
29
+ const { messages } = useUI();
30
+ const m = messages.editor;
31
+ const [showNotRun, setShowNotRun] = React.useState(false);
32
+ const multiFlow = definition ? Object.keys(definition.flows).length > 1 : false;
33
+ const flowName = (name) => (name && definition?.flows[name]?.label) || name;
34
+ const counts = Object.fromEntries(LEVELS.map((l) => [l, problems.filter((p) => p.level === l).length]));
35
+ const listId = React.useId();
36
+ return (_jsxs("section", { "aria-label": m.problems, "data-slot": "workflow-problems", className: cn("flex min-h-0 flex-col gap-2", className), children: [_jsx("p", { role: "status", className: "flex flex-wrap items-center gap-2 text-sm", children: counts.blocking || counts.warning ? (_jsxs(_Fragment, { children: [_jsx(StatusBadge, { tone: counts.blocking ? "error" : "ok", children: m.problemCount("blocking", counts.blocking) }), " ", _jsx(StatusBadge, { tone: counts.warning ? "warn" : "muted", children: m.problemCount("warning", counts.warning) })] })) : (_jsx(StatusBadge, { tone: "ok", children: m.noProblems })) }), counts["not-run"] ? (_jsx("button", { type: "button", className: "self-start text-sm text-muted-foreground underline-offset-2 hover:underline", "aria-expanded": showNotRun, "aria-controls": listId, onClick: () => setShowNotRun((v) => !v), children: m.problemCount("not-run", counts["not-run"]) })) : null, _jsx("ul", { id: listId, className: "flex min-h-0 flex-col gap-1 overflow-y-auto", children: LEVELS.flatMap((level) => level === "not-run" && !showNotRun
37
+ ? []
38
+ : problems
39
+ .filter((p) => p.level === level)
40
+ .map((p, i) => {
41
+ const target = definition ? problemTarget(p, definition.main) : null;
42
+ const text = m.problem(p);
43
+ const where = multiFlow && p.flow ? flowName(p.flow) : undefined;
44
+ const reason = p.availability?.status === "missing-capabilities"
45
+ ? messages.workflow.missingCapabilities(p.availability.missing ?? [])
46
+ : (p.availability?.reason ??
47
+ (p.availability ? messages.workflow.unsupported : undefined));
48
+ const body = (_jsxs(_Fragment, { children: [_jsx(StatusBadge, { tone: TONE[level], className: "shrink-0", children: m.problemLevel(level) }), " ", _jsxs("span", { className: "min-w-0 flex-1", children: [where ? _jsxs("span", { className: "text-muted-foreground", children: [where, ": "] }) : null, text, reason ? (_jsx("span", { className: "block text-xs text-muted-foreground", children: reason })) : null] })] }));
49
+ return (_jsx("li", { children: target && onSelect ? (_jsx("button", { type: "button", onClick: () => onSelect(target), className: "flex w-full items-start gap-2 rounded-lg border border-border px-2.5 py-1.5 text-left text-sm hover:bg-muted", children: body })) : (_jsx("div", { className: "flex items-start gap-2 rounded-lg border border-border px-2.5 py-1.5 text-sm", children: body })) }, `${level}-${i}`));
50
+ })) })] }));
51
+ }
@@ -0,0 +1,49 @@
1
+ import * as React from "react";
2
+ import type { WorkflowDefinition, WorkflowRegistry } from "../definition.js";
3
+ import { type WorkflowImportSummary as ImportPreview } from "../portable.js";
4
+ import { type WorkflowRevision } from "../workspace.js";
5
+ /** A host action: may return a promise; a rejection's message is shown. */
6
+ type HostAction<A extends unknown[]> = (...args: A) => Promise<unknown> | unknown;
7
+ export interface WorkflowWorkspaceProps {
8
+ /** The working copy, held by the host. Null while it loads. */
9
+ definition: WorkflowDefinition | null;
10
+ /** Every edit. Omitted, or `readOnly`: a viewer. */
11
+ onChange?: (definition: WorkflowDefinition) => void;
12
+ registry: WorkflowRegistry;
13
+ /** The latest saved draft revision on the host. */
14
+ saved?: WorkflowRevision | null;
15
+ /** The published revision: what runs. */
16
+ published?: WorkflowRevision | null;
17
+ /** Saves the working copy as a draft over `base`, the revision it started from. */
18
+ onSave?: HostAction<[definition: WorkflowDefinition, base: WorkflowRevision | null]>;
19
+ /** Publishes the working copy: it becomes what runs. */
20
+ onPublish?: HostAction<[definition: WorkflowDefinition]>;
21
+ /** Saves the working copy as a new, named reusable definition. */
22
+ onSaveAs?: HostAction<[definition: WorkflowDefinition, name: string]>;
23
+ onExport?: (definition: WorkflowDefinition) => void;
24
+ /** Starts an import: the host picks a file and hands back `importPreview`. */
25
+ onImport?: () => void;
26
+ importPreview?: ImportPreview | null;
27
+ onImportConfirm?: HostAction<[]>;
28
+ onImportCancel?: () => void;
29
+ readOnly?: boolean;
30
+ loading?: boolean;
31
+ error?: string | {
32
+ message: string;
33
+ } | null;
34
+ onRetry?: () => void;
35
+ className?: string;
36
+ }
37
+ /**
38
+ * The whole editor on one page: the palette, the canvas and a side panel
39
+ * (properties, problems, JSON), around a controlled working copy. It knows
40
+ * the saved and published revisions the host holds: it marks unsaved edits
41
+ * and a draft that differs from what runs, takes a newer saved revision when
42
+ * there are no edits, and when the saved revision moves under unsaved edits
43
+ * it stops and asks (use theirs, keep mine, compare). Publishing shows the
44
+ * draft against live first and is held while anything blocks. Save, publish,
45
+ * save as, import and export are the host's; it never persists anything. On
46
+ * narrow containers the palette and the side panel open as drawers.
47
+ */
48
+ export declare function WorkflowWorkspace(props: WorkflowWorkspaceProps): React.JSX.Element;
49
+ export {};