@echothink-ui/workflow 0.1.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.
@@ -0,0 +1,33 @@
1
+ import * as React from "react";
2
+ import { StatusDot } from "@echothink-ui/core";
3
+ import type { PipelineStageProps } from "./types";
4
+
5
+ export function PipelineStage({ stage, active, onSelect, className, ...props }: PipelineStageProps) {
6
+ const content = (
7
+ <>
8
+ <StatusDot status={stage.status} label={stage.status} />
9
+ <strong>{stage.label}</strong>
10
+ {stage.durationMs !== undefined ? <small>{stage.durationMs}ms</small> : null}
11
+ </>
12
+ );
13
+
14
+ return onSelect ? (
15
+ <button
16
+ {...props}
17
+ type="button"
18
+ className={`eth-workflow-pipeline-stage ${active ? "eth-workflow-pipeline-stage--active" : ""} ${className ?? ""}`}
19
+ data-eth-component="PipelineStage"
20
+ onClick={() => onSelect(stage.id)}
21
+ >
22
+ {content}
23
+ </button>
24
+ ) : (
25
+ <article
26
+ {...props}
27
+ className={`eth-workflow-pipeline-stage ${active ? "eth-workflow-pipeline-stage--active" : ""} ${className ?? ""}`}
28
+ data-eth-component="PipelineStage"
29
+ >
30
+ {content}
31
+ </article>
32
+ );
33
+ }
@@ -0,0 +1,125 @@
1
+ import * as React from "react";
2
+ import { Button } from "@echothink-ui/core";
3
+ import type { ProcessDesignerProps, ProcessNode } from "./types";
4
+
5
+ export function ProcessDesigner({
6
+ nodes,
7
+ edges,
8
+ onNodeMove,
9
+ onAddNode,
10
+ onConnect,
11
+ className,
12
+ ...props
13
+ }: ProcessDesignerProps) {
14
+ const svgRef = React.useRef<SVGSVGElement>(null);
15
+ const [positions, setPositions] = React.useState(() => initialPositions(nodes));
16
+ const [draggingId, setDraggingId] = React.useState<string | null>(null);
17
+ const [selectedNodeId, setSelectedNodeId] = React.useState<string | null>(null);
18
+
19
+ React.useEffect(() => {
20
+ setPositions(initialPositions(nodes));
21
+ }, [nodes]);
22
+
23
+ const pointFromEvent = (event: React.MouseEvent<SVGSVGElement>) => {
24
+ const rect = svgRef.current?.getBoundingClientRect();
25
+ if (!rect) return { x: 0, y: 0 };
26
+ return {
27
+ x: ((event.clientX - rect.left) / rect.width) * 900,
28
+ y: ((event.clientY - rect.top) / rect.height) * 420
29
+ };
30
+ };
31
+
32
+ const move = (event: React.MouseEvent<SVGSVGElement>) => {
33
+ if (!draggingId) return;
34
+ const point = pointFromEvent(event);
35
+ setPositions((current) => ({ ...current, [draggingId]: point }));
36
+ };
37
+
38
+ const stop = () => {
39
+ if (!draggingId) return;
40
+ const point = positions[draggingId];
41
+ if (point) onNodeMove?.(draggingId, point.x, point.y);
42
+ setDraggingId(null);
43
+ };
44
+
45
+ const clickNode = (id: string) => {
46
+ if (selectedNodeId && selectedNodeId !== id) {
47
+ onConnect?.(selectedNodeId, id);
48
+ setSelectedNodeId(null);
49
+ } else {
50
+ setSelectedNodeId(id);
51
+ }
52
+ };
53
+
54
+ return (
55
+ <section
56
+ {...props}
57
+ className={`eth-workflow-process-designer ${className ?? ""}`}
58
+ data-eth-component="ProcessDesigner"
59
+ >
60
+ <header>
61
+ <h3>Process designer</h3>
62
+ {onAddNode ? <Button intent="secondary" density="compact" onClick={onAddNode}>Add node</Button> : null}
63
+ </header>
64
+ <svg
65
+ ref={svgRef}
66
+ viewBox="0 0 900 420"
67
+ role="img"
68
+ aria-label="Process graph"
69
+ onMouseMove={move}
70
+ onMouseUp={stop}
71
+ onMouseLeave={stop}
72
+ >
73
+ {edges.map((edge, index) => {
74
+ const from = positions[edge.from];
75
+ const to = positions[edge.to];
76
+ if (!from || !to) return null;
77
+ const midX = (from.x + to.x) / 2;
78
+ const midY = (from.y + to.y) / 2;
79
+ return (
80
+ <g key={`${edge.from}-${edge.to}-${index}`}>
81
+ <line x1={from.x} y1={from.y} x2={to.x} y2={to.y} stroke="var(--eth-color-border-strong)" strokeWidth={2} markerEnd="url(#eth-workflow-arrow)" />
82
+ {edge.label ? <text x={midX} y={midY - 6} textAnchor="middle" fontSize={12}>{edge.label}</text> : null}
83
+ </g>
84
+ );
85
+ })}
86
+ <defs>
87
+ <marker id="eth-workflow-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
88
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="var(--eth-color-border-strong)" />
89
+ </marker>
90
+ </defs>
91
+ {nodes.map((node) => {
92
+ const point = positions[node.id];
93
+ if (!point) return null;
94
+ return (
95
+ <g
96
+ key={node.id}
97
+ transform={`translate(${point.x}, ${point.y})`}
98
+ className={`eth-workflow-process-designer__node eth-workflow-process-designer__node--${node.type} ${selectedNodeId === node.id ? "eth-workflow-process-designer__node--selected" : ""}`}
99
+ onMouseDown={(event) => { event.preventDefault(); setDraggingId(node.id); }}
100
+ onClick={() => clickNode(node.id)}
101
+ >
102
+ {node.type === "decision" ? (
103
+ <polygon points="0,-32 54,0 0,32 -54,0" fill="var(--eth-color-layer-01)" stroke="var(--eth-color-border-strong)" strokeWidth={2} />
104
+ ) : (
105
+ <rect x={-62} y={-28} width={124} height={56} rx={0} fill="var(--eth-color-layer-01)" stroke="var(--eth-color-border-strong)" strokeWidth={2} />
106
+ )}
107
+ <text textAnchor="middle" dominantBaseline="middle" fontSize={13}>{node.label}</text>
108
+ </g>
109
+ );
110
+ })}
111
+ </svg>
112
+ </section>
113
+ );
114
+ }
115
+
116
+ function initialPositions(nodes: ProcessNode[]) {
117
+ const positions: Record<string, { x: number; y: number }> = {};
118
+ nodes.forEach((node, index) => {
119
+ positions[node.id] = {
120
+ x: node.x ?? 100 + (index % 5) * 170,
121
+ y: node.y ?? 90 + Math.floor(index / 5) * 120
122
+ };
123
+ });
124
+ return positions;
125
+ }
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ import { StatusDot } from "@echothink-ui/core";
3
+ import type { ProcessTimelineProps } from "./types";
4
+
5
+ export function ProcessTimeline({ events = [], className, ...props }: ProcessTimelineProps) {
6
+ return (
7
+ <section
8
+ {...props}
9
+ className={`eth-workflow-process-timeline ${className ?? ""}`}
10
+ data-eth-component="ProcessTimeline"
11
+ >
12
+ <ol>
13
+ {events.map((event) => (
14
+ <li key={event.id}>
15
+ {event.status ? <StatusDot status={event.status} /> : null}
16
+ <span>{event.label}</span>
17
+ {event.timestamp ? <time dateTime={event.timestamp}>{event.timestamp}</time> : null}
18
+ </li>
19
+ ))}
20
+ </ol>
21
+ </section>
22
+ );
23
+ }
@@ -0,0 +1,66 @@
1
+ import * as React from "react";
2
+ import { Button, FormField, Select } from "@echothink-ui/core";
3
+ import { ConditionBuilder } from "./ConditionBuilder";
4
+ import type { RuleBuilderProps, WorkflowRule } from "./types";
5
+
6
+ const defaultRule: WorkflowRule = {
7
+ combinator: "all",
8
+ conditions: [{ field: "", operator: "equals", value: "" }]
9
+ };
10
+
11
+ export function RuleBuilder({ value, onChange, variables = [], className, ...props }: RuleBuilderProps) {
12
+ const [internalRule, setInternalRule] = React.useState<WorkflowRule>(value ?? defaultRule);
13
+ const rule = value ?? internalRule;
14
+ const commit = (nextRule: WorkflowRule) => {
15
+ setInternalRule(nextRule);
16
+ onChange?.(nextRule);
17
+ };
18
+
19
+ return (
20
+ <section
21
+ {...props}
22
+ className={`eth-workflow-rule-builder ${className ?? ""}`}
23
+ data-eth-component="RuleBuilder"
24
+ >
25
+ <FormField label="Match">
26
+ <Select
27
+ value={rule.combinator}
28
+ options={[
29
+ { value: "all", label: "All conditions" },
30
+ { value: "any", label: "Any condition" }
31
+ ]}
32
+ onChange={(event) => commit({ ...rule, combinator: event.currentTarget.value as WorkflowRule["combinator"] })}
33
+ />
34
+ </FormField>
35
+ <div className="eth-workflow-rule-builder__conditions">
36
+ {rule.conditions.map((condition, index) => (
37
+ <div key={index} className="eth-workflow-rule-builder__condition">
38
+ <ConditionBuilder
39
+ value={condition}
40
+ variables={variables}
41
+ onChange={(nextCondition) => {
42
+ const conditions = [...rule.conditions];
43
+ conditions[index] = nextCondition;
44
+ commit({ ...rule, conditions });
45
+ }}
46
+ />
47
+ <Button
48
+ intent="ghost"
49
+ density="compact"
50
+ onClick={() => commit({ ...rule, conditions: rule.conditions.filter((_, itemIndex) => itemIndex !== index) })}
51
+ >
52
+ Remove
53
+ </Button>
54
+ </div>
55
+ ))}
56
+ </div>
57
+ <Button
58
+ intent="secondary"
59
+ density="compact"
60
+ onClick={() => commit({ ...rule, conditions: [...rule.conditions, { field: variables[0] ?? "", operator: "equals", value: "" }] })}
61
+ >
62
+ Add condition
63
+ </Button>
64
+ </section>
65
+ );
66
+ }
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+ import { Button } from "@echothink-ui/core";
3
+ import { DataTable, type DataColumn } from "@echothink-ui/data";
4
+ import type { RuleSimulationPanelProps, RuleSimulationSample } from "./types";
5
+
6
+ type SimulationRow = RuleSimulationSample & {
7
+ actualOutcome?: unknown;
8
+ matched?: boolean;
9
+ };
10
+
11
+ export function RuleSimulationPanel({
12
+ rule,
13
+ sampleInputs,
14
+ results = [],
15
+ onRun,
16
+ className,
17
+ ...props
18
+ }: RuleSimulationPanelProps) {
19
+ const resultMap = new Map(results.map((result) => [result.sampleId, result]));
20
+ const rows: SimulationRow[] = sampleInputs.map((sample) => ({
21
+ ...sample,
22
+ actualOutcome: resultMap.get(sample.id)?.actualOutcome,
23
+ matched: resultMap.get(sample.id)?.matched
24
+ }));
25
+ const columns: DataColumn<SimulationRow>[] = [
26
+ { key: "label", header: "Sample" },
27
+ { key: "data", header: "Input", render: (row) => <code>{JSON.stringify(row.data)}</code> },
28
+ { key: "expectedOutcome", header: "Expected", render: (row) => <code>{JSON.stringify(row.expectedOutcome)}</code> },
29
+ { key: "actualOutcome", header: "Actual", render: (row) => <code>{JSON.stringify(row.actualOutcome)}</code> },
30
+ { key: "matched", header: "Matched", render: (row) => (row.matched ? "Yes" : row.matched === false ? "No" : "Not run") }
31
+ ];
32
+
33
+ return (
34
+ <section
35
+ {...props}
36
+ className={`eth-workflow-rule-simulation ${className ?? ""}`}
37
+ data-eth-component="RuleSimulationPanel"
38
+ >
39
+ <header>
40
+ <h3>Rule simulation</h3>
41
+ {onRun ? <Button onClick={onRun}>Run</Button> : null}
42
+ </header>
43
+ <pre className="eth-workflow-rule-simulation__rule"><code>{JSON.stringify(rule, null, 2)}</code></pre>
44
+ <DataTable rows={rows} columns={columns} rowKey="id" />
45
+ </section>
46
+ );
47
+ }
@@ -0,0 +1,41 @@
1
+ import * as React from "react";
2
+ import { Button, FormField, Select, StatusDot, Textarea } from "@echothink-ui/core";
3
+ import type { WorkflowHandoffPanelProps } from "./types";
4
+
5
+ export function WorkflowHandoffPanel({
6
+ item,
7
+ assigneeOptions = [],
8
+ onSubmit,
9
+ className,
10
+ ...props
11
+ }: WorkflowHandoffPanelProps) {
12
+ const [assigneeId, setAssigneeId] = React.useState(assigneeOptions[0]?.id ?? "");
13
+ const [reason, setReason] = React.useState("");
14
+
15
+ return (
16
+ <section
17
+ {...props}
18
+ className={`eth-workflow-handoff-panel ${className ?? ""}`}
19
+ data-eth-component="WorkflowHandoffPanel"
20
+ >
21
+ <header>
22
+ <h3>Workflow handoff</h3>
23
+ {item?.status ? <StatusDot status={item.status} label={item.status} /> : null}
24
+ </header>
25
+ {item ? <p>{item.label}</p> : null}
26
+ <form onSubmit={(event) => { event.preventDefault(); onSubmit?.({ assigneeId, reason }); }}>
27
+ <FormField label="Assignee">
28
+ <Select
29
+ value={assigneeId}
30
+ options={assigneeOptions.map((option) => ({ value: option.id, label: option.kind ? `${option.label} (${option.kind})` : option.label }))}
31
+ onChange={(event) => setAssigneeId(event.currentTarget.value)}
32
+ />
33
+ </FormField>
34
+ <FormField label="Reason">
35
+ <Textarea value={reason} rows={4} onChange={(event) => setReason(event.currentTarget.value)} />
36
+ </FormField>
37
+ <Button type="submit" disabled={!assigneeId}>Handoff</Button>
38
+ </form>
39
+ </section>
40
+ );
41
+ }
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ import { PipelineStage } from "./PipelineStage";
3
+ import type { WorkflowPipelineViewProps } from "./types";
4
+
5
+ export function WorkflowPipelineView({
6
+ stages,
7
+ activeStageId,
8
+ onSelect,
9
+ className,
10
+ ...props
11
+ }: WorkflowPipelineViewProps) {
12
+ return (
13
+ <section
14
+ {...props}
15
+ className={`eth-workflow-pipeline-view ${className ?? ""}`}
16
+ data-eth-component="WorkflowPipelineView"
17
+ aria-label="Workflow pipeline"
18
+ >
19
+ <ol className="eth-workflow-pipeline-view__stages">
20
+ {stages.map((stage, index) => (
21
+ <li key={stage.id}>
22
+ <PipelineStage
23
+ stage={stage}
24
+ active={stage.id === activeStageId}
25
+ onSelect={onSelect}
26
+ aria-posinset={index + 1}
27
+ aria-setsize={stages.length}
28
+ />
29
+ </li>
30
+ ))}
31
+ </ol>
32
+ </section>
33
+ );
34
+ }
@@ -0,0 +1,107 @@
1
+ import type * as React from "react";
2
+ import type { EthOperationalStatus } from "@echothink-ui/core";
3
+
4
+ export interface WorkflowStage extends Record<string, unknown> {
5
+ id: string;
6
+ label: string;
7
+ status: EthOperationalStatus;
8
+ startedAt?: string;
9
+ durationMs?: number;
10
+ }
11
+
12
+ export interface WorkflowPipelineViewProps
13
+ extends Omit<React.HTMLAttributes<HTMLElement>, "onSelect"> {
14
+ stages: WorkflowStage[];
15
+ activeStageId?: string;
16
+ onSelect?: (id: string) => void;
17
+ }
18
+
19
+ export interface PipelineStageProps extends Omit<React.HTMLAttributes<HTMLElement>, "onSelect"> {
20
+ stage: WorkflowStage;
21
+ active?: boolean;
22
+ onSelect?: (id: string) => void;
23
+ }
24
+
25
+ export interface ProcessNode extends Record<string, unknown> {
26
+ id: string;
27
+ label: string;
28
+ type: "task" | "decision" | "start" | "end";
29
+ x?: number;
30
+ y?: number;
31
+ }
32
+
33
+ export interface ProcessEdge extends Record<string, unknown> {
34
+ from: string;
35
+ to: string;
36
+ label?: string;
37
+ }
38
+
39
+ export interface ProcessDesignerProps
40
+ extends Omit<React.HTMLAttributes<HTMLElement>, "onSelect"> {
41
+ nodes: ProcessNode[];
42
+ edges: ProcessEdge[];
43
+ onNodeMove?: (id: string, x: number, y: number) => void;
44
+ onAddNode?: () => void;
45
+ onConnect?: (from: string, to: string) => void;
46
+ }
47
+
48
+ export interface WorkflowRuleCondition extends Record<string, unknown> {
49
+ field: string;
50
+ operator: "equals" | "not-equals" | "contains" | "gt" | "gte" | "lt" | "lte" | "exists";
51
+ value?: string;
52
+ }
53
+
54
+ export interface WorkflowRule extends Record<string, unknown> {
55
+ combinator: "all" | "any";
56
+ conditions: WorkflowRuleCondition[];
57
+ }
58
+
59
+ export interface RuleBuilderProps
60
+ extends Omit<React.HTMLAttributes<HTMLElement>, "onChange"> {
61
+ value?: WorkflowRule;
62
+ onChange?: (rule: WorkflowRule) => void;
63
+ variables?: string[];
64
+ }
65
+
66
+ export interface ConditionBuilderProps
67
+ extends Omit<React.HTMLAttributes<HTMLElement>, "onChange"> {
68
+ value: WorkflowRuleCondition;
69
+ onChange: (condition: WorkflowRuleCondition) => void;
70
+ variables?: string[];
71
+ }
72
+
73
+ export interface RuleSimulationSample extends Record<string, unknown> {
74
+ id: string;
75
+ label: string;
76
+ data: unknown;
77
+ expectedOutcome?: unknown;
78
+ }
79
+
80
+ export interface RuleSimulationResult extends Record<string, unknown> {
81
+ sampleId: string;
82
+ actualOutcome: unknown;
83
+ matched: boolean;
84
+ }
85
+
86
+ export interface RuleSimulationPanelProps extends Omit<React.HTMLAttributes<HTMLElement>, "results"> {
87
+ rule: unknown;
88
+ sampleInputs: RuleSimulationSample[];
89
+ results?: RuleSimulationResult[];
90
+ onRun?: () => void;
91
+ }
92
+
93
+ export interface WorkflowHandoffPanelProps extends Omit<React.HTMLAttributes<HTMLElement>, "onSubmit"> {
94
+ item?: { id: string; label: React.ReactNode; status?: EthOperationalStatus };
95
+ assigneeOptions?: Array<{ id: string; label: string; kind?: "user" | "team" | "agent" }>;
96
+ onSubmit?: (handoff: { assigneeId: string; reason: string }) => void;
97
+ }
98
+
99
+ export interface ApprovalWorkflowEditorProps
100
+ extends Omit<React.HTMLAttributes<HTMLElement>, "onChange"> {
101
+ steps?: Array<{ id: string; label: string; approver?: string; required?: boolean }>;
102
+ onChange?: (steps: Array<{ id: string; label: string; approver?: string; required?: boolean }>) => void;
103
+ }
104
+
105
+ export interface ProcessTimelineProps extends React.HTMLAttributes<HTMLElement> {
106
+ events?: Array<{ id: string; label: React.ReactNode; timestamp?: string; status?: EthOperationalStatus }>;
107
+ }
package/src/css.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module "*.css";
package/src/index.tsx ADDED
@@ -0,0 +1,42 @@
1
+ import "./styles.css";
2
+
3
+ export type {
4
+ ApprovalWorkflowEditorProps,
5
+ ConditionBuilderProps,
6
+ PipelineStageProps,
7
+ ProcessDesignerProps,
8
+ ProcessEdge,
9
+ ProcessNode,
10
+ ProcessTimelineProps,
11
+ RuleBuilderProps,
12
+ RuleSimulationPanelProps,
13
+ RuleSimulationResult,
14
+ RuleSimulationSample,
15
+ WorkflowHandoffPanelProps,
16
+ WorkflowPipelineViewProps,
17
+ WorkflowRule,
18
+ WorkflowRuleCondition,
19
+ WorkflowStage
20
+ } from "./components/types";
21
+ export { ApprovalWorkflowEditor } from "./components/ApprovalWorkflowEditor";
22
+ export { ConditionBuilder } from "./components/ConditionBuilder";
23
+ export { PipelineStage } from "./components/PipelineStage";
24
+ export { ProcessDesigner } from "./components/ProcessDesigner";
25
+ export { ProcessTimeline } from "./components/ProcessTimeline";
26
+ export { RuleBuilder } from "./components/RuleBuilder";
27
+ export { RuleSimulationPanel } from "./components/RuleSimulationPanel";
28
+ export { WorkflowHandoffPanel } from "./components/WorkflowHandoffPanel";
29
+ export { WorkflowPipelineView } from "./components/WorkflowPipelineView";
30
+
31
+ export const WorkflowComponentNames = [
32
+ "WorkflowPipelineView",
33
+ "PipelineStage",
34
+ "ProcessDesigner",
35
+ "RuleBuilder",
36
+ "ConditionBuilder",
37
+ "RuleSimulationPanel",
38
+ "WorkflowHandoffPanel",
39
+ "ApprovalWorkflowEditor",
40
+ "ProcessTimeline"
41
+ ] as const;
42
+ export type WorkflowComponentName = (typeof WorkflowComponentNames)[number];