@calliopelabs/ui 0.7.0 → 0.12.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 (124) hide show
  1. package/README.md +266 -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 +44 -3
  28. package/dist/index.js +40 -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 +430 -0
  49. package/dist/provider/messages.js +666 -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/WorkflowQuestionCard.d.ts +18 -0
  87. package/dist/workflow/WorkflowQuestionCard.js +78 -0
  88. package/dist/workflow/canvas/WorkflowCanvas.d.ts +39 -0
  89. package/dist/workflow/canvas/WorkflowCanvas.js +648 -0
  90. package/dist/workflow/definition.d.ts +408 -0
  91. package/dist/workflow/definition.js +411 -0
  92. package/dist/workflow/editor/WorkflowDiff.d.ts +17 -0
  93. package/dist/workflow/editor/WorkflowDiff.js +25 -0
  94. package/dist/workflow/editor/WorkflowImportPreview.d.ts +19 -0
  95. package/dist/workflow/editor/WorkflowImportPreview.js +50 -0
  96. package/dist/workflow/editor/WorkflowInspector.d.ts +40 -0
  97. package/dist/workflow/editor/WorkflowInspector.js +349 -0
  98. package/dist/workflow/editor/WorkflowJsonView.d.ts +16 -0
  99. package/dist/workflow/editor/WorkflowJsonView.js +40 -0
  100. package/dist/workflow/editor/WorkflowPalette.d.ts +16 -0
  101. package/dist/workflow/editor/WorkflowPalette.js +36 -0
  102. package/dist/workflow/editor/WorkflowProblems.d.ts +20 -0
  103. package/dist/workflow/editor/WorkflowProblems.js +51 -0
  104. package/dist/workflow/editor/WorkflowWorkspace.d.ts +49 -0
  105. package/dist/workflow/editor/WorkflowWorkspace.js +154 -0
  106. package/dist/workflow/gallery.d.ts +21 -0
  107. package/dist/workflow/gallery.js +29 -0
  108. package/dist/workflow/layout.d.ts +49 -0
  109. package/dist/workflow/layout.js +162 -0
  110. package/dist/workflow/model.d.ts +50 -0
  111. package/dist/workflow/model.js +7 -0
  112. package/dist/workflow/palette.d.ts +34 -0
  113. package/dist/workflow/palette.js +70 -0
  114. package/dist/workflow/portable.d.ts +162 -0
  115. package/dist/workflow/portable.js +547 -0
  116. package/dist/workflow/problems.d.ts +38 -0
  117. package/dist/workflow/problems.js +409 -0
  118. package/dist/workflow/question.d.ts +26 -0
  119. package/dist/workflow/question.js +1 -0
  120. package/dist/workflow/run.d.ts +94 -0
  121. package/dist/workflow/run.js +31 -0
  122. package/dist/workflow/workspace.d.ts +33 -0
  123. package/dist/workflow/workspace.js +17 -0
  124. package/package.json +6 -1
@@ -0,0 +1,154 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { CopyPlusIcon, DownloadIcon, ListTreeIcon, PanelRightIcon, RocketIcon, SaveIcon, UndoIcon, UploadIcon, } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../../lib/cn.js";
6
+ import { ConfirmDialog } from "../../overlays/ConfirmDialog.js";
7
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../overlays/Dialog.js";
8
+ import { Drawer, DrawerContent, DrawerTitle } from "../../overlays/Drawer.js";
9
+ import { Button } from "../../primitives/Button.js";
10
+ import { Field } from "../../primitives/Field.js";
11
+ import { Input } from "../../primitives/Input.js";
12
+ import { Segmented } from "../../primitives/Segmented.js";
13
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../primitives/Tabs.js";
14
+ import { useUI } from "../../provider/UIProvider.js";
15
+ import { ErrorState } from "../../states/ErrorState.js";
16
+ import { LoadingState } from "../../states/LoadingState.js";
17
+ import { StatusBadge } from "../../status/StatusBadge.js";
18
+ import { WorkflowCanvas } from "../canvas/WorkflowCanvas.js";
19
+ import { insertPaletteItem } from "../palette.js";
20
+ import { diffDefinitions, sameDefinition, } from "../portable.js";
21
+ import { blockingProblems, checkDefinition } from "../problems.js";
22
+ import { documentStatus } from "../workspace.js";
23
+ import { WorkflowDiff } from "./WorkflowDiff.js";
24
+ import { WorkflowImportPreview } from "./WorkflowImportPreview.js";
25
+ import { WorkflowInspector } from "./WorkflowInspector.js";
26
+ import { WorkflowJsonView } from "./WorkflowJsonView.js";
27
+ import { WorkflowPalette } from "./WorkflowPalette.js";
28
+ import { WorkflowProblems } from "./WorkflowProblems.js";
29
+ const messageOf = (e) => (e instanceof Error ? e.message : typeof e === "string" ? e : "");
30
+ /**
31
+ * The whole editor on one page: the palette, the canvas and a side panel
32
+ * (properties, problems, JSON), around a controlled working copy. It knows
33
+ * the saved and published revisions the host holds: it marks unsaved edits
34
+ * and a draft that differs from what runs, takes a newer saved revision when
35
+ * there are no edits, and when the saved revision moves under unsaved edits
36
+ * it stops and asks (use theirs, keep mine, compare). Publishing shows the
37
+ * draft against live first and is held while anything blocks. Save, publish,
38
+ * save as, import and export are the host's; it never persists anything. On
39
+ * narrow containers the palette and the side panel open as drawers.
40
+ */
41
+ export function WorkflowWorkspace(props) {
42
+ const { messages } = useUI();
43
+ if (props.loading)
44
+ return (_jsx(LoadingState, { variant: "page", label: messages.workspace.loading, className: props.className }));
45
+ if (props.error || !props.definition)
46
+ return (_jsx(ErrorState, { variant: "page", title: messages.workspace.loadFailed, error: props.error ?? null, onRetry: props.onRetry, className: props.className }));
47
+ return _jsx(Workspace, { ...props, definition: props.definition });
48
+ }
49
+ function Workspace({ definition, onChange, registry, saved = null, published = null, onSave, onPublish, onSaveAs, onExport, onImport, importPreview = null, onImportConfirm, onImportCancel, readOnly: readOnlyProp = false, className, }) {
50
+ const { messages } = useUI();
51
+ const m = messages.workspace;
52
+ const readOnly = readOnlyProp || !onChange;
53
+ const [base, setBase] = React.useState(saved ?? published);
54
+ const [flowName, setFlowName] = React.useState(definition.main);
55
+ const flow = definition.flows[flowName] ? flowName : definition.main;
56
+ const [selection, setSelection] = React.useState(null);
57
+ const [panel, setPanel] = React.useState("inspector");
58
+ const [drawer, setDrawer] = React.useState(null);
59
+ const [dialog, setDialog] = React.useState(null);
60
+ const [busy, setBusy] = React.useState(null);
61
+ const [failure, setFailure] = React.useState(null);
62
+ const [notice, setNotice] = React.useState(null);
63
+ const [name, setName] = React.useState("");
64
+ // A newer saved revision: take it when it is this copy, or when there is nothing to lose.
65
+ const [seen, setSeen] = React.useState(saved?.id);
66
+ const [adopt, setAdopt] = React.useState(null);
67
+ if (saved && saved.id !== seen) {
68
+ setSeen(saved.id);
69
+ const same = sameDefinition(definition, saved.definition);
70
+ if (same || !base || sameDefinition(definition, base.definition)) {
71
+ setBase(saved);
72
+ if (!same)
73
+ setAdopt(saved.id);
74
+ }
75
+ }
76
+ if (adopt && (!saved || saved.id !== adopt || sameDefinition(definition, saved.definition)))
77
+ setAdopt(null);
78
+ React.useEffect(() => {
79
+ if (adopt && saved?.id === adopt)
80
+ onChange?.(saved.definition);
81
+ }, [adopt, saved, onChange]);
82
+ const status = documentStatus(definition, base, saved, published);
83
+ const problems = React.useMemo(() => checkDefinition(definition, registry), [definition, registry]);
84
+ const blocking = blockingProblems(problems).length;
85
+ const warnings = problems.filter((p) => p.level === "warning").length;
86
+ const run = async (kind, action, done) => {
87
+ setBusy(kind);
88
+ setFailure(null);
89
+ setNotice(null);
90
+ try {
91
+ const result = await action();
92
+ if (result === false)
93
+ return false;
94
+ setNotice(done);
95
+ return true;
96
+ }
97
+ catch (e) {
98
+ setFailure(messageOf(e) || m.failed);
99
+ return false;
100
+ }
101
+ finally {
102
+ setBusy(null);
103
+ }
104
+ };
105
+ const select = (s) => {
106
+ setSelection(s);
107
+ if (s && s.flow !== flow)
108
+ setFlowName(s.flow);
109
+ };
110
+ const add = (item) => {
111
+ const label = item.label ||
112
+ (item.template.kind === "step"
113
+ ? messages.editor.customStep
114
+ : messages.editor.kind(item.template.kind));
115
+ const r = insertPaletteItem(definition, flow, item, registry, label, selection?.type === "node" && selection.flow === flow ? selection.id : null);
116
+ onChange?.(r.definition);
117
+ select({ type: "node", flow, id: r.node.id });
118
+ setDrawer(null);
119
+ };
120
+ const flows = Object.entries(definition.flows);
121
+ const edit = readOnly ? undefined : onChange;
122
+ const palette = (_jsx(WorkflowPalette, { registry: registry, onAdd: readOnly ? undefined : add, className: "min-h-0 flex-1" }));
123
+ const side = (_jsxs(Tabs, { value: panel, onValueChange: (v) => setPanel(v), className: "flex min-h-0 flex-1 flex-col", children: [_jsxs(TabsList, { "aria-label": m.panels, children: [_jsx(TabsTrigger, { value: "inspector", children: messages.editor.inspector }), _jsx(TabsTrigger, { value: "problems", children: m.problemsTab(blocking + warnings) }), _jsx(TabsTrigger, { value: "json", children: m.jsonTab })] }), _jsx(TabsContent, { value: "inspector", className: "min-h-0 flex-1 overflow-y-auto", children: _jsx(WorkflowInspector, { definition: definition, registry: registry, selection: selection, onSelectionChange: select, onChange: edit, readOnly: readOnly }) }), _jsx(TabsContent, { value: "problems", className: "min-h-0 flex-1 overflow-y-auto", children: _jsx(WorkflowProblems, { problems: problems, definition: definition, onSelect: (s) => {
124
+ select(s);
125
+ setPanel("inspector");
126
+ setDrawer(null);
127
+ } }) }), _jsx(TabsContent, { value: "json", className: "min-h-0 flex-1 overflow-y-auto", children: _jsx(WorkflowJsonView, { definition: definition, onChange: edit, readOnly: readOnly }) })] }));
128
+ return (_jsxs("div", { "data-slot": "workflow-workspace", className: cn("@container flex h-full min-h-0 flex-col gap-3", className), children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-1.5", children: [_jsx("h1", { className: "truncate font-head text-xl font-semibold", children: definition.name }), _jsxs("p", { role: "status", className: "flex flex-wrap items-center gap-1.5 text-sm", children: [readOnly ? _jsx(StatusBadge, { tone: "muted", children: messages.editor.readOnly }) : null, " ", status.conflict ? (_jsx(StatusBadge, { tone: "error", children: m.conflict })) : status.dirty ? (_jsx(StatusBadge, { tone: "warn", children: m.unsaved })) : saved ? (_jsx(StatusBadge, { tone: "muted", children: m.saved(saved.label) })) : null, " ", published ? (status.unpublished ? (_jsx(StatusBadge, { tone: "info", children: m.differsFromLive(published.label) })) : (_jsx(StatusBadge, { tone: "ok", children: m.live(published.label) }))) : (_jsx(StatusBadge, { tone: "muted", children: m.notPublished }))] })] }), _jsxs("div", { role: "toolbar", "aria-label": m.actions, className: "flex flex-wrap items-center gap-1.5", children: [onImport && !readOnly ? (_jsxs(Button, { size: "sm", variant: "ghost", onClick: onImport, children: [_jsx(UploadIcon, { "aria-hidden": true }), m.import] })) : null, onExport ? (_jsxs(Button, { size: "sm", variant: "ghost", onClick: () => onExport(definition), children: [_jsx(DownloadIcon, { "aria-hidden": true }), m.export] })) : null, !readOnly && status.dirty && base ? (_jsxs(Button, { size: "sm", variant: "ghost", onClick: () => setDialog("discard"), children: [_jsx(UndoIcon, { "aria-hidden": true }), m.discard] })) : null, onSaveAs && !readOnly ? (_jsxs(Button, { size: "sm", variant: "outline", disabled: busy !== null, onClick: () => {
129
+ setName(m.copyName(definition.name));
130
+ setDialog("save-as");
131
+ }, children: [_jsx(CopyPlusIcon, { "aria-hidden": true }), m.saveAs] })) : null, onSave && !readOnly ? (_jsxs(Button, { size: "sm", variant: "outline", disabled: !status.dirty || status.conflict || busy !== null, onClick: () => void run("save", () => onSave(definition, base), m.savedNotice), children: [_jsx(SaveIcon, { "aria-hidden": true }), busy === "save" ? m.saving : m.save] })) : null, onPublish && !readOnly ? (_jsxs(Button, { size: "sm", disabled: status.conflict || busy !== null || (!status.unpublished && !!published), onClick: () => setDialog("publish"), children: [_jsx(RocketIcon, { "aria-hidden": true }), m.publish] })) : null] })] }), failure ? (_jsx("p", { role: "alert", className: "rounded-lg border border-danger-border bg-danger-bg px-3 py-2 text-sm text-danger-fg", children: failure })) : null, notice ? (_jsx("p", { className: "sr-only", role: "status", children: notice })) : null, status.conflict && saved ? (_jsxs("div", { role: "alert", className: "flex flex-wrap items-center gap-2 rounded-lg border border-warning-border bg-warning-bg px-3 py-2 text-sm text-warning-fg", children: [_jsx("span", { className: "min-w-0 flex-1", children: m.conflictDescription(saved.by, saved.label) }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => setDialog("compare"), children: m.compare }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => {
132
+ setBase(saved);
133
+ onChange?.(saved.definition);
134
+ }, children: m.useTheirs }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => setBase(saved), children: m.keepMine })] })) : null, _jsxs("div", { className: "flex min-h-0 flex-1 gap-3", children: [_jsx("aside", { "aria-label": m.paletteRegion, className: "hidden w-60 shrink-0 flex-col @5xl:flex", children: palette }), _jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col gap-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [!readOnly ? (_jsxs(Button, { size: "sm", variant: "outline", className: "@5xl:hidden", onClick: () => setDrawer("palette"), children: [_jsx(ListTreeIcon, { "aria-hidden": true }), messages.editor.palette] })) : null, _jsxs(Button, { size: "sm", variant: "outline", className: "@5xl:hidden", onClick: () => setDrawer("panel"), children: [_jsx(PanelRightIcon, { "aria-hidden": true }), m.panels] }), flows.length > 1 ? (_jsx(Segmented, { label: m.flow, size: "sm", value: flow, onValueChange: (v) => {
135
+ setFlowName(v);
136
+ setSelection(null);
137
+ }, options: flows.map(([key, f]) => ({ value: key, label: f.label ?? key })) })) : null] }), _jsx(WorkflowCanvas, { definition: definition, flow: flow, registry: registry, onChange: edit, readOnly: readOnly, selection: selection, onSelectionChange: select, problems: problems, className: "min-h-96 flex-1" }, flow)] }), _jsx("aside", { "aria-label": m.panels, className: "hidden w-88 shrink-0 flex-col @5xl:flex", children: side })] }), _jsx(Drawer, { open: drawer !== null, onOpenChange: (open) => !open && setDrawer(null), children: _jsxs(DrawerContent, { side: drawer === "palette" ? "left" : "right", className: "p-4", children: [_jsx(DrawerTitle, { children: drawer === "palette" ? messages.editor.palette : m.panels }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: drawer === "palette" ? palette : side })] }) }), _jsx(Dialog, { open: dialog === "publish", onOpenChange: (open) => !open && setDialog(null), children: _jsxs(DialogContent, { className: "max-w-xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: m.publishTitle }), _jsx(DialogDescription, { children: published ? m.publishDescription(published.label) : m.firstPublish })] }), published ? (_jsx(WorkflowDiff, { diff: diffDefinitions(published.definition, definition), flows: definition.flows, label: m.draftAgainstLive, className: "max-h-80 overflow-y-auto" })) : null, blocking ? (_jsx("p", { role: "alert", className: "text-sm text-danger-fg", children: m.publishBlocked(blocking) })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setDialog(null), children: messages.editor.cancel }), _jsx(Button, { type: "button", disabled: blocking > 0 || busy !== null, onClick: async () => {
138
+ if (await run("publish", () => onPublish?.(definition), m.publishedNotice))
139
+ setDialog(null);
140
+ }, children: busy === "publish" ? m.publishing : m.publish })] })] }) }), _jsx(Dialog, { open: dialog === "save-as", onOpenChange: (open) => !open && setDialog(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: m.saveAsTitle }), _jsx(DialogDescription, { children: m.saveAsDescription })] }), _jsxs("form", { className: "flex flex-col gap-4", onSubmit: async (e) => {
141
+ e.preventDefault();
142
+ const trimmed = name.trim();
143
+ if (!trimmed)
144
+ return;
145
+ if (await run("save-as", () => onSaveAs?.({ ...definition, name: trimmed }, trimmed), m.savedAsNotice(trimmed)))
146
+ setDialog(null);
147
+ }, children: [_jsx(Field, { label: messages.editor.name, required: true, children: _jsx(Input, { value: name, onChange: (e) => setName(e.target.value) }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setDialog(null), children: messages.editor.cancel }), _jsx(Button, { type: "submit", disabled: !name.trim() || busy !== null, children: busy === "save-as" ? m.saving : m.saveAs })] })] })] }) }), _jsx(Dialog, { open: dialog === "compare", onOpenChange: (open) => !open && setDialog(null), children: _jsxs(DialogContent, { className: "max-w-xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: m.compareTitle }), _jsx(DialogDescription, { children: m.compareDescription })] }), saved ? (_jsx(WorkflowDiff, { diff: diffDefinitions(saved.definition, definition), flows: definition.flows, label: m.compareTitle, className: "max-h-80 overflow-y-auto" })) : null] }) }), _jsx(ConfirmDialog, { open: dialog === "discard", onOpenChange: (open) => !open && setDialog(null), title: m.discardTitle, description: m.discardDescription, confirmLabel: m.discard, destructive: true, onConfirm: () => {
148
+ if (base)
149
+ onChange?.(base.definition);
150
+ setSelection(null);
151
+ } }), _jsx(Dialog, { open: !!importPreview, onOpenChange: (open) => !open && onImportCancel?.(), children: _jsxs(DialogContent, { className: "max-w-xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: m.importTitle }), _jsx(DialogDescription, { children: m.importDescription })] }), importPreview ? (_jsx(WorkflowImportPreview, { preview: importPreview, busy: busy === "import", onCancel: onImportCancel, onConfirm: onImportConfirm
152
+ ? () => void run("import", () => onImportConfirm(), m.importedNotice)
153
+ : undefined, className: "max-h-[70vh] overflow-y-auto" })) : null] }) })] }));
154
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The gallery's filters, and a filter over workflows held in memory. Pure:
3
+ * no hooks, no browser APIs. A host that searches on its server passes the
4
+ * filters to its query instead and hands the gallery the result.
5
+ */
6
+ import type { WorkflowScope, WorkflowStatus, WorkflowSummary } from "./model.js";
7
+ /** Every filter's "no filter" value. */
8
+ export declare const ANY_FILTER = "any";
9
+ export interface WorkflowGalleryFilters {
10
+ /** Free text, matched against name, description and module. */
11
+ query: string;
12
+ /** A module id, or `"any"`. */
13
+ module: string;
14
+ scope: WorkflowScope | typeof ANY_FILTER;
15
+ status: WorkflowStatus | typeof ANY_FILTER;
16
+ }
17
+ export declare const DEFAULT_WORKFLOW_FILTERS: WorkflowGalleryFilters;
18
+ /** True when any filter narrows the gallery, so an empty result is "no matches", not "none yet". */
19
+ export declare function isFiltered(filters: WorkflowGalleryFilters): boolean;
20
+ /** The workflows the filters keep, in their order. For hosts that hold the whole set. */
21
+ export declare function filterWorkflows(workflows: readonly WorkflowSummary[], filters: WorkflowGalleryFilters): WorkflowSummary[];
@@ -0,0 +1,29 @@
1
+ /** Every filter's "no filter" value. */
2
+ export const ANY_FILTER = "any";
3
+ export const DEFAULT_WORKFLOW_FILTERS = {
4
+ query: "",
5
+ module: ANY_FILTER,
6
+ scope: ANY_FILTER,
7
+ status: ANY_FILTER,
8
+ };
9
+ /** True when any filter narrows the gallery, so an empty result is "no matches", not "none yet". */
10
+ export function isFiltered(filters) {
11
+ return (filters.query.trim() !== "" ||
12
+ filters.module !== ANY_FILTER ||
13
+ filters.scope !== ANY_FILTER ||
14
+ filters.status !== ANY_FILTER);
15
+ }
16
+ /** The workflows the filters keep, in their order. For hosts that hold the whole set. */
17
+ export function filterWorkflows(workflows, filters) {
18
+ const terms = filters.query.toLocaleLowerCase().split(/\s+/).filter(Boolean);
19
+ return workflows.filter((w) => {
20
+ if (filters.module !== ANY_FILTER && w.module.id !== filters.module)
21
+ return false;
22
+ if (filters.scope !== ANY_FILTER && w.scope !== filters.scope)
23
+ return false;
24
+ if (filters.status !== ANY_FILTER && w.status !== filters.status)
25
+ return false;
26
+ const text = `${w.name} ${w.description ?? ""} ${w.module.label}`.toLocaleLowerCase();
27
+ return terms.every((term) => text.includes(term));
28
+ });
29
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Where a flow's nodes sit on the canvas, and the parts of it that fold into
3
+ * one: node sizes from their ports, a layered layout along the run (ranks by
4
+ * longest path over next edges, two barycentre sweeps to untangle branches),
5
+ * and the regions a person can collapse (a loop's body, a fan-out's branches
6
+ * up to their join). Pure.
7
+ */
8
+ import { type WorkflowDefinition, type WorkflowFlow, type WorkflowNode, type WorkflowPosition, type WorkflowRegistry } from "./definition.js";
9
+ export declare const NODE_WIDTH = 224;
10
+ /** The header: kind, label, summary. */
11
+ export declare const NODE_HEADER = 76;
12
+ /** One port row below the header. */
13
+ export declare const PORT_ROW = 22;
14
+ /** The rows a node draws below its header: inputs on the left; exits (when it has several) and outputs on the right. */
15
+ export declare function portRows(node: WorkflowNode, registry: WorkflowRegistry, definition?: WorkflowDefinition): {
16
+ left: number;
17
+ right: number;
18
+ };
19
+ export declare function nodeSize(node: WorkflowNode, registry: WorkflowRegistry, definition?: WorkflowDefinition): {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ /** Longest-path ranks over next edges. */
24
+ export declare function ranksOf(flow: WorkflowFlow): Map<string, number>;
25
+ /**
26
+ * A layered layout, left to right along the run. Every node gets a position;
27
+ * `keep` (the default) lets a node's stored `at` win, so only new nodes move.
28
+ */
29
+ export declare function layoutFlow(flow: WorkflowFlow, registry: WorkflowRegistry, options?: {
30
+ keep?: boolean;
31
+ definition?: WorkflowDefinition;
32
+ }): Record<string, WorkflowPosition>;
33
+ /** The flow with every node placed by the layered layout, stored positions replaced. */
34
+ export declare function arrangeFlow(flow: WorkflowFlow, registry: WorkflowRegistry, definition?: WorkflowDefinition): WorkflowFlow;
35
+ /** A part of a flow that folds into one: a loop's body or a fan-out's branches. */
36
+ export interface WorkflowRegion {
37
+ /** `loop:<from>><to>` or `branches:<fan-out id>`. */
38
+ id: string;
39
+ kind: "loop" | "branches";
40
+ /** The node it is named after: the loop's first node, or the fan-out. */
41
+ head: string;
42
+ members: readonly string[];
43
+ }
44
+ /**
45
+ * The regions of a flow: each loop's body (from where it returns to where it
46
+ * leaves) and each fan-out's branches up to their nearest join. Regions that
47
+ * overlap without nesting are dropped, so they always form a tree.
48
+ */
49
+ export declare function regionsOf(flow: WorkflowFlow): WorkflowRegion[];
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Where a flow's nodes sit on the canvas, and the parts of it that fold into
3
+ * one: node sizes from their ports, a layered layout along the run (ranks by
4
+ * longest path over next edges, two barycentre sweeps to untangle branches),
5
+ * and the regions a person can collapse (a loop's body, a fan-out's branches
6
+ * up to their join). Pure.
7
+ */
8
+ import { downstream, exitsOf, inputsOf, outputsOf, upstream, } from "./definition.js";
9
+ export const NODE_WIDTH = 224;
10
+ /** The header: kind, label, summary. */
11
+ export const NODE_HEADER = 76;
12
+ /** One port row below the header. */
13
+ export const PORT_ROW = 22;
14
+ const GAP_ALONG = 72;
15
+ const GAP_ACROSS = 40;
16
+ /** The rows a node draws below its header: inputs on the left; exits (when it has several) and outputs on the right. */
17
+ export function portRows(node, registry, definition) {
18
+ const exits = exitsOf(node);
19
+ return {
20
+ left: inputsOf(node, registry, definition).length,
21
+ right: (exits.length > 1 ? exits.length : 0) + outputsOf(node, registry, definition).length,
22
+ };
23
+ }
24
+ export function nodeSize(node, registry, definition) {
25
+ const { left, right } = portRows(node, registry, definition);
26
+ const rows = Math.max(left, right);
27
+ return { width: NODE_WIDTH, height: NODE_HEADER + (rows ? rows * PORT_ROW + 8 : 0) };
28
+ }
29
+ /** Longest-path ranks over next edges. */
30
+ export function ranksOf(flow) {
31
+ const rank = new Map(flow.nodes.map((n) => [n.id, 0]));
32
+ const next = flow.edges.filter((e) => e.kind === "next" && rank.has(e.from) && rank.has(e.to));
33
+ for (let pass = 0; pass < flow.nodes.length; pass++) {
34
+ let changed = false;
35
+ for (const e of next) {
36
+ const want = (rank.get(e.from) ?? 0) + 1;
37
+ if (want > (rank.get(e.to) ?? 0)) {
38
+ rank.set(e.to, want);
39
+ changed = true;
40
+ }
41
+ }
42
+ if (!changed)
43
+ break;
44
+ }
45
+ return rank;
46
+ }
47
+ /**
48
+ * A layered layout, left to right along the run. Every node gets a position;
49
+ * `keep` (the default) lets a node's stored `at` win, so only new nodes move.
50
+ */
51
+ export function layoutFlow(flow, registry, options = {}) {
52
+ const keep = options.keep ?? true;
53
+ const rank = ranksOf(flow);
54
+ const byRank = [];
55
+ for (const n of flow.nodes)
56
+ (byRank[rank.get(n.id) ?? 0] ??= []).push(n.id);
57
+ for (let r = 0; r < byRank.length; r++)
58
+ byRank[r] ??= [];
59
+ const order = new Map();
60
+ byRank.forEach((ids) => ids.forEach((id, i) => order.set(id, i)));
61
+ const next = flow.edges.filter((e) => e.kind === "next");
62
+ for (let sweep = 0; sweep < 2; sweep++)
63
+ for (let r = 1; r < byRank.length; r++) {
64
+ const bary = (id) => {
65
+ const ins = next
66
+ .filter((e) => e.to === id && rank.get(e.from) === r - 1)
67
+ .map((e) => order.get(e.from) ?? 0);
68
+ return ins.length ? ins.reduce((a, b) => a + b, 0) / ins.length : (order.get(id) ?? 0);
69
+ };
70
+ const sorted = [...(byRank[r] ?? [])].sort((a, b) => bary(a) - bary(b) || (order.get(a) ?? 0) - (order.get(b) ?? 0));
71
+ byRank[r] = sorted;
72
+ sorted.forEach((id, i) => order.set(id, i));
73
+ }
74
+ const size = new Map(flow.nodes.map((n) => [n.id, nodeSize(n, registry, options.definition)]));
75
+ const heights = byRank.map((ids) => ids.reduce((sum, id, i) => sum + (size.get(id)?.height ?? 0) + (i ? GAP_ACROSS : 0), 0));
76
+ const tallest = Math.max(0, ...heights);
77
+ const out = {};
78
+ byRank.forEach((ids, r) => {
79
+ let y = (tallest - (heights[r] ?? 0)) / 2;
80
+ for (const id of ids) {
81
+ out[id] = { x: r * (NODE_WIDTH + GAP_ALONG), y: Math.round(y) };
82
+ y += (size.get(id)?.height ?? 0) + GAP_ACROSS;
83
+ }
84
+ });
85
+ if (keep)
86
+ for (const n of flow.nodes)
87
+ if (n.at)
88
+ out[n.id] = n.at;
89
+ return out;
90
+ }
91
+ /** The flow with every node placed by the layered layout, stored positions replaced. */
92
+ export function arrangeFlow(flow, registry, definition) {
93
+ const at = layoutFlow(flow, registry, { keep: false, definition });
94
+ return { ...flow, nodes: flow.nodes.map((n) => ({ ...n, at: at[n.id] ?? n.at })) };
95
+ }
96
+ /**
97
+ * The regions of a flow: each loop's body (from where it returns to where it
98
+ * leaves) and each fan-out's branches up to their nearest join. Regions that
99
+ * overlap without nesting are dropped, so they always form a tree.
100
+ */
101
+ export function regionsOf(flow) {
102
+ const found = [];
103
+ for (const e of flow.edges) {
104
+ if (e.kind !== "loop")
105
+ continue;
106
+ const down = downstream(flow, e.to);
107
+ const up = upstream(flow, e.from);
108
+ const members = [
109
+ e.to,
110
+ ...flow.nodes.map((n) => n.id).filter((id) => down.has(id) && up.has(id)),
111
+ e.from,
112
+ ];
113
+ const unique = [...new Set(members)];
114
+ if (unique.length >= 2)
115
+ found.push({ id: `loop:${e.from}>${e.to}`, kind: "loop", head: e.to, members: unique });
116
+ }
117
+ for (const n of flow.nodes) {
118
+ if (n.kind !== "fanout")
119
+ continue;
120
+ const dist = new Set([n.id]);
121
+ const queue = [n.id];
122
+ let join = null;
123
+ while (queue.length && !join) {
124
+ const id = queue.shift();
125
+ for (const e of flow.edges)
126
+ if (e.kind === "next" && e.from === id && !dist.has(e.to)) {
127
+ dist.add(e.to);
128
+ if (flow.nodes.find((x) => x.id === e.to)?.kind === "join") {
129
+ join = e.to;
130
+ break;
131
+ }
132
+ queue.push(e.to);
133
+ }
134
+ }
135
+ if (!join)
136
+ continue;
137
+ const down = downstream(flow, n.id);
138
+ const up = upstream(flow, join);
139
+ const members = [
140
+ n.id,
141
+ ...flow.nodes.map((x) => x.id).filter((id) => down.has(id) && up.has(id)),
142
+ join,
143
+ ];
144
+ found.push({
145
+ id: `branches:${n.id}`,
146
+ kind: "branches",
147
+ head: n.id,
148
+ members: [...new Set(members)],
149
+ });
150
+ }
151
+ found.sort((a, b) => b.members.length - a.members.length);
152
+ const kept = [];
153
+ for (const r of found) {
154
+ const ok = kept.every((k) => {
155
+ const shared = r.members.filter((id) => k.members.includes(id)).length;
156
+ return shared === 0 || shared === r.members.length || shared === k.members.length;
157
+ });
158
+ if (ok && !kept.some((k) => k.id === r.id))
159
+ kept.push(r);
160
+ }
161
+ return kept;
162
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The shared workflow model: what a host hands the workflow components. It is
3
+ * module-neutral. A module (software, content, media, legal, paperwork, a
4
+ * process) is a label and a version the host supplies; nothing here assumes
5
+ * tickets, repositories, branches or code. Pure: no hooks, no browser APIs.
6
+ */
7
+ /** A saved definition's publication state. */
8
+ export type WorkflowStatus = "draft" | "published";
9
+ /** Who can see and use a saved definition. */
10
+ export type WorkflowScope = "private" | "project" | "organization" | "shared";
11
+ /** The module a workflow belongs to, as the host names it. */
12
+ export interface WorkflowModule {
13
+ /** Stable key, for filters ("media", "legal"). */
14
+ id: string;
15
+ /** Display name, in the host's copy ("Media", "Legal review"). */
16
+ label: string;
17
+ /** The module version the definition was written against. */
18
+ version?: string;
19
+ }
20
+ /**
21
+ * Whether this factory can run the workflow, as the host has established it.
22
+ * `unsupported`: the module is not installed or not qualified here.
23
+ * `missing-capabilities`: the module is present but a capability it needs is not.
24
+ * Only `available` lets a start action be enabled; the host still owns
25
+ * authorization and execution.
26
+ */
27
+ export interface WorkflowAvailability {
28
+ status: "available" | "unsupported" | "missing-capabilities";
29
+ /** The capabilities this destination lacks, by display name. */
30
+ missing?: readonly string[];
31
+ /** The host's own explanation, shown in place of the default. */
32
+ reason?: string;
33
+ }
34
+ /** One saved, reusable workflow definition, as a gallery shows it. */
35
+ export interface WorkflowSummary {
36
+ /** Stable across revisions. */
37
+ id: string;
38
+ name: string;
39
+ description?: string;
40
+ module: WorkflowModule;
41
+ /** The definition's revision, as the host labels it ("12", "2026-11-14.3"). */
42
+ revision?: string;
43
+ scope: WorkflowScope;
44
+ status: WorkflowStatus;
45
+ availability: WorkflowAvailability;
46
+ }
47
+ /** The actions a workflow card can offer. */
48
+ export type WorkflowAction = "edit" | "start" | "export";
49
+ /** What the person may do, as the host's authorization says. Omitted means allowed. */
50
+ export type WorkflowPermissions = Partial<Record<WorkflowAction, boolean>>;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The shared workflow model: what a host hands the workflow components. It is
3
+ * module-neutral. A module (software, content, media, legal, paperwork, a
4
+ * process) is a label and a version the host supplies; nothing here assumes
5
+ * tickets, repositories, branches or code. Pure: no hooks, no browser APIs.
6
+ */
7
+ export {};
@@ -0,0 +1,34 @@
1
+ /**
2
+ * What can be added to a flow, grouped as the palette and the canvas's add
3
+ * menu show it: each module's step presets, then gates by kind, logic, and
4
+ * judges, then a custom step and a sub-flow. Pure.
5
+ */
6
+ import { type WorkflowDefinition, type WorkflowNode, type WorkflowNodeTemplate, type WorkflowRegistry } from "./definition.js";
7
+ export interface WorkflowPaletteItem {
8
+ /** Unique across the palette. */
9
+ key: string;
10
+ template: WorkflowNodeTemplate;
11
+ /** The preset's or gate kind's own words; empty for a bare kind, which the caller's copy names. */
12
+ label: string;
13
+ hint?: string;
14
+ /** Whether this destination runs it; an unavailable item can still be added. */
15
+ available: boolean;
16
+ }
17
+ export interface WorkflowPaletteGroup {
18
+ /** A module id, or `gates`, `logic`, `judges`, `other`. */
19
+ key: string;
20
+ /** A module's label; empty for the fixed groups, which the caller's copy names. */
21
+ label: string;
22
+ module: boolean;
23
+ items: WorkflowPaletteItem[];
24
+ }
25
+ export declare function paletteOf(registry: WorkflowRegistry): WorkflowPaletteGroup[];
26
+ /**
27
+ * The definition with a palette item added to a flow, after `after` (or the
28
+ * flow's last node) on its first free exit, placed one column to its right.
29
+ * `name` is the caller's copy for an item with no label of its own.
30
+ */
31
+ export declare function insertPaletteItem(definition: WorkflowDefinition, flowName: string, item: WorkflowPaletteItem, registry: WorkflowRegistry, name: string, after?: string | null): {
32
+ definition: WorkflowDefinition;
33
+ node: WorkflowNode;
34
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * What can be added to a flow, grouped as the palette and the canvas's add
3
+ * menu show it: each module's step presets, then gates by kind, logic, and
4
+ * judges, then a custom step and a sub-flow. Pure.
5
+ */
6
+ import { addNode, createNode, freshId, withFlow, } from "./definition.js";
7
+ import { NODE_WIDTH, layoutFlow } from "./layout.js";
8
+ const LOGIC = ["condition", "goto", "fanout", "join", "variants"];
9
+ const JUDGES = ["evaluator", "classifier", "check"];
10
+ export function paletteOf(registry) {
11
+ const runs = (kind) => (registry.nodeKinds?.[kind]?.status ?? "available") === "available";
12
+ const bare = (kind) => ({
13
+ key: kind,
14
+ template: { kind },
15
+ label: "",
16
+ available: runs(kind),
17
+ });
18
+ const modules = registry.modules.map((m) => ({
19
+ key: m.id,
20
+ label: m.label,
21
+ module: true,
22
+ items: m.presets.map((p) => ({
23
+ key: `${m.id}/${p.id}`,
24
+ template: { kind: "step", preset: { module: m.id, id: p.id } },
25
+ label: p.label,
26
+ ...(p.hint ? { hint: p.hint } : {}),
27
+ available: runs("step") &&
28
+ (m.availability?.status ?? "available") === "available" &&
29
+ (!registry.capabilities ||
30
+ (p.capabilities ?? []).every((c) => registry.capabilities?.includes(c))),
31
+ })),
32
+ }));
33
+ return [
34
+ ...modules.filter((m) => m.items.length),
35
+ {
36
+ key: "gates",
37
+ label: "",
38
+ module: false,
39
+ items: registry.gateKinds.map((g) => ({
40
+ key: `gate/${g.id}`,
41
+ template: { kind: "gate", gate: g.id },
42
+ label: g.label,
43
+ ...(g.hint ? { hint: g.hint } : {}),
44
+ available: runs("gate") &&
45
+ (g.availability?.status ?? "available") === "available" &&
46
+ (!registry.capabilities ||
47
+ (g.capabilities ?? []).every((c) => registry.capabilities?.includes(c))),
48
+ })),
49
+ },
50
+ { key: "logic", label: "", module: false, items: LOGIC.map(bare) },
51
+ { key: "judges", label: "", module: false, items: JUDGES.map(bare) },
52
+ { key: "other", label: "", module: false, items: [bare("step"), bare("subflow")] },
53
+ ].filter((g) => g.items.length);
54
+ }
55
+ /**
56
+ * The definition with a palette item added to a flow, after `after` (or the
57
+ * flow's last node) on its first free exit, placed one column to its right.
58
+ * `name` is the caller's copy for an item with no label of its own.
59
+ */
60
+ export function insertPaletteItem(definition, flowName, item, registry, name, after) {
61
+ const flow = definition.flows[flowName] ?? { nodes: [], edges: [] };
62
+ const id = freshId(item.label || name, Object.values(definition.flows).flatMap((f) => f.nodes.map((n) => n.id)));
63
+ const prev = flow.nodes.find((n) => n.id === after) ?? flow.nodes.at(-1);
64
+ const base = prev ? layoutFlow(flow, registry, { definition })[prev.id] : undefined;
65
+ const node = {
66
+ ...createNode(item.template, id, registry, item.label || name),
67
+ ...(base ? { at: { x: base.x + NODE_WIDTH + 72, y: base.y } } : {}),
68
+ };
69
+ return { definition: withFlow(definition, flowName, addNode(flow, node, prev?.id)), node };
70
+ }