@object-ui/app-shell 7.0.0 → 7.2.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 (141) hide show
  1. package/CHANGELOG.md +560 -0
  2. package/dist/console/AppContent.js +23 -17
  3. package/dist/console/ConsoleShell.d.ts +16 -0
  4. package/dist/console/ConsoleShell.js +43 -2
  5. package/dist/console/ai/AiChatPage.js +47 -16
  6. package/dist/console/ai/LiveCanvas.d.ts +8 -2
  7. package/dist/console/ai/LiveCanvas.js +6 -4
  8. package/dist/console/home/HomeLayout.js +5 -7
  9. package/dist/console/home/HomePage.js +1 -9
  10. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  11. package/dist/console/organizations/OrganizationsPage.js +22 -3
  12. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  13. package/dist/console/organizations/provisionEnvironment.js +64 -0
  14. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  15. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  16. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  17. package/dist/environment/EnvironmentListToolbar.js +59 -0
  18. package/dist/environment/entitlements.d.ts +90 -0
  19. package/dist/environment/entitlements.js +91 -0
  20. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  21. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  22. package/dist/hooks/useActionModal.js +15 -1
  23. package/dist/hooks/useAiSurface.d.ts +59 -0
  24. package/dist/hooks/useAiSurface.js +78 -0
  25. package/dist/hooks/useChatConversation.d.ts +30 -0
  26. package/dist/hooks/useChatConversation.js +63 -0
  27. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  28. package/dist/hooks/useConsoleActionRuntime.js +42 -10
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +10 -2
  31. package/dist/layout/AppHeader.js +28 -4
  32. package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
  33. package/dist/layout/ConsoleFloatingChatbot.js +41 -10
  34. package/dist/layout/ConsoleLayout.js +5 -6
  35. package/dist/layout/ContextSelectors.js +59 -35
  36. package/dist/layout/agentPicker.d.ts +56 -0
  37. package/dist/layout/agentPicker.js +40 -0
  38. package/dist/preview/CommitTimeline.d.ts +15 -0
  39. package/dist/preview/CommitTimeline.js +82 -0
  40. package/dist/preview/DraftPreviewBar.js +20 -7
  41. package/dist/preview/UnpublishedAppBar.js +11 -7
  42. package/dist/preview/commitHistory.d.ts +28 -0
  43. package/dist/preview/commitHistory.js +48 -0
  44. package/dist/providers/ExpressionProvider.js +9 -3
  45. package/dist/providers/MetadataProvider.js +9 -0
  46. package/dist/utils/index.d.ts +2 -2
  47. package/dist/utils/index.js +1 -1
  48. package/dist/utils/recordFormNavigation.d.ts +60 -0
  49. package/dist/utils/recordFormNavigation.js +35 -0
  50. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  51. package/dist/utils/resolvePageVarTokens.js +72 -0
  52. package/dist/views/CreateViewDialog.js +14 -1
  53. package/dist/views/FlowRunner.d.ts +2 -30
  54. package/dist/views/FlowRunner.js +18 -50
  55. package/dist/views/ObjectView.js +26 -12
  56. package/dist/views/ScreenView.d.ts +70 -0
  57. package/dist/views/ScreenView.js +73 -0
  58. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  59. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  60. package/dist/views/metadata-admin/DirectoryPage.js +2 -14
  61. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
  62. package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
  63. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  64. package/dist/views/metadata-admin/PackagesPage.js +58 -5
  65. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  66. package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
  67. package/dist/views/metadata-admin/ResourceListPage.js +28 -19
  68. package/dist/views/metadata-admin/StudioHomePage.js +6 -14
  69. package/dist/views/metadata-admin/anchors.js +20 -2
  70. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  71. package/dist/views/metadata-admin/createBody.js +30 -0
  72. package/dist/views/metadata-admin/i18n.js +108 -2
  73. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
  74. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
  75. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
  76. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  77. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  78. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  79. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  80. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  81. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  82. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
  83. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  84. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  85. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
  86. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
  87. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
  88. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
  89. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  90. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  91. package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
  92. package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
  93. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  94. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +102 -0
  95. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  96. package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
  97. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  98. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  99. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  100. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  101. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  102. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  103. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  104. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  105. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  106. package/dist/views/metadata-admin/issuePath.js +65 -0
  107. package/dist/views/metadata-admin/package-scope.d.ts +41 -0
  108. package/dist/views/metadata-admin/package-scope.js +59 -0
  109. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  110. package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
  111. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
  112. package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
  113. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  114. package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
  115. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
  116. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  117. package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
  118. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  119. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  120. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  121. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  122. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  123. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  124. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
  125. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
  126. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +17 -1
  127. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
  128. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  129. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  130. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  131. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  132. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
  133. package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
  134. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  135. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  136. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +20 -0
  137. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
  138. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +76 -2
  139. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
  140. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
  141. package/package.json +38 -38
@@ -9,7 +9,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  * threaded in: previews run in a sandbox with no params context.
10
10
  */
11
11
  import * as React from 'react';
12
- import { SchemaRenderer } from '@object-ui/react';
12
+ import { SchemaRenderer, RecordContextProvider } from '@object-ui/react';
13
+ import { buildExpandFields } from '@object-ui/core';
14
+ import { buildDefaultPageSchema } from '@object-ui/plugin-detail';
13
15
  import { PreviewShell, PreviewErrorBoundary, PreviewMessage } from './PreviewShell';
14
16
  import { OutlineStrip } from './OutlineStrip';
15
17
  import { PageBlockCanvas } from './PageBlockCanvas';
@@ -88,6 +90,113 @@ export function PagePreview({ draft, editing, selection, onSelectionChange, onPa
88
90
  onPatch({ children: next });
89
91
  onSelectionChange?.({ kind: 'block', id: `children[${next.length - 1}]`, label: newBlock.type });
90
92
  }, [canEdit, draft, onPatch, onSelectionChange, shape]);
93
+ // ── Record binding ──────────────────────────────────────────────────────
94
+ // A `type: 'record'` page's `record:*` blocks (details / highlights / path /
95
+ // alert) read their data from <RecordContextProvider>. The metadata editor
96
+ // has no record route, so without binding a sample they render the
97
+ // "bind a record to preview" placeholder — i.e. the author designs blind.
98
+ // Fetch a handful of real records of the bound object + its schema and let
99
+ // the author pick which one to preview against (mirrors the runtime
100
+ // RecordDetailView's RecordContextProvider).
101
+ // Match the runtime resolver (usePageAssignment): a record page is keyed by
102
+ // either bare `type: 'record'` (editor draft shape) or `pageType: 'record'`
103
+ // (persisted envelope shape). Both must bind a sample record so record:*
104
+ // blocks render real data.
105
+ const isRecordPage = draft?.type === 'record'
106
+ || draft?.pageType === 'record';
107
+ const recordObject = isRecordPage ? draft?.object : undefined;
108
+ const [recordSamples, setRecordSamples] = React.useState([]);
109
+ const [recordSchema, setRecordSchema] = React.useState(null);
110
+ const [selectedRecordId, setSelectedRecordId] = React.useState(null);
111
+ React.useEffect(() => {
112
+ if (!recordObject) {
113
+ setRecordSamples([]);
114
+ setRecordSchema(null);
115
+ setSelectedRecordId(null);
116
+ return;
117
+ }
118
+ let cancelled = false;
119
+ (async () => {
120
+ try {
121
+ const opts = { headers: { accept: 'application/json' }, credentials: 'include' };
122
+ // Schema first: it tells us which fields are lookup/master_detail so we
123
+ // can `$expand` them. Without expansion record:details/highlights would
124
+ // show raw foreign-key IDs (e.g. "O4VKrNesnsj2JYMa") instead of display
125
+ // names — the runtime RecordDetailView $expands for exactly this reason.
126
+ const schemaRes = await fetch(`/api/v1/meta/object/${encodeURIComponent(recordObject)}`, opts);
127
+ const schemaJson = await schemaRes.json().catch(() => null);
128
+ const schema = schemaJson?.item ?? schemaJson?.data ?? schemaJson;
129
+ const expand = buildExpandFields(schema?.fields);
130
+ const query = expand.length > 0
131
+ ? `?$top=50&$expand=${encodeURIComponent(expand.join(','))}`
132
+ : `?$top=50`;
133
+ const recsRes = await fetch(`/api/v1/data/${encodeURIComponent(recordObject)}${query}`, opts);
134
+ const recsJson = await recsRes.json().catch(() => null);
135
+ // The REST data endpoint returns `{ object, records, total, hasMore }`;
136
+ // tolerate the other common envelopes too.
137
+ const recs = Array.isArray(recsJson?.records) ? recsJson.records
138
+ : Array.isArray(recsJson?.items) ? recsJson.items
139
+ : Array.isArray(recsJson?.data) ? recsJson.data
140
+ : Array.isArray(recsJson) ? recsJson : [];
141
+ if (cancelled)
142
+ return;
143
+ setRecordSamples(recs);
144
+ setRecordSchema(schema);
145
+ // Same id-resolution order as recordIdOf so the initial selection's
146
+ // value matches an <option> even for objects keyed only by `name`.
147
+ setSelectedRecordId((prev) => prev ?? (recs[0]?.id ?? recs[0]?._id ?? recs[0]?.name ?? null));
148
+ }
149
+ catch {
150
+ if (!cancelled) {
151
+ setRecordSamples([]);
152
+ setRecordSchema(null);
153
+ }
154
+ }
155
+ })();
156
+ return () => { cancelled = true; };
157
+ }, [recordObject]);
158
+ const recordIdOf = (r) => r?.id ?? r?._id ?? r?.name;
159
+ const recordLabelOf = (r) => String(r?.name ?? r?.title ?? r?.label ?? r?.subject ?? recordIdOf(r) ?? '(record)');
160
+ const selectedRecord = React.useMemo(() => {
161
+ if (!recordSamples.length)
162
+ return null;
163
+ return recordSamples.find((r) => String(recordIdOf(r)) === String(selectedRecordId)) ?? recordSamples[0];
164
+ }, [recordSamples, selectedRecordId]);
165
+ // ── Slotted record page synthesis ────────────────────────────────────────
166
+ // A `kind: 'slotted'` page carries an empty `regions: []` plus a `slots` map
167
+ // of overrides, so the raw draft renders blank through SchemaRenderer (there
168
+ // are no regions to walk). Mirror the runtime RecordDetailView: synthesize the
169
+ // canonical default page from the bound object's schema and apply the slot
170
+ // overrides via the SAME `buildDefaultPageSchema(objectDef, { slots })` path,
171
+ // so omitted slots fall through to the synthesized header/details/discussion
172
+ // while authored slots (highlights/tabs/…) override in place. Non-slotted
173
+ // pages render their authored schema unchanged.
174
+ const isSlotted = draft?.kind === 'slotted';
175
+ const renderSchema = React.useMemo(() => {
176
+ if (!isSlotted)
177
+ return schema;
178
+ try {
179
+ const slots = draft?.slots ?? {};
180
+ // `recordSchema` arrives async; until then synthesize with no objectDef
181
+ // (structure renders immediately, field-level detail fills in on load).
182
+ return buildDefaultPageSchema(recordSchema ?? undefined, { slots });
183
+ }
184
+ catch {
185
+ return schema;
186
+ }
187
+ }, [isSlotted, schema, draft, recordSchema]);
188
+ // Wrap record-page content in the record context (+ a sample-record picker)
189
+ // so detail/highlights/path/alert blocks render real data. No-op for
190
+ // non-record pages and for record pages with no rows yet (renders the node
191
+ // unchanged so the existing placeholder still shows).
192
+ const withRecordBinding = (node) => {
193
+ if (!recordObject || !selectedRecord)
194
+ return node;
195
+ return (_jsxs(RecordContextProvider, { objectName: recordObject, recordId: recordIdOf(selectedRecord), data: selectedRecord, objectSchema: recordSchema ?? undefined, embedded: true, children: [recordSamples.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 border-b bg-muted/30 text-xs", children: [_jsx("span", { className: "text-muted-foreground shrink-0", children: "Preview record" }), _jsx("select", { className: "h-7 rounded-md border bg-background px-2 text-xs max-w-[260px]", value: String(selectedRecordId ?? ''), onChange: (e) => setSelectedRecordId(e.target.value), children: recordSamples.map((r) => {
196
+ const id = recordIdOf(r);
197
+ return _jsx("option", { value: String(id), children: recordLabelOf(r) }, String(id));
198
+ }) }), _jsxs("span", { className: "text-muted-foreground/70 shrink-0", children: [recordSamples.length, " sample", recordSamples.length === 1 ? '' : 's'] })] })), node] }));
199
+ };
91
200
  // Interface page → always mirror the runtime (InterfaceListPage), in BOTH
92
201
  // design and preview modes. These pages are config-driven, not region-
93
202
  // composed, so there is nothing to drag on a canvas: the author edits the
@@ -106,7 +215,7 @@ export function PagePreview({ draft, editing, selection, onSelectionChange, onPa
106
215
  // rename, and add blocks inline. The outline strip becomes
107
216
  // redundant in this view.
108
217
  if (designMode && shape === 'regions') {
109
- return (_jsx(PreviewShell, { hint: `page · design`, children: _jsx(PageBlockCanvas, { draft: draft, onPatch: canEdit ? onPatch : undefined, selection: selection ?? null, onSelectionChange: onSelectionChange }) }));
218
+ return (_jsx(PreviewShell, { hint: `page · design`, children: withRecordBinding(_jsx(PageBlockCanvas, { draft: draft, onPatch: canEdit ? onPatch : undefined, selection: selection ?? null, onSelectionChange: onSelectionChange })) }));
110
219
  }
111
- return (_jsx(PreviewShell, { hint: `page${designMode ? ' · design' : ''}`, children: _jsxs(PreviewErrorBoundary, { fallbackHint: "The Page schema is incomplete or references a component that hasn't been registered yet.", children: [designMode && (_jsx(OutlineStrip, { title: tr('engine.inspector.pageBlock.outlineLabel', locale), entries: blockEntries, selectedId: selectedId, onSelect: (e) => onSelectionChange?.({ kind: 'block', id: e.id, label: e.label }), onAdd: canEdit ? handleAddBlock : undefined, addLabel: tr('engine.inspector.add.block', locale) })), _jsx("div", { className: "min-h-[200px] max-h-[70vh] overflow-auto p-4", children: _jsx(SchemaRenderer, { schema: schema }) })] }) }));
220
+ return (_jsx(PreviewShell, { hint: `page${designMode ? ' · design' : ''}`, children: _jsxs(PreviewErrorBoundary, { fallbackHint: "The Page schema is incomplete or references a component that hasn't been registered yet.", children: [designMode && (_jsx(OutlineStrip, { title: tr('engine.inspector.pageBlock.outlineLabel', locale), entries: blockEntries, selectedId: selectedId, onSelect: (e) => onSelectionChange?.({ kind: 'block', id: e.id, label: e.label }), onAdd: canEdit ? handleAddBlock : undefined, addLabel: tr('engine.inspector.add.block', locale) })), withRecordBinding(_jsx("div", { className: "min-h-[200px] max-h-[70vh] overflow-auto p-4", children: _jsx(SchemaRenderer, { schema: renderSchema }) }))] }) }));
112
221
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ProblemsPanel — lists every structural + server validation issue for the
3
+ * flow draft. Each row shows the severity icon and the message; clicking a row
4
+ * selects and reveals (pans to) the offending node/edge on the canvas. Mirrors
5
+ * the "Problems" tab of an IDE / the error panel in Salesforce Flow Builder.
6
+ *
7
+ * Pure presentation: the issue list is derived upstream (see `flow-problems`)
8
+ * from the live draft, so rows clear as the author fixes each problem.
9
+ */
10
+ import * as React from 'react';
11
+ import type { FlowProblem } from './flow-problems';
12
+ export interface ProblemsPanelProps {
13
+ problems: FlowProblem[];
14
+ /** Selected element key (`node:<id>` or an edge's `edgeKey`) to highlight matching rows. */
15
+ selectedKey?: string | null;
16
+ onSelectProblem: (problem: FlowProblem) => void;
17
+ }
18
+ export declare function ProblemsPanel({ problems, selectedKey, onSelectProblem }: ProblemsPanelProps): React.JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AlertCircle, AlertTriangle, CheckCircle2, CircleDot, GitBranch } from 'lucide-react';
3
+ import { cn } from '@object-ui/components';
4
+ function targetLabel(p) {
5
+ if (p.target.kind === 'node')
6
+ return p.target.nodeId;
7
+ if (p.target.kind === 'edge')
8
+ return `${p.target.source} → ${p.target.target}`;
9
+ return 'flow';
10
+ }
11
+ export function ProblemsPanel({ problems, selectedKey, onSelectProblem }) {
12
+ const errorCount = problems.filter((p) => p.level === 'error').length;
13
+ const warningCount = problems.length - errorCount;
14
+ return (_jsxs("div", { className: "flex h-full flex-col text-xs", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2 font-medium text-muted-foreground", children: [_jsx("span", { children: "Problems" }), errorCount > 0 && (_jsxs("span", { className: "inline-flex items-center gap-1 text-destructive", children: [_jsx(AlertCircle, { className: "h-3 w-3" }), " ", errorCount] })), warningCount > 0 && (_jsxs("span", { className: "inline-flex items-center gap-1 text-amber-600 dark:text-amber-400", children: [_jsx(AlertTriangle, { className: "h-3 w-3" }), " ", warningCount] }))] }), problems.length === 0 ? (_jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-1.5 p-4 text-center text-muted-foreground", children: [_jsx(CheckCircle2, { className: "h-5 w-5 text-emerald-500" }), _jsx("span", { children: "No problems \u2014 this flow is structurally valid." })] })) : (_jsx("ul", { className: "flex-1 overflow-auto p-1.5", children: problems.map((p) => {
15
+ const isEdge = p.target.kind === 'edge';
16
+ const isFlow = p.target.kind === 'flow';
17
+ const key = p.target.kind === 'node'
18
+ ? `node:${p.target.nodeId}`
19
+ : p.target.kind === 'edge'
20
+ ? p.target.edgeKey
21
+ : null;
22
+ const active = !!key && key === selectedKey;
23
+ const Icon = p.level === 'error' ? AlertCircle : AlertTriangle;
24
+ const TargetIcon = isEdge ? GitBranch : CircleDot;
25
+ return (_jsx("li", { children: _jsxs("button", { type: "button", disabled: isFlow, onClick: () => onSelectProblem(p), className: cn('flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left transition-colors', isFlow ? 'cursor-default' : 'cursor-pointer hover:bg-accent', active && 'bg-accent ring-1 ring-primary/40'), children: [_jsx(Icon, { className: cn('mt-0.5 h-3.5 w-3.5 shrink-0', p.level === 'error' ? 'text-destructive' : 'text-amber-500') }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block leading-snug text-foreground", children: p.message }), _jsxs("span", { className: "mt-0.5 flex items-center gap-1 text-[10px] text-muted-foreground", children: [_jsx(TargetIcon, { className: "h-2.5 w-2.5 shrink-0" }), _jsx("span", { className: "truncate font-mono", children: targetLabel(p) }), p.source === 'server' && _jsx("span", { className: "uppercase tracking-wide", children: "\u00B7 schema" }), p.source === 'expression' && _jsx("span", { className: "uppercase tracking-wide", children: "\u00B7 expression" })] })] })] }) }, p.id));
26
+ }) }))] }));
27
+ }
@@ -4,15 +4,16 @@
4
4
  *
5
5
  * A 9.0 report binds a semantic-layer `dataset` and selects its measures
6
6
  * (`values`) grouped by dimensions (`rows`, plus `columns` across for a
7
- * matrix); rendering through plugin-report's `ReportRenderer` keeps the
8
- * studio preview pixel-equal with the runtime — including the matrix
9
- * cross-tab and the numbers consistent with every other surface on the
10
- * same dataset (`adapter.queryDataset`). Drill-down stays inert here: the
11
- * preview passes no `onDrill` sink.
7
+ * matrix); a `joined` report instead stacks dataset-bound `blocks`. Both
8
+ * render through plugin-report's `ReportRenderer` (→ DatasetReportRenderer),
9
+ * keeping the studio preview pixel-equal with the runtime including the
10
+ * matrix cross-tab and the joined block stack — and the numbers consistent
11
+ * with every other surface on the same dataset (`adapter.queryDataset`).
12
+ * Drill-down stays inert here: the preview passes no `onDrill` sink.
12
13
  *
13
- * A draft without a dataset binding (e.g. stored pre-9.0 query-form JSON)
14
- * gets an actionable empty state pointing at the inspector's Dataset control
15
- * instead of the retired legacy renderer.
14
+ * A draft with neither a dataset nor any dataset-bound block gets an
15
+ * actionable empty state pointing at the right inspector control instead of
16
+ * the retired pre-9.0 inline-query renderer.
16
17
  */
17
18
  import * as React from 'react';
18
19
  import type { MetadataPreviewProps } from '../preview-registry';
@@ -6,15 +6,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  *
7
7
  * A 9.0 report binds a semantic-layer `dataset` and selects its measures
8
8
  * (`values`) grouped by dimensions (`rows`, plus `columns` across for a
9
- * matrix); rendering through plugin-report's `ReportRenderer` keeps the
10
- * studio preview pixel-equal with the runtime — including the matrix
11
- * cross-tab and the numbers consistent with every other surface on the
12
- * same dataset (`adapter.queryDataset`). Drill-down stays inert here: the
13
- * preview passes no `onDrill` sink.
9
+ * matrix); a `joined` report instead stacks dataset-bound `blocks`. Both
10
+ * render through plugin-report's `ReportRenderer` (→ DatasetReportRenderer),
11
+ * keeping the studio preview pixel-equal with the runtime including the
12
+ * matrix cross-tab and the joined block stack — and the numbers consistent
13
+ * with every other surface on the same dataset (`adapter.queryDataset`).
14
+ * Drill-down stays inert here: the preview passes no `onDrill` sink.
14
15
  *
15
- * A draft without a dataset binding (e.g. stored pre-9.0 query-form JSON)
16
- * gets an actionable empty state pointing at the inspector's Dataset control
17
- * instead of the retired legacy renderer.
16
+ * A draft with neither a dataset nor any dataset-bound block gets an
17
+ * actionable empty state pointing at the right inspector control instead of
18
+ * the retired pre-9.0 inline-query renderer.
18
19
  */
19
20
  import * as React from 'react';
20
21
  import { Database, Loader2 } from 'lucide-react';
@@ -23,13 +24,29 @@ import { PreviewShell, PreviewErrorBoundary, PreviewEmptyState } from './Preview
23
24
  const ReportRenderer = React.lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportRenderer })));
24
25
  export function ReportPreview({ draft }) {
25
26
  const adapter = useAdapter();
26
- // ADR-0021 single-form: a report binds a semantic-layer dataset.
27
- if (typeof draft.dataset === 'string' && draft.dataset) {
28
- const rows = Array.isArray(draft.rows) ? draft.rows.filter(Boolean) : [];
29
- return (_jsx(PreviewShell, { hint: `report · dataset "${draft.dataset}"${rows.length ? ' · by ' + rows.join(', ') : ''}`, children: _jsx(PreviewErrorBoundary, { fallbackHint: "The Report references a dataset/measure that doesn't resolve, or its config is incomplete.", children: _jsx(React.Suspense, { fallback: _jsxs("div", { className: "flex items-center gap-2 p-4 text-xs text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Loading report renderer\u2026"] }), children: _jsx("div", { className: "p-3 min-h-[200px] max-h-[70vh] overflow-auto", children: _jsx(ReportRenderer, { schema: draft, dataSource: adapter }) }) }) }) }));
27
+ const d = draft;
28
+ // ADR-0021 single-form: a report binds a semantic-layer dataset; a `joined`
29
+ // report instead carries its data on dataset-bound `blocks`. Both render
30
+ // through plugin-report's ReportRenderer ( DatasetReportRenderer, which
31
+ // stacks each block). Previously only the single-dataset shape was
32
+ // previewed, so a joined report fell through to the "bind a dataset" empty
33
+ // state and the author designed blind.
34
+ const hasDataset = typeof d.dataset === 'string' && !!d.dataset;
35
+ const isJoinedWithBlocks = d.type === 'joined' &&
36
+ Array.isArray(d.blocks) &&
37
+ d.blocks.some((b) => typeof b?.dataset === 'string' && !!b.dataset);
38
+ if (hasDataset || isJoinedWithBlocks) {
39
+ const rows = Array.isArray(d.rows) ? d.rows.filter(Boolean) : [];
40
+ const hint = isJoinedWithBlocks
41
+ ? `report · joined · ${d.blocks.length} block${d.blocks.length === 1 ? '' : 's'}`
42
+ : `report · dataset "${d.dataset}"${rows.length ? ' · by ' + rows.join(', ') : ''}`;
43
+ return (_jsx(PreviewShell, { hint: hint, children: _jsx(PreviewErrorBoundary, { fallbackHint: "The Report references a dataset/measure that doesn't resolve, or its config is incomplete.", children: _jsx(React.Suspense, { fallback: _jsxs("div", { className: "flex items-center gap-2 p-4 text-xs text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Loading report renderer\u2026"] }), children: _jsx("div", { className: "p-3 min-h-[200px] max-h-[70vh] overflow-auto", children: _jsx(ReportRenderer, { schema: draft, dataSource: adapter }) }) }) }) }));
30
44
  }
31
- // No dataset bound either a fresh draft or stored pre-9.0 query-form
32
- // JSON (objectName/columns), whose inline-query renderer was retired with
33
- // the 9.0 cutover. Point the author at the dataset binding.
34
- return (_jsx(PreviewShell, { children: _jsx(PreviewEmptyState, { icon: _jsx(Database, { className: "h-8 w-8" }), title: "Bind a dataset to preview this report", description: "Since the 9.0 single-form cutover a report renders its dataset's measures (values) grouped by dimensions (rows). Choose a Dataset in the right panel to start designing." }) }));
45
+ // Nothing renderable yet. A joined report needs at least one dataset-bound
46
+ // block; every other type needs a top-level dataset. Point the author at the
47
+ // right control instead of the retired pre-9.0 inline-query renderer.
48
+ const joined = d.type === 'joined';
49
+ return (_jsx(PreviewShell, { children: _jsx(PreviewEmptyState, { icon: _jsx(Database, { className: "h-8 w-8" }), title: joined ? 'Add a block to preview this joined report' : 'Bind a dataset to preview this report', description: joined
50
+ ? 'A joined report stacks dataset-bound blocks. Add a block and bind its dataset + measures in the right panel to start designing.'
51
+ : "Since the 9.0 single-form cutover a report renders its dataset's measures (values) grouped by dimensions (rows). Choose a Dataset in the right panel to start designing." }) }));
35
52
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ScreenPreview — live design-time preview of a flow `screen` node, rendered
3
+ * exactly as the end user will see it at runtime.
4
+ *
5
+ * It builds a runtime `ScreenSpec` from the node's authored `config` and hands
6
+ * it to the shared {@link ScreenView} — the SAME renderer the runtime
7
+ * FlowRunner uses — so the preview can never drift from runtime (the
8
+ * design↔runtime divergence #1927 set out to kill). `{var}` references in the
9
+ * title/description are interpolated, and `fields` are gated by their
10
+ * `visibleWhen`, against the supplied `variables` (the flow's declared defaults
11
+ * in the inspector, or the live simulated values when paused in the Debug
12
+ * simulator).
13
+ *
14
+ * Object-form mode is fed the SAME enriched object list the runtime uses
15
+ * (`useMetadata().objects`, which derives inline master-detail `subforms` from
16
+ * `inlineEdit` relationships) so the preview renders those child grids too.
17
+ *
18
+ * Homes: the flow node inspector (live-updates as the config is edited) and the
19
+ * Debug simulator's paused-at-screen state.
20
+ */
21
+ import * as React from 'react';
22
+ import { type ScreenPreviewNode } from './screen-spec';
23
+ export type { ScreenPreviewNode } from './screen-spec';
24
+ export interface ScreenPreviewProps {
25
+ /** The screen node to preview. */
26
+ node: ScreenPreviewNode;
27
+ /**
28
+ * Variable values for `{var}` interpolation in the title/description AND for
29
+ * evaluating each field's `visibleWhen`. The inspector passes the flow's
30
+ * declared defaults; the simulator passes the live run state at the pause
31
+ * point. Unknown `{var}` refs stay literal, and fields whose `visibleWhen`
32
+ * can't be decided stay visible — the design preview never hides on missing
33
+ * data.
34
+ */
35
+ variables?: Record<string, unknown>;
36
+ className?: string;
37
+ }
38
+ export declare function ScreenPreview({ node, variables, className }: ScreenPreviewProps): React.JSX.Element;
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * ScreenPreview — live design-time preview of a flow `screen` node, rendered
5
+ * exactly as the end user will see it at runtime.
6
+ *
7
+ * It builds a runtime `ScreenSpec` from the node's authored `config` and hands
8
+ * it to the shared {@link ScreenView} — the SAME renderer the runtime
9
+ * FlowRunner uses — so the preview can never drift from runtime (the
10
+ * design↔runtime divergence #1927 set out to kill). `{var}` references in the
11
+ * title/description are interpolated, and `fields` are gated by their
12
+ * `visibleWhen`, against the supplied `variables` (the flow's declared defaults
13
+ * in the inspector, or the live simulated values when paused in the Debug
14
+ * simulator).
15
+ *
16
+ * Object-form mode is fed the SAME enriched object list the runtime uses
17
+ * (`useMetadata().objects`, which derives inline master-detail `subforms` from
18
+ * `inlineEdit` relationships) so the preview renders those child grids too.
19
+ *
20
+ * Homes: the flow node inspector (live-updates as the config is edited) and the
21
+ * Debug simulator's paused-at-screen state.
22
+ */
23
+ import * as React from 'react';
24
+ import { Button, cn } from '@object-ui/components';
25
+ import { useAdapter } from '../../../providers/AdapterProvider';
26
+ import { useMetadata } from '../../../providers/MetadataProvider';
27
+ import { ScreenView, isObjectFormScreen, initialScreenValues } from '../../ScreenView';
28
+ import { buildScreenSpec, interpolate, hiddenFieldCount } from './screen-spec';
29
+ export function ScreenPreview({ node, variables, className }) {
30
+ const adapter = useAdapter();
31
+ const meta = useMetadata();
32
+ const spec = React.useMemo(() => buildScreenSpec(node, variables), [node, variables]);
33
+ const isObjectForm = isObjectFormScreen(spec);
34
+ // Enriched object defs (incl. derived master-detail `subforms`) — the exact
35
+ // list the runtime FlowRunner gets. Only read in object-form mode so a plain
36
+ // field screen never triggers the all-objects fetch.
37
+ const objects = isObjectForm ? meta.objects : undefined;
38
+ const title = interpolate(spec.title, variables);
39
+ const description = interpolate(spec.description, variables);
40
+ const hidden = isObjectForm ? 0 : hiddenFieldCount(node, variables);
41
+ // Reset transient input state when the screen's STRUCTURE changes (fields
42
+ // added/removed/retyped/gated, or object-form target/mode); typing survives a
43
+ // label/title-only edit.
44
+ const structKey = isObjectForm
45
+ ? `obj:${spec.objectName}:${spec.mode ?? 'create'}`
46
+ : 'fields:' + spec.fields.map((f) => `${f.name}:${f.type ?? ''}:${f.required ? 1 : 0}`).join('|');
47
+ const empty = !title && !description && !isObjectForm && spec.fields.length === 0 && hidden === 0;
48
+ return (_jsxs("div", { className: cn('overflow-hidden rounded-md border bg-background', className), children: [_jsx("div", { className: "flex items-center gap-1.5 border-b bg-muted/30 px-3 py-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Preview" }), _jsx("div", { className: "max-h-[60vh] overflow-auto p-4", children: empty ? (_jsx("p", { className: "text-sm italic text-muted-foreground", children: "Add a title, description, fields, or an object form to preview this screen." })) : (_jsxs(_Fragment, { children: [title && _jsx("h3", { className: "text-base font-semibold leading-tight", children: title }), description && (_jsx("p", { className: cn('whitespace-pre-line text-sm text-muted-foreground', title && 'mt-1'), children: description })), _jsx(ScreenFormPreview, { spec: spec, adapter: adapter, objects: objects }, structKey), !isObjectForm && hidden > 0 && (_jsxs("p", { className: "mt-3 text-[11px] italic text-muted-foreground", children: [hidden, " field", hidden === 1 ? '' : 's', " hidden by ", hidden === 1 ? 'its' : 'their', " \u201Cvisible when\u201D", ' ', "condition", hidden === 1 ? '' : 's', "."] })), !isObjectForm && spec.fields.length > 0 && (_jsx("div", { className: "mt-4 flex justify-end", children: _jsx(Button, { size: "sm", disabled: true, children: "Submit" }) }))] })) })] }));
49
+ }
50
+ /**
51
+ * Holds the transient field values so the preview is interactive (the author
52
+ * can type into it) while never persisting anything.
53
+ */
54
+ function ScreenFormPreview({ spec, adapter, objects }) {
55
+ const [values, setValues] = React.useState(() => initialScreenValues(spec));
56
+ return (_jsx(ScreenView, { screen: spec, values: values, onValueChange: (name, v) => setValues((p) => ({ ...p, [name]: v })), dataSource: adapter ?? undefined, objects: objects, objectForm: {
57
+ showSubmit: false,
58
+ showCancel: false,
59
+ noDataSourceMessage: 'Connect to a backend to preview this object form.',
60
+ } }));
61
+ }
@@ -75,6 +75,20 @@ export declare function topAnchor(p: Point): Point;
75
75
  export declare function edgePath(from: Point, to: Point): string;
76
76
  /** Midpoint of an edge — anchor for the condition label + insert affordance. */
77
77
  export declare function edgeMidpoint(from: Point, to: Point): Point;
78
+ /** True for an ADR-0044 declared back-edge (a revise/rework loop's return). */
79
+ export declare function isBackEdge(edge: Pick<FlowEdge, 'type'>): boolean;
80
+ /** Right-center anchor of a node — where a back-edge's return arc attaches. */
81
+ export declare function rightAnchor(p: Point): Point;
82
+ /**
83
+ * Curved return path for a declared back-edge (ADR-0044 revise loop). Unlike a
84
+ * forward edge (top→bottom), a back-edge re-enters an earlier node, so we route
85
+ * it off the right side of both endpoints and bow it out to the right — a
86
+ * distinct return arc that reads as "loop back" rather than crossing the
87
+ * top-to-bottom forward edges.
88
+ */
89
+ export declare function backEdgePath(from: Point, to: Point): string;
90
+ /** Anchor for a back-edge's label pill — the apex of its return arc. */
91
+ export declare function backEdgeLabelAnchor(from: Point, to: Point): Point;
78
92
  /**
79
93
  * Stable identity for an edge. Prefers an explicit `edge.id`; otherwise falls
80
94
  * back to a `source->target#index` composite so an unsaved edge still has a
@@ -40,6 +40,12 @@ export function computeLayout(nodes, edges) {
40
40
  for (const e of edges) {
41
41
  if (!byId.has(e.source) || !byId.has(e.target) || e.source === e.target)
42
42
  continue;
43
+ // ADR-0044: a declared back-edge (`type: 'back'`) re-enters an earlier node
44
+ // to close a revise loop. Exclude it from layering — exactly as the engine
45
+ // excludes it from DAG validation — so the loop doesn't drag its target
46
+ // node below the wait point. The edge is still drawn (as a return arc).
47
+ if (isBackEdge(e))
48
+ continue;
43
49
  if (!outAdj.has(e.source))
44
50
  outAdj.set(e.source, []);
45
51
  outAdj.get(e.source).push(e.target);
@@ -167,6 +173,37 @@ export function edgePath(from, to) {
167
173
  export function edgeMidpoint(from, to) {
168
174
  return { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
169
175
  }
176
+ /** True for an ADR-0044 declared back-edge (a revise/rework loop's return). */
177
+ export function isBackEdge(edge) {
178
+ return edge.type === 'back';
179
+ }
180
+ /** Right-center anchor of a node — where a back-edge's return arc attaches. */
181
+ export function rightAnchor(p) {
182
+ return { x: p.x + NODE_W, y: p.y + NODE_H / 2 };
183
+ }
184
+ /** Horizontal bow of a back-edge's return arc, scaled to the vertical span. */
185
+ function backEdgeBow(from, to) {
186
+ return Math.max(64, Math.abs(to.y - from.y) * 0.35);
187
+ }
188
+ /**
189
+ * Curved return path for a declared back-edge (ADR-0044 revise loop). Unlike a
190
+ * forward edge (top→bottom), a back-edge re-enters an earlier node, so we route
191
+ * it off the right side of both endpoints and bow it out to the right — a
192
+ * distinct return arc that reads as "loop back" rather than crossing the
193
+ * top-to-bottom forward edges.
194
+ */
195
+ export function backEdgePath(from, to) {
196
+ const bow = backEdgeBow(from, to);
197
+ const c1 = { x: from.x + bow, y: from.y };
198
+ const c2 = { x: to.x + bow, y: to.y };
199
+ return `M ${from.x},${from.y} C ${c1.x},${c1.y} ${c2.x},${c2.y} ${to.x},${to.y}`;
200
+ }
201
+ /** Anchor for a back-edge's label pill — the apex of its return arc. */
202
+ export function backEdgeLabelAnchor(from, to) {
203
+ // The cubic's t=0.5 point sits ~0.75·bow right of the (shared) right edge.
204
+ const bow = backEdgeBow(from, to);
205
+ return { x: Math.max(from.x, to.x) + bow * 0.75, y: (from.y + to.y) / 2 };
206
+ }
170
207
  /**
171
208
  * Stable identity for an edge. Prefers an explicit `edge.id`; otherwise falls
172
209
  * back to a `source->target#index` composite so an unsaved edge still has a
@@ -66,16 +66,32 @@ export interface NodeCardProps {
66
66
  runState?: 'active' | 'visited';
67
67
  /** Dim nodes not yet reached while a simulation is in progress. */
68
68
  dimmed?: boolean;
69
+ /** Structural-validation error highlight (e.g. part of an un-declared cycle). */
70
+ invalid?: boolean;
71
+ /**
72
+ * Validation badge shown at the card's top-right corner (error or warning),
73
+ * with the issue message(s) as its tooltip. Cleared when the issue resolves.
74
+ */
75
+ badge?: {
76
+ level: 'error' | 'warning';
77
+ title: string;
78
+ };
69
79
  onPointerDown?: (e: React.PointerEvent) => void;
70
80
  onSelect?: () => void;
71
81
  onAppend?: () => void;
82
+ /**
83
+ * ADR-0044: when set (approval nodes without an existing revise loop), render
84
+ * the one-click "add revision loop" affordance — drops a wait node + the
85
+ * `revise` and declared `back` edges in a single gesture.
86
+ */
87
+ onAddReviseLoop?: () => void;
72
88
  }
73
89
  /**
74
90
  * A single draggable flow node rendered at an absolute canvas coordinate.
75
91
  * The card body drives selection + reposition; a dedicated bottom "+" handle
76
92
  * (edit mode only) appends a connected child without ambiguity.
77
93
  */
78
- export declare function NodeCard({ type, label, summary, position, selected, editable, runState, dimmed, onPointerDown, onSelect, onAppend, }: NodeCardProps): React.JSX.Element;
94
+ export declare function NodeCard({ type, label, summary, position, selected, editable, runState, dimmed, invalid, badge, onPointerDown, onSelect, onAppend, onAddReviseLoop, }: NodeCardProps): React.JSX.Element;
79
95
  export interface NodePaletteProps {
80
96
  locale?: string;
81
97
  /** Node types to offer. Defaults to the hardcoded {@link NODE_PALETTE}. */
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
6
6
  * palette popover. Kept dependency-free and Shadcn-native (Tailwind + lucide).
7
7
  */
8
8
  import * as React from 'react';
9
- import { Code, CircleDot, CircleStop, Diamond, FilePen, FilePlus, FileSearch, FileX, GitFork, Globe, ListChecks, MonitorSmartphone, Play, Plug, Plus, Repeat, ShieldAlert, TimerReset, UserCheck, Variable, Workflow, Zap, } from 'lucide-react';
9
+ import { AlertCircle, AlertTriangle, Code, CircleDot, CircleStop, Diamond, FilePen, FilePlus, FileSearch, FileX, GitFork, Globe, IterationCcw, ListChecks, MonitorSmartphone, Play, Plug, Plus, Repeat, ShieldAlert, TimerReset, UserCheck, Variable, Workflow, Zap, } from 'lucide-react';
10
10
  import { cn } from '@object-ui/components';
11
11
  import { NODE_W, NODE_H } from './flow-canvas-layout';
12
12
  export function nodeIcon(type) {
@@ -188,6 +188,7 @@ export function nodeTone(type) {
188
188
  case 'delete_record':
189
189
  case 'get_record':
190
190
  return TONES.record;
191
+ case 'http':
191
192
  case 'http_request':
192
193
  case 'connector_action':
193
194
  case 'script':
@@ -237,6 +238,7 @@ export function nodeCategory(type) {
237
238
  case 'screen':
238
239
  case 'user_task':
239
240
  return 'Human';
241
+ case 'http':
240
242
  case 'http_request':
241
243
  case 'connector_action':
242
244
  case 'script':
@@ -265,7 +267,7 @@ export const NODE_PALETTE = [
265
267
  { type: 'try_catch', label: 'Try / Catch', hint: 'Protect steps with error handling and retry', category: 'Logic' },
266
268
  { type: 'approval', label: 'Approval', hint: 'Pause for a human decision', category: 'Human' },
267
269
  { type: 'screen', label: 'Screen', hint: 'Collect input from a user', category: 'Human' },
268
- { type: 'http_request', label: 'HTTP request', hint: 'Call an external API', category: 'Integration' },
270
+ { type: 'http', label: 'HTTP request', hint: 'Call an external API', category: 'Integration' },
269
271
  { type: 'connector_action', label: 'Connector', hint: 'Run an integration action', category: 'Integration' },
270
272
  { type: 'script', label: 'Script', hint: 'Run custom code', category: 'Integration' },
271
273
  { type: 'subflow', label: 'Subflow', hint: 'Invoke another flow', category: 'Flow' },
@@ -301,6 +303,7 @@ export function defaultNodeExtras(type) {
301
303
  // Seed a node-model approval: at least one approver + spec defaults. The
302
304
  // author wires the out-edges with labels `approve` / `reject`.
303
305
  return { config: { approvers: [{ type: 'manager' }], behavior: 'first_response', lockRecord: true } };
306
+ case 'http':
304
307
  case 'http_request':
305
308
  return { config: { method: 'GET' } };
306
309
  case 'script':
@@ -314,9 +317,13 @@ export function defaultNodeExtras(type) {
314
317
  * The card body drives selection + reposition; a dedicated bottom "+" handle
315
318
  * (edit mode only) appends a connected child without ambiguity.
316
319
  */
317
- export function NodeCard({ type, label, summary, position, selected, editable, runState, dimmed, onPointerDown, onSelect, onAppend, }) {
320
+ export function NodeCard({ type, label, summary, position, selected, editable, runState, dimmed, invalid, badge, onPointerDown, onSelect, onAppend, onAddReviseLoop, }) {
318
321
  const tone = nodeTone(type);
319
- return (_jsxs("div", { className: "absolute transition-opacity duration-200", style: { left: position.x, top: position.y, width: NODE_W, height: NODE_H, opacity: dimmed ? 0.35 : 1 }, children: [_jsxs("div", { role: "button", tabIndex: 0, "aria-pressed": selected, onPointerDown: onPointerDown, onClick: (e) => {
322
+ return (_jsxs("div", {
323
+ // `group` so the hover-revealed affordances (append "+", revise loop)
324
+ // appear when the pointer is anywhere over the card, not only over the
325
+ // tiny button itself.
326
+ className: "group absolute transition-opacity duration-200", "data-invalid": invalid || undefined, style: { left: position.x, top: position.y, width: NODE_W, height: NODE_H, opacity: dimmed ? 0.35 : 1 }, children: [_jsxs("div", { role: "button", tabIndex: 0, "aria-pressed": selected, onPointerDown: onPointerDown, onClick: (e) => {
320
327
  e.stopPropagation();
321
328
  onSelect?.();
322
329
  }, onKeyDown: (e) => {
@@ -330,10 +337,20 @@ export function NodeCard({ type, label, summary, position, selected, editable, r
330
337
  ? 'border-emerald-500/70 ring-1 ring-emerald-400/40'
331
338
  : selected
332
339
  ? 'border-primary shadow-md ring-2 ring-primary/30'
333
- : 'border-border/80'), children: [_jsx("div", { className: cn('flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition-transform duration-150 group-hover:scale-105', tone.chip, runState === 'active' && 'animate-pulse'), children: _jsx(NodeTypeIcon, { type: type, className: cn('h-[18px] w-[18px]', tone.icon) }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { title: label, className: "truncate text-[13px] font-semibold leading-tight text-foreground", children: label }), _jsxs("div", { className: "mt-1 flex items-baseline gap-1.5 leading-tight", children: [_jsx("span", { className: cn('shrink-0 text-[10px] font-semibold uppercase tracking-[0.08em]', tone.label), children: type }), summary && (_jsx("span", { className: "min-w-0 truncate font-mono text-[10px] text-muted-foreground", title: summary, children: summary }))] })] })] }), editable && type !== 'end' && (_jsx("button", { type: "button", title: "Add connected node", "aria-label": "Add connected node", onPointerDown: (e) => e.stopPropagation(), onClick: (e) => {
340
+ : invalid
341
+ ? 'border-destructive ring-2 ring-destructive/50'
342
+ : 'border-border/80'), children: [_jsx("div", { className: cn('flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition-transform duration-150 group-hover:scale-105', tone.chip, runState === 'active' && 'animate-pulse'), children: _jsx(NodeTypeIcon, { type: type, className: cn('h-[18px] w-[18px]', tone.icon) }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { title: label, className: "truncate text-[13px] font-semibold leading-tight text-foreground", children: label }), _jsxs("div", { className: "mt-1 flex items-baseline gap-1.5 leading-tight", children: [_jsx("span", { className: cn('shrink-0 text-[10px] font-semibold uppercase tracking-[0.08em]', tone.label), children: type }), summary && (_jsx("span", { className: "min-w-0 truncate font-mono text-[10px] text-muted-foreground", title: summary, children: summary }))] })] })] }), badge && (_jsx("span", { title: badge.title, "data-problem": badge.level, className: cn('absolute -right-2 -top-2 z-20 inline-flex h-5 w-5 items-center justify-center rounded-full border bg-background shadow-sm', badge.level === 'error'
343
+ ? 'border-destructive/50 text-destructive'
344
+ : 'border-amber-500/50 text-amber-600 dark:text-amber-400'), children: badge.level === 'error' ? (_jsx(AlertCircle, { className: "h-3 w-3" })) : (_jsx(AlertTriangle, { className: "h-3 w-3" })) })), editable && type !== 'end' && (_jsx("button", { type: "button", title: "Add connected node", "aria-label": "Add connected node", onPointerDown: (e) => e.stopPropagation(), onClick: (e) => {
334
345
  e.stopPropagation();
335
346
  onAppend?.();
336
- }, className: cn('absolute left-1/2 -bottom-3 z-10 inline-flex h-6 w-6 -translate-x-1/2 items-center justify-center rounded-full border bg-background text-muted-foreground shadow-sm transition-colors', 'opacity-0 hover:border-primary hover:text-primary group-hover:opacity-100 focus-visible:opacity-100'), children: _jsx(Plus, { className: "h-3.5 w-3.5" }) }))] }));
347
+ }, className: cn('absolute left-1/2 -bottom-3 z-10 inline-flex h-6 w-6 -translate-x-1/2 items-center justify-center rounded-full border bg-background text-muted-foreground shadow-sm transition-colors', 'opacity-0 hover:border-primary hover:text-primary group-hover:opacity-100 focus-visible:opacity-100'), children: _jsx(Plus, { className: "h-3.5 w-3.5" }) })), onAddReviseLoop && (_jsx("button", { type: "button", title: "Add revision loop (send back for revision)", "aria-label": "Add revision loop", onPointerDown: (e) => e.stopPropagation(), onClick: (e) => {
348
+ e.stopPropagation();
349
+ onAddReviseLoop();
350
+ },
351
+ // Anchored to the right edge — where the back-edge's return arc
352
+ // attaches — and tinted amber to match the rendered back-edge.
353
+ className: cn('absolute -right-3 top-1/2 z-10 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full border bg-background text-amber-600 shadow-sm transition-colors dark:text-amber-400', 'opacity-0 hover:border-amber-500 hover:text-amber-600 group-hover:opacity-100 focus-visible:opacity-100 dark:hover:text-amber-400'), children: _jsx(IterationCcw, { className: "h-3.5 w-3.5" }) }))] }));
337
354
  }
338
355
  /** Compact popover listing the node types an author can add, grouped by category. */
339
356
  export function NodePalette({ items = NODE_PALETTE, onPick, onClose }) {
@@ -0,0 +1,19 @@
1
+ import type { DiagnosticLevel } from './simulator/flow-sim-types';
2
+ export interface ExprProblem {
3
+ target: {
4
+ kind: 'node';
5
+ nodeId: string;
6
+ } | {
7
+ kind: 'edge';
8
+ source: string;
9
+ target: string;
10
+ };
11
+ level: DiagnosticLevel;
12
+ message: string;
13
+ }
14
+ /**
15
+ * Scan a flow draft for expression problems, resolved onto node / edge targets.
16
+ * Pure: no network — the trigger object's fields are not expanded (root-only
17
+ * scope), which is why the start node is excluded from the ref check.
18
+ */
19
+ export declare function flowExpressionProblems(draft: Record<string, unknown>): ExprProblem[];