@startsimpli/ui 0.4.14 → 0.4.16

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 (78) hide show
  1. package/README.md +457 -398
  2. package/package.json +18 -13
  3. package/src/components/__tests__/calendar-view-popup.test.tsx +42 -0
  4. package/src/components/__tests__/chat.test.tsx +129 -0
  5. package/src/components/__tests__/meetings-list.test.tsx +114 -0
  6. package/src/components/__tests__/slide-deck-viewer.test.tsx +82 -0
  7. package/src/components/__tests__/workspace.test.tsx +106 -0
  8. package/src/components/account/__tests__/account.test.tsx +5 -32
  9. package/src/components/account/change-password-form.tsx +1 -28
  10. package/src/components/calendar/calendar-view.tsx +31 -0
  11. package/src/components/calendar/index.ts +7 -0
  12. package/src/components/calendar/meetings-list.tsx +202 -0
  13. package/src/components/calendar/upcoming-meetings.tsx +5 -5
  14. package/src/components/chat/ChatComposer.tsx +113 -0
  15. package/src/components/chat/ChatMessage.tsx +81 -0
  16. package/src/components/chat/ChatThread.tsx +57 -0
  17. package/src/components/chat/index.ts +12 -0
  18. package/src/components/chat/types.ts +20 -0
  19. package/src/components/index.ts +13 -0
  20. package/src/components/slide-deck/SlideCanvas.tsx +68 -0
  21. package/src/components/slide-deck/SlideDeckViewer.tsx +144 -0
  22. package/src/components/slide-deck/SlideFilmstrip.tsx +73 -0
  23. package/src/components/slide-deck/index.ts +7 -0
  24. package/src/components/slide-deck/types.ts +18 -0
  25. package/src/components/team/DomainClaimCard.tsx +170 -0
  26. package/src/components/team/InviteMemberDialog.tsx +182 -0
  27. package/src/components/team/LeaveTeamDialog.tsx +130 -0
  28. package/src/components/team/MembersTable.tsx +138 -0
  29. package/src/components/team/OrgSwitcher.tsx +68 -0
  30. package/src/components/team/PendingInvitationCallout.tsx +106 -0
  31. package/src/components/team/RoleSelector.tsx +68 -0
  32. package/src/components/team/__tests__/team-components.test.tsx +352 -0
  33. package/src/components/team/__tests__/team-settings-page.test.tsx +146 -0
  34. package/src/components/team/domain-claim-card-default-class-names.ts +45 -0
  35. package/src/components/team/index.ts +62 -0
  36. package/src/components/team/invite-member-dialog-default-class-names.ts +41 -0
  37. package/src/components/team/leave-team-dialog-default-class-names.ts +33 -0
  38. package/src/components/team/members-table-default-class-names.ts +39 -0
  39. package/src/components/team/org-switcher-default-class-names.ts +13 -0
  40. package/src/components/team/pages/DomainsSettingsPage.tsx +289 -0
  41. package/src/components/team/pages/TeamSettingsPage.tsx +423 -0
  42. package/src/components/team/pages/domains-settings-page-default-class-names.ts +89 -0
  43. package/src/components/team/pages/index.ts +33 -0
  44. package/src/components/team/pages/team-settings-page-default-class-names.ts +116 -0
  45. package/src/components/team/pages/types.ts +135 -0
  46. package/src/components/team/pending-invitation-callout-default-class-names.ts +22 -0
  47. package/src/components/team/role-selector-default-class-names.ts +11 -0
  48. package/src/components/team/types.ts +97 -0
  49. package/src/components/workflows/ExecNodeDetails.tsx +83 -0
  50. package/src/components/workflows/ExecutionTimeline.tsx +146 -0
  51. package/src/components/workflows/NodeInspector.tsx +257 -0
  52. package/src/components/workflows/NodePalette.tsx +119 -0
  53. package/src/components/workflows/WorkflowCanvas.tsx +113 -0
  54. package/src/components/workflows/WorkflowEdge.tsx +65 -0
  55. package/src/components/workflows/WorkflowEditor.tsx +130 -0
  56. package/src/components/workflows/WorkflowNode.tsx +198 -0
  57. package/src/components/workflows/WorkflowRunViewer.tsx +81 -0
  58. package/src/components/workflows/__tests__/ExecutionTimeline.test.tsx +99 -0
  59. package/src/components/workflows/__tests__/NodeInspector.test.tsx +74 -0
  60. package/src/components/workflows/__tests__/NodePalette.test.tsx +46 -0
  61. package/src/components/workflows/__tests__/WorkflowCanvas.test.tsx +59 -0
  62. package/src/components/workflows/__tests__/WorkflowNode.test.tsx +92 -0
  63. package/src/components/workflows/__tests__/WorkflowRunViewer.test.tsx +138 -0
  64. package/src/components/workflows/__tests__/auto-layout.test.ts +107 -0
  65. package/src/components/workflows/__tests__/serialization.test.ts +278 -0
  66. package/src/components/workflows/exec-status.ts +90 -0
  67. package/src/components/workflows/hooks/useCanvasGraph.ts +70 -0
  68. package/src/components/workflows/hooks/useNodeStatusOverlay.ts +47 -0
  69. package/src/components/workflows/index.ts +78 -0
  70. package/src/components/workflows/layout/auto-layout.ts +142 -0
  71. package/src/components/workflows/node-icons.ts +31 -0
  72. package/src/components/workflows/serialization.ts +171 -0
  73. package/src/components/workflows/theme/categories.ts +96 -0
  74. package/src/components/workflows/types.ts +231 -0
  75. package/src/components/workflows/workflows.css +29 -0
  76. package/src/components/workspace/DualPaneWorkspace.tsx +187 -0
  77. package/src/components/workspace/SplitPane.tsx +174 -0
  78. package/src/components/workspace/index.ts +4 -0
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import type { FlowNode, FlowEdge } from '../serialization';
5
+ import type { WfExecutionView, NodeStatus } from '../types';
6
+ import type { WorkflowNodeData } from '../WorkflowNode';
7
+ import type { WorkflowEdgeData } from '../WorkflowEdge';
8
+
9
+ /**
10
+ * Merge an optional {@link WfExecutionView} status overlay onto flow nodes and
11
+ * edges. Each node's `data.status` is set from the matching node execution
12
+ * (defaulting to "idle"); each edge's `data.sourceStatus` is set from its
13
+ * source node so {@link WorkflowEdge} can animate live runs.
14
+ *
15
+ * Returns fresh node/edge arrays (referentially stable when inputs are) so
16
+ * xyflow re-renders only on actual overlay changes — suitable for polling.
17
+ */
18
+ export function useNodeStatusOverlay(
19
+ nodes: FlowNode[],
20
+ edges: FlowEdge[],
21
+ execution?: WfExecutionView
22
+ ): { nodes: FlowNode[]; edges: FlowEdge[] } {
23
+ return React.useMemo(() => {
24
+ const statusFor = (nodeId: string): NodeStatus =>
25
+ execution?.nodes?.[nodeId]?.status ?? 'idle';
26
+
27
+ const overlaidNodes: FlowNode[] = nodes.map((n) => {
28
+ const status = statusFor(n.id);
29
+ const data = n.data as WorkflowNodeData;
30
+ if (data.status === status) return n;
31
+ return { ...n, data: { ...data, status } };
32
+ });
33
+
34
+ const overlaidEdges: FlowEdge[] = edges.map((e) => {
35
+ const sourceStatus = statusFor(e.source);
36
+ const data = (e.data ?? { connectionType: 'main' }) as FlowEdge['data'] &
37
+ WorkflowEdgeData;
38
+ if (data.sourceStatus === sourceStatus) return e;
39
+ return {
40
+ ...e,
41
+ data: { ...data, connectionType: data.connectionType ?? 'main', sourceStatus },
42
+ };
43
+ });
44
+
45
+ return { nodes: overlaidNodes, edges: overlaidEdges };
46
+ }, [nodes, edges, execution]);
47
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @startsimpli/ui/workflows — generic workflow-graph foundation.
3
+ *
4
+ * Pure-TS foundation (types, serialization, layout) for the workflow
5
+ * editor/viewer. Isolated behind a dedicated subpath so consumers that do
6
+ * not need @xyflow/react don't pull it in (mirrors gantt / email-editor).
7
+ *
8
+ * Also exports the read-only execution-monitor components (timeline + run
9
+ * viewer), which are plain React + shared @startsimpli/ui primitives.
10
+ */
11
+
12
+ // Types
13
+ export type {
14
+ NodeStatus,
15
+ RunStatus,
16
+ WfNode,
17
+ WfPosition,
18
+ WfEdge,
19
+ NodeTypeDef,
20
+ WorkflowGraph,
21
+ BackendConnection,
22
+ BackendConnections,
23
+ BackendNode,
24
+ NodeExecView,
25
+ WfExecutionView,
26
+ JsonSchema,
27
+ } from './types';
28
+
29
+ // Serialization (graph <-> xyflow <-> backend connections)
30
+ export {
31
+ graphToFlow,
32
+ flowToGraph,
33
+ connectionsToEdges,
34
+ edgeId,
35
+ } from './serialization';
36
+ export type { FlowNode, FlowEdge } from './serialization';
37
+
38
+ // Layout
39
+ export { autoLayout, layoutLR, layoutTB } from './layout/auto-layout';
40
+ export type { LayoutDirection, AutoLayoutOptions } from './layout/auto-layout';
41
+
42
+ // Theme
43
+ export {
44
+ CATEGORY_TOKENS,
45
+ DEFAULT_CATEGORY_TOKEN,
46
+ getCategoryToken,
47
+ } from './theme/categories';
48
+ export type { CategoryToken } from './theme/categories';
49
+
50
+ // Execution monitor (read-only run visualization)
51
+ export { ExecutionTimeline } from './ExecutionTimeline';
52
+ export type { ExecutionTimelineProps } from './ExecutionTimeline';
53
+ export { WorkflowRunViewer } from './WorkflowRunViewer';
54
+ export type { WorkflowRunViewerProps } from './WorkflowRunViewer';
55
+ export { ExecNodeDetails } from './ExecNodeDetails';
56
+ export type { ExecNodeDetailsProps } from './ExecNodeDetails';
57
+
58
+ // Visual editor (interactive xyflow canvas + palette + inspector)
59
+ export { WorkflowNode } from './WorkflowNode';
60
+ export type { WorkflowNodeData } from './WorkflowNode';
61
+ export { WorkflowEdge } from './WorkflowEdge';
62
+ export type { WorkflowEdgeData } from './WorkflowEdge';
63
+ export { WorkflowCanvas } from './WorkflowCanvas';
64
+ export type { WorkflowCanvasProps } from './WorkflowCanvas';
65
+ export { NodePalette, NODE_DND_MIME } from './NodePalette';
66
+ export type { NodePaletteProps } from './NodePalette';
67
+ export { NodeInspector } from './NodeInspector';
68
+ export type { NodeInspectorProps } from './NodeInspector';
69
+ export { WorkflowEditor } from './WorkflowEditor';
70
+ export type { WorkflowEditorProps } from './WorkflowEditor';
71
+
72
+ // Editor hooks
73
+ export { useCanvasGraph } from './hooks/useCanvasGraph';
74
+ export type { UseCanvasGraphOptions } from './hooks/useCanvasGraph';
75
+ export { useNodeStatusOverlay } from './hooks/useNodeStatusOverlay';
76
+
77
+ // Category icon resolver (lucide)
78
+ export { getCategoryIcon } from './node-icons';
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Dagre-based auto-layout for workflow flow graphs.
3
+ *
4
+ * Wraps dagre with LR (left-to-right) and TB (top-to-bottom) presets,
5
+ * assigning non-overlapping positions to xyflow nodes. Nodes with explicit
6
+ * positions can be preserved (`preservePositions`), and edgeless / orphan
7
+ * nodes are laid out by dagre alongside connected ones (dagre places
8
+ * disconnected components without overlap).
9
+ *
10
+ * Pure-TS — no React. Operates on the structural FlowNode/FlowEdge shapes
11
+ * from serialization.ts.
12
+ */
13
+
14
+ import dagre from 'dagre';
15
+
16
+ import type { FlowNode, FlowEdge } from '../serialization';
17
+
18
+ export type LayoutDirection = 'LR' | 'TB';
19
+
20
+ export interface AutoLayoutOptions {
21
+ /** Layout direction. Defaults to 'TB'. */
22
+ direction?: LayoutDirection;
23
+ /** Default node width (px) for layout sizing. */
24
+ nodeWidth?: number;
25
+ /** Default node height (px) for layout sizing. */
26
+ nodeHeight?: number;
27
+ /** Horizontal spacing between ranks/nodes (px). */
28
+ rankSep?: number;
29
+ /** Spacing between nodes in the same rank (px). */
30
+ nodeSep?: number;
31
+ /**
32
+ * When true, nodes whose source data already has an explicit position
33
+ * keep it; only positionless nodes are laid out by dagre.
34
+ */
35
+ preservePositions?: boolean;
36
+ }
37
+
38
+ const DEFAULT_NODE_WIDTH = 180;
39
+ const DEFAULT_NODE_HEIGHT = 60;
40
+ const DEFAULT_RANK_SEP = 80;
41
+ const DEFAULT_NODE_SEP = 50;
42
+
43
+ /** Whether this flow node carries an explicit (user-set) position. */
44
+ function hasExplicitPosition(node: FlowNode): boolean {
45
+ return Boolean(node.data?.node?.position);
46
+ }
47
+
48
+ /**
49
+ * Compute non-overlapping positions for the given nodes/edges.
50
+ *
51
+ * Returns new node objects with updated `position`; edges are returned
52
+ * unchanged. The original arrays are not mutated.
53
+ */
54
+ export function autoLayout(
55
+ nodes: FlowNode[],
56
+ edges: FlowEdge[],
57
+ options: AutoLayoutOptions = {}
58
+ ): { nodes: FlowNode[]; edges: FlowEdge[] } {
59
+ const {
60
+ direction = 'TB',
61
+ nodeWidth = DEFAULT_NODE_WIDTH,
62
+ nodeHeight = DEFAULT_NODE_HEIGHT,
63
+ rankSep = DEFAULT_RANK_SEP,
64
+ nodeSep = DEFAULT_NODE_SEP,
65
+ preservePositions = false,
66
+ } = options;
67
+
68
+ if (nodes.length === 0) {
69
+ return { nodes: [], edges };
70
+ }
71
+
72
+ const g = new dagre.graphlib.Graph();
73
+ g.setGraph({
74
+ rankdir: direction,
75
+ ranksep: rankSep,
76
+ nodesep: nodeSep,
77
+ marginx: 20,
78
+ marginy: 20,
79
+ });
80
+ g.setDefaultEdgeLabel(() => ({}));
81
+
82
+ const pinned = new Set<string>();
83
+
84
+ for (const node of nodes) {
85
+ const w = node.width ?? nodeWidth;
86
+ const h = node.height ?? nodeHeight;
87
+ g.setNode(node.id, { width: w, height: h });
88
+ if (preservePositions && hasExplicitPosition(node)) {
89
+ pinned.add(node.id);
90
+ }
91
+ }
92
+
93
+ for (const edge of edges) {
94
+ // Only add edges whose endpoints are present (defensive).
95
+ if (g.hasNode(edge.source) && g.hasNode(edge.target)) {
96
+ g.setEdge(edge.source, edge.target);
97
+ }
98
+ }
99
+
100
+ dagre.layout(g);
101
+
102
+ const laidNodes: FlowNode[] = nodes.map((node) => {
103
+ if (pinned.has(node.id)) {
104
+ // Keep the explicit position verbatim.
105
+ return {
106
+ ...node,
107
+ position: { ...(node.data.node.position as { x: number; y: number }) },
108
+ };
109
+ }
110
+ const pos = g.node(node.id);
111
+ const w = node.width ?? nodeWidth;
112
+ const h = node.height ?? nodeHeight;
113
+ // dagre returns center coords; convert to top-left for xyflow.
114
+ return {
115
+ ...node,
116
+ position: {
117
+ x: (pos?.x ?? 0) - w / 2,
118
+ y: (pos?.y ?? 0) - h / 2,
119
+ },
120
+ };
121
+ });
122
+
123
+ return { nodes: laidNodes, edges };
124
+ }
125
+
126
+ /** Convenience: left-to-right layout. */
127
+ export function layoutLR(
128
+ nodes: FlowNode[],
129
+ edges: FlowEdge[],
130
+ options: Omit<AutoLayoutOptions, 'direction'> = {}
131
+ ) {
132
+ return autoLayout(nodes, edges, { ...options, direction: 'LR' });
133
+ }
134
+
135
+ /** Convenience: top-to-bottom layout. */
136
+ export function layoutTB(
137
+ nodes: FlowNode[],
138
+ edges: FlowEdge[],
139
+ options: Omit<AutoLayoutOptions, 'direction'> = {}
140
+ ) {
141
+ return autoLayout(nodes, edges, { ...options, direction: 'TB' });
142
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Map a category token's icon hint (a lucide-react icon name) to the actual
3
+ * lucide component. Kept separate from theme/categories.ts so that module
4
+ * stays pure-TS (no React/lucide import) while the editor can still resolve a
5
+ * real icon component. Unknown names fall back to `Box`.
6
+ */
7
+ import {
8
+ Box,
9
+ GitBranch,
10
+ Play,
11
+ Settings2,
12
+ Shuffle,
13
+ Workflow,
14
+ Zap,
15
+ type LucideIcon,
16
+ } from 'lucide-react';
17
+
18
+ const ICONS: Record<string, LucideIcon> = {
19
+ Box,
20
+ GitBranch,
21
+ Play,
22
+ Settings2,
23
+ Shuffle,
24
+ Workflow,
25
+ Zap,
26
+ };
27
+
28
+ export function getCategoryIcon(iconName: string | undefined): LucideIcon {
29
+ if (!iconName) return Box;
30
+ return ICONS[iconName] ?? Box;
31
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Workflow graph <-> xyflow conversion.
3
+ *
4
+ * `graphToFlow` turns a {@link WorkflowGraph} (flat nodes + edges) into the
5
+ * xyflow `{ nodes, edges }` shape an editor renders.
6
+ *
7
+ * `flowToGraph` turns xyflow nodes/edges back into `{ nodes, connections }`
8
+ * where `connections` is the LOCKED backend n8n shape:
9
+ *
10
+ * ```json
11
+ * { "sourceId": { "main": [[{ "node": "targetId", "type": "main", "index": 0 }]] } }
12
+ * ```
13
+ *
14
+ * Round-trip guarantee: `flowToGraph(graphToFlow(g)).connections` equals the
15
+ * direct serialization of `g`'s edges into that shape. The xyflow
16
+ * `sourceHandle` encodes the source output index and `targetHandle` encodes
17
+ * the target input index, so multi-output (if/else), parallel fan-out, and
18
+ * non-zero target inputs all survive the round-trip.
19
+ */
20
+
21
+ import type {
22
+ WorkflowGraph,
23
+ WfNode,
24
+ WfEdge,
25
+ BackendConnection,
26
+ BackendConnections,
27
+ } from './types';
28
+
29
+ // ── xyflow shapes (structurally typed; we only depend on the fields used) ──
30
+
31
+ /** Minimal xyflow Node shape produced/consumed here. */
32
+ export interface FlowNode {
33
+ id: string;
34
+ type?: string;
35
+ position: { x: number; y: number };
36
+ data: { node: WfNode } & Record<string, unknown>;
37
+ width?: number;
38
+ height?: number;
39
+ }
40
+
41
+ /** Minimal xyflow Edge shape produced/consumed here. */
42
+ export interface FlowEdge {
43
+ id: string;
44
+ source: string;
45
+ target: string;
46
+ sourceHandle?: string | null;
47
+ targetHandle?: string | null;
48
+ type?: string;
49
+ data?: { connectionType: string } & Record<string, unknown>;
50
+ }
51
+
52
+ const DEFAULT_CONNECTION_TYPE = 'main';
53
+
54
+ /** Stable edge id encoding source/output -> target/input. */
55
+ export function edgeId(
56
+ source: string,
57
+ sourceOutput: number,
58
+ target: string,
59
+ targetInput: number
60
+ ): string {
61
+ return `${source}:${sourceOutput}->${target}:${targetInput}`;
62
+ }
63
+
64
+ /**
65
+ * Convert a {@link WorkflowGraph} into xyflow `{ nodes, edges }`.
66
+ *
67
+ * Node positions: explicit `node.position` is preserved; otherwise a
68
+ * placeholder `{ x: 0, y: 0 }` is assigned (run auto-layout for real
69
+ * coordinates).
70
+ */
71
+ export function graphToFlow(graph: WorkflowGraph): {
72
+ nodes: FlowNode[];
73
+ edges: FlowEdge[];
74
+ } {
75
+ const nodes: FlowNode[] = graph.nodes.map((node) => ({
76
+ id: node.id,
77
+ type: node.type,
78
+ position: node.position ?? { x: 0, y: 0 },
79
+ data: { node },
80
+ }));
81
+
82
+ const edges: FlowEdge[] = graph.edges.map((edge) => {
83
+ const sourceOutput = edge.sourceOutput ?? 0;
84
+ const targetInput = edge.targetInput ?? 0;
85
+ const connectionType = edge.type ?? DEFAULT_CONNECTION_TYPE;
86
+ return {
87
+ id: edgeId(edge.source, sourceOutput, edge.target, targetInput),
88
+ source: edge.source,
89
+ target: edge.target,
90
+ sourceHandle: String(sourceOutput),
91
+ targetHandle: String(targetInput),
92
+ type: 'default',
93
+ data: { connectionType },
94
+ };
95
+ });
96
+
97
+ return { nodes, edges };
98
+ }
99
+
100
+ /** Parse a handle id back to its numeric index (null/undefined -> 0). */
101
+ function handleIndex(handle: string | null | undefined): number {
102
+ if (handle === null || handle === undefined || handle === '') return 0;
103
+ const n = Number.parseInt(handle, 10);
104
+ return Number.isNaN(n) ? 0 : n;
105
+ }
106
+
107
+ /**
108
+ * Convert xyflow nodes/edges back into `{ nodes, connections }`.
109
+ *
110
+ * `connections` is the LOCKED backend n8n shape. The `main` (or other
111
+ * connection type) array is positional by source output index — earlier
112
+ * unconnected outputs are back-filled with empty arrays so a connection on
113
+ * output index 1 yields `[[], [...]]`.
114
+ */
115
+ export function flowToGraph(
116
+ nodes: FlowNode[],
117
+ edges: FlowEdge[]
118
+ ): { nodes: WfNode[]; connections: BackendConnections } {
119
+ const outNodes: WfNode[] = nodes.map((n) => n.data.node);
120
+
121
+ const connections: BackendConnections = {};
122
+
123
+ for (const edge of edges) {
124
+ const connectionType =
125
+ edge.data?.connectionType ?? edge.type ?? DEFAULT_CONNECTION_TYPE;
126
+ const sourceOutput = handleIndex(edge.sourceHandle);
127
+ const targetInput = handleIndex(edge.targetHandle);
128
+
129
+ const sourceMap = (connections[edge.source] ??= {});
130
+ const outputLists = (sourceMap[connectionType] ??= []);
131
+
132
+ // Back-fill positional output slots up to sourceOutput.
133
+ while (outputLists.length <= sourceOutput) {
134
+ outputLists.push([]);
135
+ }
136
+
137
+ const conn: BackendConnection = {
138
+ node: edge.target,
139
+ type: connectionType,
140
+ index: targetInput,
141
+ };
142
+ outputLists[sourceOutput].push(conn);
143
+ }
144
+
145
+ return { nodes: outNodes, connections };
146
+ }
147
+
148
+ /**
149
+ * Hydrate flat {@link WfEdge}s from the LOCKED backend connections shape.
150
+ * Inverse of the connections half of {@link flowToGraph}; useful when
151
+ * loading a workflow from the API into editor state.
152
+ */
153
+ export function connectionsToEdges(connections: BackendConnections): WfEdge[] {
154
+ const edges: WfEdge[] = [];
155
+ for (const [source, outputs] of Object.entries(connections)) {
156
+ for (const [type, outputLists] of Object.entries(outputs)) {
157
+ outputLists.forEach((conns, sourceOutput) => {
158
+ for (const conn of conns) {
159
+ edges.push({
160
+ source,
161
+ target: conn.node,
162
+ sourceOutput,
163
+ targetInput: conn.index,
164
+ type,
165
+ });
166
+ }
167
+ });
168
+ }
169
+ }
170
+ return edges;
171
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Generic category → visual token map.
3
+ *
4
+ * Maps a node-type category (free string, e.g. the backend
5
+ * NodeType.Category choices) to a small token bundle the renderer can use.
6
+ * Tokens are framework-agnostic (CSS custom-property friendly hex + a
7
+ * Tailwind-ish class hint) so this stays in pure-TS land — no React here.
8
+ *
9
+ * Categories mirror the backend NodeType.Category text choices but the
10
+ * map is open: unknown categories resolve to DEFAULT_CATEGORY_TOKEN.
11
+ */
12
+
13
+ export interface CategoryToken {
14
+ /** Stable category key. */
15
+ category: string;
16
+ /** Human label. */
17
+ label: string;
18
+ /** Accent color (hex) for borders/handles. */
19
+ color: string;
20
+ /** Background tint (hex). */
21
+ background: string;
22
+ /** Foreground/text color (hex). */
23
+ foreground: string;
24
+ /** Icon hint (lucide-react icon name) — consumed by a later React epic. */
25
+ icon: string;
26
+ }
27
+
28
+ /** Fallback token for unknown categories. */
29
+ export const DEFAULT_CATEGORY_TOKEN: CategoryToken = {
30
+ category: 'default',
31
+ label: 'Node',
32
+ color: '#64748b', // slate-500
33
+ background: '#f1f5f9', // slate-100
34
+ foreground: '#0f172a', // slate-900
35
+ icon: 'Box',
36
+ };
37
+
38
+ /**
39
+ * Built-in category tokens. Keys match backend NodeType.Category values.
40
+ */
41
+ export const CATEGORY_TOKENS: Record<string, CategoryToken> = {
42
+ trigger: {
43
+ category: 'trigger',
44
+ label: 'Trigger',
45
+ color: '#16a34a', // green-600
46
+ background: '#dcfce7', // green-100
47
+ foreground: '#14532d', // green-900
48
+ icon: 'Zap',
49
+ },
50
+ action: {
51
+ category: 'action',
52
+ label: 'Action',
53
+ color: '#2563eb', // blue-600
54
+ background: '#dbeafe', // blue-100
55
+ foreground: '#1e3a8a', // blue-900
56
+ icon: 'Play',
57
+ },
58
+ condition: {
59
+ category: 'condition',
60
+ label: 'Condition',
61
+ color: '#d97706', // amber-600
62
+ background: '#fef3c7', // amber-100
63
+ foreground: '#78350f', // amber-900
64
+ icon: 'GitBranch',
65
+ },
66
+ transform: {
67
+ category: 'transform',
68
+ label: 'Transform',
69
+ color: '#7c3aed', // violet-600
70
+ background: '#ede9fe', // violet-100
71
+ foreground: '#4c1d95', // violet-900
72
+ icon: 'Shuffle',
73
+ },
74
+ subworkflow: {
75
+ category: 'subworkflow',
76
+ label: 'Sub-workflow',
77
+ color: '#0891b2', // cyan-600
78
+ background: '#cffafe', // cyan-100
79
+ foreground: '#164e63', // cyan-900
80
+ icon: 'Workflow',
81
+ },
82
+ control: {
83
+ category: 'control',
84
+ label: 'Control',
85
+ color: '#db2777', // pink-600
86
+ background: '#fce7f3', // pink-100
87
+ foreground: '#831843', // pink-900
88
+ icon: 'Settings2',
89
+ },
90
+ };
91
+
92
+ /** Resolve a category token, falling back to the default. */
93
+ export function getCategoryToken(category: string | undefined): CategoryToken {
94
+ if (!category) return DEFAULT_CATEGORY_TOKEN;
95
+ return CATEGORY_TOKENS[category] ?? DEFAULT_CATEGORY_TOKEN;
96
+ }