@umodeapporg/ui 0.23.1 → 0.24.1
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.
- package/dist/assets/umode-ui.css +100 -0
- package/dist/components/UI/Flow/FlowBlockNode.d.ts +35 -0
- package/dist/components/UI/Flow/FlowBlockNode.js +84 -0
- package/dist/components/UI/Flow/FlowBlockNode.js.map +1 -0
- package/dist/components/UI/Flow/FlowCanvas.d.ts +21 -0
- package/dist/components/UI/Flow/FlowCanvas.js +252 -0
- package/dist/components/UI/Flow/FlowCanvas.js.map +1 -0
- package/dist/components/UI/Flow/FlowConnectionEdge.d.ts +18 -0
- package/dist/components/UI/Flow/FlowConnectionEdge.js +115 -0
- package/dist/components/UI/Flow/FlowConnectionEdge.js.map +1 -0
- package/dist/components/UI/Flow/FlowEditorProvider.d.ts +17 -0
- package/dist/components/UI/Flow/FlowEditorProvider.js +27 -0
- package/dist/components/UI/Flow/FlowEditorProvider.js.map +1 -0
- package/dist/components/UI/Flow/FlowInspector.d.ts +11 -0
- package/dist/components/UI/Flow/FlowInspector.js +127 -0
- package/dist/components/UI/Flow/FlowInspector.js.map +1 -0
- package/dist/components/UI/Flow/FlowIssueList.d.ts +8 -0
- package/dist/components/UI/Flow/FlowIssueList.js +55 -0
- package/dist/components/UI/Flow/FlowIssueList.js.map +1 -0
- package/dist/components/UI/Flow/FlowLaneMap.d.ts +30 -0
- package/dist/components/UI/Flow/FlowLaneMap.js +206 -0
- package/dist/components/UI/Flow/FlowLaneMap.js.map +1 -0
- package/dist/components/UI/Flow/FlowNodeShell.d.ts +20 -0
- package/dist/components/UI/Flow/FlowNodeShell.js +81 -0
- package/dist/components/UI/Flow/FlowNodeShell.js.map +1 -0
- package/dist/components/UI/Flow/FlowResizablePanel.d.ts +20 -0
- package/dist/components/UI/Flow/FlowResizablePanel.js +117 -0
- package/dist/components/UI/Flow/FlowResizablePanel.js.map +1 -0
- package/dist/components/UI/Flow/FlowZoomControls.d.ts +12 -0
- package/dist/components/UI/Flow/FlowZoomControls.js +53 -0
- package/dist/components/UI/Flow/FlowZoomControls.js.map +1 -0
- package/dist/components/UI/Flow/flowBlockRegistry.d.ts +12 -0
- package/dist/components/UI/Flow/flowBlockRegistry.js +20 -0
- package/dist/components/UI/Flow/flowBlockRegistry.js.map +1 -0
- package/dist/components/UI/Flow/flowEditorStore.d.ts +49 -0
- package/dist/components/UI/Flow/flowEditorStore.js +302 -0
- package/dist/components/UI/Flow/flowEditorStore.js.map +1 -0
- package/dist/components/UI/Flow/flowGraph.d.ts +32 -0
- package/dist/components/UI/Flow/flowGraph.js +157 -0
- package/dist/components/UI/Flow/flowGraph.js.map +1 -0
- package/dist/components/UI/Flow/flowTypes.d.ts +74 -0
- package/dist/components/UI/Flow/flowTypes.js +2 -0
- package/dist/components/UI/Flow/flowTypes.js.map +1 -0
- package/dist/components/UI/Flow/index.d.ts +12 -0
- package/dist/components/UI/Flow/index.js +40 -0
- package/dist/components/UI/Flow/index.js.map +1 -0
- package/dist/components/UI/Flow/useFlowLabels.d.ts +10 -0
- package/dist/components/UI/Flow/useFlowLabels.js +51 -0
- package/dist/components/UI/Flow/useFlowLabels.js.map +1 -0
- package/dist/locales/en/ui.json.d.ts +64 -0
- package/dist/locales/en/ui.json.js +3 -0
- package/dist/locales/en/ui.json.js.map +1 -1
- package/dist/locales/index.d.ts +128 -0
- package/dist/locales/pt/ui.json.d.ts +64 -0
- package/dist/locales/pt/ui.json.js +3 -0
- package/dist/locales/pt/ui.json.js.map +1 -1
- package/package.json +10 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { twMerge } from "tailwind-merge";
|
|
6
|
+
import ControlledButton from "../../Form/ControlledButton.js";
|
|
7
|
+
import { ReactFlowProvider, MarkerType, ReactFlow, Background, BackgroundVariant, BaseEdge, EdgeLabelRenderer, Handle, Position } from "@xyflow/react";
|
|
8
|
+
import OutlinedChip from "../Chips/OutlinedChip.js";
|
|
9
|
+
import FlowZoomControls from "./FlowZoomControls.js";
|
|
10
|
+
import "@xyflow/react/dist/base.css";
|
|
11
|
+
/* empty css */
|
|
12
|
+
const LANE_WIDTH = 192;
|
|
13
|
+
const LANE_HEIGHT = 168;
|
|
14
|
+
const LANE_GAP = 80;
|
|
15
|
+
const HIDDEN_HANDLE_CLASS = "!size-px !min-h-0 !min-w-0 !border-0 !bg-transparent";
|
|
16
|
+
const LaneNode = ({ data }) => {
|
|
17
|
+
const { lane, onLaneClick } = data;
|
|
18
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
19
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-foreground", children: lane.title }),
|
|
20
|
+
lane.subtitle && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-foreground-subtle", children: lane.subtitle }),
|
|
21
|
+
lane.badges && /* @__PURE__ */ jsx("div", { className: "mt-auto flex flex-wrap gap-1", children: lane.badges })
|
|
22
|
+
] });
|
|
23
|
+
return /* @__PURE__ */ jsxs("div", { style: { width: LANE_WIDTH, height: LANE_HEIGHT }, className: "relative", children: [
|
|
24
|
+
/* @__PURE__ */ jsx(
|
|
25
|
+
Handle,
|
|
26
|
+
{
|
|
27
|
+
id: "over-out",
|
|
28
|
+
type: "source",
|
|
29
|
+
position: Position.Top,
|
|
30
|
+
isConnectable: false,
|
|
31
|
+
className: HIDDEN_HANDLE_CLASS
|
|
32
|
+
}
|
|
33
|
+
),
|
|
34
|
+
/* @__PURE__ */ jsx(
|
|
35
|
+
Handle,
|
|
36
|
+
{
|
|
37
|
+
id: "over-in",
|
|
38
|
+
type: "target",
|
|
39
|
+
position: Position.Top,
|
|
40
|
+
isConnectable: false,
|
|
41
|
+
className: HIDDEN_HANDLE_CLASS
|
|
42
|
+
}
|
|
43
|
+
),
|
|
44
|
+
onLaneClick ? /* @__PURE__ */ jsx(
|
|
45
|
+
ControlledButton,
|
|
46
|
+
{
|
|
47
|
+
defaultHtmlButton: true,
|
|
48
|
+
onClick: () => onLaneClick(lane.laneId),
|
|
49
|
+
className: "interactive flex size-full flex-col gap-y-1 rounded-xl border border-border bg-surface p-3 text-left shadow-sm hover:border-border-emphasis focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
50
|
+
children: content
|
|
51
|
+
}
|
|
52
|
+
) : /* @__PURE__ */ jsx("div", { className: "flex size-full flex-col gap-y-1 rounded-xl border border-border bg-surface p-3 shadow-sm", children: content }),
|
|
53
|
+
/* @__PURE__ */ jsx(
|
|
54
|
+
Handle,
|
|
55
|
+
{
|
|
56
|
+
id: "under-out",
|
|
57
|
+
type: "source",
|
|
58
|
+
position: Position.Bottom,
|
|
59
|
+
isConnectable: false,
|
|
60
|
+
className: HIDDEN_HANDLE_CLASS
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
/* @__PURE__ */ jsx(
|
|
64
|
+
Handle,
|
|
65
|
+
{
|
|
66
|
+
id: "under-in",
|
|
67
|
+
type: "target",
|
|
68
|
+
position: Position.Bottom,
|
|
69
|
+
isConnectable: false,
|
|
70
|
+
className: HIDDEN_HANDLE_CLASS
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
] });
|
|
74
|
+
};
|
|
75
|
+
const ArcEdge = ({
|
|
76
|
+
id,
|
|
77
|
+
sourceX,
|
|
78
|
+
sourceY,
|
|
79
|
+
targetX,
|
|
80
|
+
targetY,
|
|
81
|
+
data,
|
|
82
|
+
markerEnd,
|
|
83
|
+
selected
|
|
84
|
+
}) => {
|
|
85
|
+
const arc = data?.arc;
|
|
86
|
+
const isOver = arc?.side !== "under";
|
|
87
|
+
const isBackward = targetX < sourceX;
|
|
88
|
+
const lift = 40 + Math.abs(targetX - sourceX) * 0.2 + (isBackward ? 32 : 0);
|
|
89
|
+
const controlY = isOver ? Math.min(sourceY, targetY) - lift * 1.3 : Math.max(sourceY, targetY) + lift * 1.3;
|
|
90
|
+
const path = `M ${sourceX},${sourceY} C ${sourceX},${controlY} ${targetX},${controlY} ${targetX},${targetY}`;
|
|
91
|
+
const labelX = (sourceX + targetX) / 2;
|
|
92
|
+
const labelY = 0.125 * (sourceY + targetY) + 0.75 * controlY;
|
|
93
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
94
|
+
/* @__PURE__ */ jsx(
|
|
95
|
+
BaseEdge,
|
|
96
|
+
{
|
|
97
|
+
id,
|
|
98
|
+
path,
|
|
99
|
+
markerEnd,
|
|
100
|
+
interactionWidth: 16,
|
|
101
|
+
className: classNames("umode-flow-arc", {
|
|
102
|
+
"umode-flow-arc--warning": arc?.tone === "warning",
|
|
103
|
+
"umode-flow-arc--danger": arc?.tone === "danger",
|
|
104
|
+
"umode-flow-arc--dashed": arc?.dashed,
|
|
105
|
+
"umode-flow-arc--selected": selected
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
),
|
|
109
|
+
arc?.label && /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx(
|
|
110
|
+
"div",
|
|
111
|
+
{
|
|
112
|
+
className: "nodrag nopan pointer-events-none absolute whitespace-nowrap",
|
|
113
|
+
style: { transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` },
|
|
114
|
+
children: /* @__PURE__ */ jsx(
|
|
115
|
+
OutlinedChip,
|
|
116
|
+
{
|
|
117
|
+
label: arc.label,
|
|
118
|
+
tone: arc.tone === "warning" ? "warning" : arc.tone === "danger" ? "danger" : "neutral",
|
|
119
|
+
variant: "outlined",
|
|
120
|
+
size: "xs",
|
|
121
|
+
preserveLabelCase: true
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
) })
|
|
126
|
+
] });
|
|
127
|
+
};
|
|
128
|
+
const PRO_OPTIONS = { hideAttribution: true };
|
|
129
|
+
const NODE_TYPES = { flowLane: LaneNode };
|
|
130
|
+
const EDGE_TYPES = { flowArc: ArcEdge };
|
|
131
|
+
const FlowLaneMapContent = ({
|
|
132
|
+
lanes,
|
|
133
|
+
arcs,
|
|
134
|
+
onArcClick,
|
|
135
|
+
onLaneClick,
|
|
136
|
+
selectedArcId,
|
|
137
|
+
ariaLabel,
|
|
138
|
+
className
|
|
139
|
+
}) => {
|
|
140
|
+
const { t } = useTranslation("ui");
|
|
141
|
+
const safeLanes = useMemo(() => Array.isArray(lanes) ? lanes : [], [lanes]);
|
|
142
|
+
const safeArcs = useMemo(() => Array.isArray(arcs) ? arcs : [], [arcs]);
|
|
143
|
+
const nodes = useMemo(
|
|
144
|
+
() => safeLanes.map((lane, index) => ({
|
|
145
|
+
id: lane.laneId,
|
|
146
|
+
type: "flowLane",
|
|
147
|
+
position: { x: index * (LANE_WIDTH + LANE_GAP), y: 0 },
|
|
148
|
+
draggable: false,
|
|
149
|
+
selectable: false,
|
|
150
|
+
data: { lane, onLaneClick }
|
|
151
|
+
})),
|
|
152
|
+
[safeLanes, onLaneClick]
|
|
153
|
+
);
|
|
154
|
+
const laneIds = useMemo(() => new Set(safeLanes.map((lane) => lane.laneId)), [safeLanes]);
|
|
155
|
+
const edges = useMemo(
|
|
156
|
+
() => safeArcs.filter(
|
|
157
|
+
(arc) => laneIds.has(arc.sourceLaneId) && laneIds.has(arc.targetLaneId) && arc.sourceLaneId !== arc.targetLaneId
|
|
158
|
+
).map((arc) => ({
|
|
159
|
+
id: arc.arcId,
|
|
160
|
+
type: "flowArc",
|
|
161
|
+
source: arc.sourceLaneId,
|
|
162
|
+
sourceHandle: `${arc.side}-out`,
|
|
163
|
+
target: arc.targetLaneId,
|
|
164
|
+
targetHandle: `${arc.side}-in`,
|
|
165
|
+
selected: arc.arcId === selectedArcId,
|
|
166
|
+
markerEnd: { type: MarkerType.ArrowClosed, width: 14, height: 14 },
|
|
167
|
+
data: { arc }
|
|
168
|
+
})),
|
|
169
|
+
[safeArcs, laneIds, selectedArcId]
|
|
170
|
+
);
|
|
171
|
+
return /* @__PURE__ */ jsx(
|
|
172
|
+
"div",
|
|
173
|
+
{
|
|
174
|
+
role: "region",
|
|
175
|
+
"aria-label": ariaLabel ?? t("flow.laneMap.label"),
|
|
176
|
+
className: twMerge("umode-flow relative h-full min-h-[24rem] w-full rounded-xl bg-canvas", className),
|
|
177
|
+
children: /* @__PURE__ */ jsxs(
|
|
178
|
+
ReactFlow,
|
|
179
|
+
{
|
|
180
|
+
nodes,
|
|
181
|
+
edges,
|
|
182
|
+
nodeTypes: NODE_TYPES,
|
|
183
|
+
edgeTypes: EDGE_TYPES,
|
|
184
|
+
nodesDraggable: false,
|
|
185
|
+
nodesConnectable: false,
|
|
186
|
+
deleteKeyCode: null,
|
|
187
|
+
onEdgeClick: (_event, edge) => onArcClick?.(edge.id),
|
|
188
|
+
minZoom: 0.25,
|
|
189
|
+
maxZoom: 1.5,
|
|
190
|
+
fitView: true,
|
|
191
|
+
fitViewOptions: { padding: 0.15, minZoom: 0.75 },
|
|
192
|
+
proOptions: PRO_OPTIONS,
|
|
193
|
+
children: [
|
|
194
|
+
/* @__PURE__ */ jsx(Background, { variant: BackgroundVariant.Dots, gap: 16, size: 1 }),
|
|
195
|
+
/* @__PURE__ */ jsx(FlowZoomControls, {})
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
const FlowLaneMap = (props) => /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(FlowLaneMapContent, { ...props }) });
|
|
203
|
+
export {
|
|
204
|
+
FlowLaneMap as default
|
|
205
|
+
};
|
|
206
|
+
//# sourceMappingURL=FlowLaneMap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlowLaneMap.js","sources":["../../../../src/components/UI/Flow/FlowLaneMap.tsx"],"sourcesContent":["import { ReactNode, useMemo } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport {\n Background,\n BackgroundVariant,\n BaseEdge,\n type Edge,\n EdgeLabelRenderer,\n type EdgeProps,\n Handle,\n MarkerType,\n type Node,\n type NodeProps,\n Position,\n type ProOptions,\n ReactFlow,\n ReactFlowProvider,\n} from '@xyflow/react';\n\nimport OutlinedChip from '../Chips/OutlinedChip';\n\nimport FlowZoomControls from './FlowZoomControls';\n\nimport '@xyflow/react/dist/base.css';\nimport '../../../styles/flow.css';\n\nexport interface IFlowLane {\n laneId: string;\n title: string;\n subtitle?: string;\n /** Counters or chips under the title (rules acting on entry, stage requirements). */\n badges?: ReactNode;\n}\n\nexport type TFlowArcTone = 'default' | 'warning' | 'danger';\n\nexport interface IFlowArc {\n arcId: string;\n sourceLaneId: string;\n targetLaneId: string;\n label: string;\n /** `over` arcs above the lanes, `under` below them; the host decides what each side means. */\n side: 'over' | 'under';\n tone?: TFlowArcTone;\n dashed?: boolean;\n}\n\ninterface IFlowLaneMapProps {\n lanes: Array<IFlowLane>;\n arcs: Array<IFlowArc>;\n onArcClick?: (arcId: string) => void;\n onLaneClick?: (laneId: string) => void;\n selectedArcId?: string | null;\n ariaLabel?: string;\n className?: string;\n}\n\nconst LANE_WIDTH = 192;\nconst LANE_HEIGHT = 168;\nconst LANE_GAP = 80;\n\ninterface ILaneNodeData extends Record<string, unknown> {\n lane: IFlowLane;\n onLaneClick?: (laneId: string) => void;\n}\n\ninterface IArcEdgeData extends Record<string, unknown> {\n arc: IFlowArc;\n}\n\ntype TLaneNode = Node<ILaneNodeData, 'flowLane'>;\ntype TArcEdge = Edge<IArcEdgeData, 'flowArc'>;\n\nconst HIDDEN_HANDLE_CLASS = '!size-px !min-h-0 !min-w-0 !border-0 !bg-transparent';\n\nconst LaneNode: React.FC<NodeProps<TLaneNode>> = ({ data }) => {\n const { lane, onLaneClick } = data;\n const content = (\n <>\n <span className=\"truncate text-sm font-semibold text-foreground\">{lane.title}</span>\n {lane.subtitle && <span className=\"truncate text-xs text-foreground-subtle\">{lane.subtitle}</span>}\n {lane.badges && <div className=\"mt-auto flex flex-wrap gap-1\">{lane.badges}</div>}\n </>\n );\n\n return (\n <div style={{ width: LANE_WIDTH, height: LANE_HEIGHT }} className=\"relative\">\n <Handle\n id=\"over-out\"\n type=\"source\"\n position={Position.Top}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n <Handle\n id=\"over-in\"\n type=\"target\"\n position={Position.Top}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n {onLaneClick ? (\n <ControlledButton\n defaultHtmlButton\n onClick={() => onLaneClick(lane.laneId)}\n className=\"interactive flex size-full flex-col gap-y-1 rounded-xl border border-border bg-surface p-3 text-left shadow-sm hover:border-border-emphasis focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {content}\n </ControlledButton>\n ) : (\n <div className=\"flex size-full flex-col gap-y-1 rounded-xl border border-border bg-surface p-3 shadow-sm\">\n {content}\n </div>\n )}\n <Handle\n id=\"under-out\"\n type=\"source\"\n position={Position.Bottom}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n <Handle\n id=\"under-in\"\n type=\"target\"\n position={Position.Bottom}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n </div>\n );\n};\n\n/** A curve between two lane edges whose height grows with the distance, so long arcs clear the short ones. */\nconst ArcEdge: React.FC<EdgeProps<TArcEdge>> = ({\n id,\n sourceX,\n sourceY,\n targetX,\n targetY,\n data,\n markerEnd,\n selected,\n}) => {\n const arc = data?.arc;\n const isOver = arc?.side !== 'under';\n const isBackward = targetX < sourceX;\n const lift = 40 + Math.abs(targetX - sourceX) * 0.2 + (isBackward ? 32 : 0);\n const controlY = isOver ? Math.min(sourceY, targetY) - lift * 1.3 : Math.max(sourceY, targetY) + lift * 1.3;\n const path = `M ${sourceX},${sourceY} C ${sourceX},${controlY} ${targetX},${controlY} ${targetX},${targetY}`;\n const labelX = (sourceX + targetX) / 2;\n const labelY = 0.125 * (sourceY + targetY) + 0.75 * controlY;\n\n return (\n <>\n <BaseEdge\n id={id}\n path={path}\n markerEnd={markerEnd}\n interactionWidth={16}\n className={classNames('umode-flow-arc', {\n 'umode-flow-arc--warning': arc?.tone === 'warning',\n 'umode-flow-arc--danger': arc?.tone === 'danger',\n 'umode-flow-arc--dashed': arc?.dashed,\n 'umode-flow-arc--selected': selected,\n })}\n />\n {arc?.label && (\n <EdgeLabelRenderer>\n <div\n className=\"nodrag nopan pointer-events-none absolute whitespace-nowrap\"\n style={{ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` }}\n >\n <OutlinedChip\n label={arc.label}\n tone={arc.tone === 'warning' ? 'warning' : arc.tone === 'danger' ? 'danger' : 'neutral'}\n variant=\"outlined\"\n size=\"xs\"\n preserveLabelCase\n />\n </div>\n </EdgeLabelRenderer>\n )}\n </>\n );\n};\n\nconst PRO_OPTIONS: ProOptions = { hideAttribution: true };\nconst NODE_TYPES = { flowLane: LaneNode };\nconst EDGE_TYPES = { flowArc: ArcEdge };\n\n/**\n * A read-only map of fixed columns joined by arcs: the shape of a pipeline seen from\n * above. Lanes keep the order they are given, so no graph layout is needed.\n */\nconst FlowLaneMapContent: React.FC<IFlowLaneMapProps> = ({\n lanes,\n arcs,\n onArcClick,\n onLaneClick,\n selectedArcId,\n ariaLabel,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const safeLanes = useMemo(() => (Array.isArray(lanes) ? lanes : []), [lanes]);\n const safeArcs = useMemo(() => (Array.isArray(arcs) ? arcs : []), [arcs]);\n\n const nodes = useMemo<Array<TLaneNode>>(\n () =>\n safeLanes.map((lane, index) => ({\n id: lane.laneId,\n type: 'flowLane',\n position: { x: index * (LANE_WIDTH + LANE_GAP), y: 0 },\n draggable: false,\n selectable: false,\n data: { lane, onLaneClick },\n })),\n [safeLanes, onLaneClick],\n );\n\n const laneIds = useMemo(() => new Set(safeLanes.map((lane) => lane.laneId)), [safeLanes]);\n\n const edges = useMemo<Array<TArcEdge>>(\n () =>\n safeArcs\n .filter(\n (arc) =>\n laneIds.has(arc.sourceLaneId) && laneIds.has(arc.targetLaneId) && arc.sourceLaneId !== arc.targetLaneId,\n )\n .map((arc) => ({\n id: arc.arcId,\n type: 'flowArc',\n source: arc.sourceLaneId,\n sourceHandle: `${arc.side}-out`,\n target: arc.targetLaneId,\n targetHandle: `${arc.side}-in`,\n selected: arc.arcId === selectedArcId,\n markerEnd: { type: MarkerType.ArrowClosed, width: 14, height: 14 },\n data: { arc },\n })),\n [safeArcs, laneIds, selectedArcId],\n );\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel ?? t('flow.laneMap.label')}\n className={twMerge('umode-flow relative h-full min-h-[24rem] w-full rounded-xl bg-canvas', className)}\n >\n <ReactFlow<TLaneNode, TArcEdge>\n nodes={nodes}\n edges={edges}\n nodeTypes={NODE_TYPES}\n edgeTypes={EDGE_TYPES}\n nodesDraggable={false}\n nodesConnectable={false}\n deleteKeyCode={null}\n onEdgeClick={(_event, edge) => onArcClick?.(edge.id)}\n minZoom={0.25}\n maxZoom={1.5}\n fitView\n fitViewOptions={{ padding: 0.15, minZoom: 0.75 }}\n proOptions={PRO_OPTIONS}\n >\n <Background variant={BackgroundVariant.Dots} gap={16} size={1} />\n <FlowZoomControls />\n </ReactFlow>\n </div>\n );\n};\n\nconst FlowLaneMap: React.FC<IFlowLaneMapProps> = (props) => (\n <ReactFlowProvider>\n <FlowLaneMapContent {...props} />\n </ReactFlowProvider>\n);\n\nexport default FlowLaneMap;\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { SymbolCodepoints } from '../MaterialSymbol';
|
|
3
|
+
import { TFlowIssueSeverity, TFlowRunStepStatus } from './flowTypes';
|
|
4
|
+
export interface IFlowNodeShellProps {
|
|
5
|
+
icon: SymbolCodepoints;
|
|
6
|
+
title: string;
|
|
7
|
+
summary?: ReactNode;
|
|
8
|
+
categoryLabel?: string;
|
|
9
|
+
selected?: boolean;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
issueSeverity?: TFlowIssueSeverity;
|
|
12
|
+
issueCount?: number;
|
|
13
|
+
runStatus?: TFlowRunStepStatus;
|
|
14
|
+
/** The ports, rendered by the canvas at the block's edges. */
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
/** The look of one block: icon, title, a one-line summary and its status badges. */
|
|
19
|
+
declare const FlowNodeShell: React.FC<IFlowNodeShellProps>;
|
|
20
|
+
export default FlowNodeShell;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import OutlinedChip from "../Chips/OutlinedChip.js";
|
|
5
|
+
import { MaterialSymbol } from "../MaterialSymbol/index.js";
|
|
6
|
+
import { useFlowLabels, FLOW_RUN_STATUS_TONE } from "./useFlowLabels.js";
|
|
7
|
+
const RUN_BORDER_CLASS = {
|
|
8
|
+
passed: "border-success-border",
|
|
9
|
+
wouldRun: "border-info-border",
|
|
10
|
+
skipped: "border-border",
|
|
11
|
+
blocked: "border-warning-border",
|
|
12
|
+
failed: "border-danger-border",
|
|
13
|
+
error: "border-danger-border"
|
|
14
|
+
};
|
|
15
|
+
const FlowNodeShell = ({
|
|
16
|
+
icon,
|
|
17
|
+
title,
|
|
18
|
+
summary,
|
|
19
|
+
categoryLabel,
|
|
20
|
+
selected = false,
|
|
21
|
+
disabled = false,
|
|
22
|
+
issueSeverity,
|
|
23
|
+
issueCount = 0,
|
|
24
|
+
runStatus,
|
|
25
|
+
children,
|
|
26
|
+
className
|
|
27
|
+
}) => {
|
|
28
|
+
const { t } = useTranslation("ui");
|
|
29
|
+
const labels = useFlowLabels();
|
|
30
|
+
const borderClass = runStatus ? RUN_BORDER_CLASS[runStatus] : issueSeverity === "error" ? "border-danger-border" : issueSeverity === "warning" ? "border-warning-border" : "border-border";
|
|
31
|
+
return /* @__PURE__ */ jsxs(
|
|
32
|
+
"div",
|
|
33
|
+
{
|
|
34
|
+
className: classNames(
|
|
35
|
+
"relative flex w-64 flex-col gap-y-2 rounded-xl border-2 bg-surface px-4 py-3 shadow-sm transition-colors duration-fast",
|
|
36
|
+
borderClass,
|
|
37
|
+
{ "ring-2 ring-ring ring-offset-2 ring-offset-canvas": selected, "opacity-60": disabled },
|
|
38
|
+
className
|
|
39
|
+
),
|
|
40
|
+
children: [
|
|
41
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
42
|
+
/* @__PURE__ */ jsx("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground-muted", children: /* @__PURE__ */ jsx(MaterialSymbol, { icon, size: 18, "aria-hidden": true }) }),
|
|
43
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col", children: [
|
|
44
|
+
categoryLabel && /* @__PURE__ */ jsx("span", { className: "text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: categoryLabel }),
|
|
45
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-foreground", children: title })
|
|
46
|
+
] })
|
|
47
|
+
] }),
|
|
48
|
+
summary && /* @__PURE__ */ jsx("div", { className: "line-clamp-2 text-xs text-foreground-muted", children: summary }),
|
|
49
|
+
(runStatus || issueCount > 0 || disabled) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1", children: [
|
|
50
|
+
runStatus && /* @__PURE__ */ jsx(
|
|
51
|
+
OutlinedChip,
|
|
52
|
+
{
|
|
53
|
+
size: "xs",
|
|
54
|
+
variant: "soft",
|
|
55
|
+
tone: FLOW_RUN_STATUS_TONE[runStatus],
|
|
56
|
+
label: labels.runStatus[runStatus],
|
|
57
|
+
preserveLabelCase: true,
|
|
58
|
+
dot: true
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
issueCount > 0 && /* @__PURE__ */ jsx(
|
|
62
|
+
OutlinedChip,
|
|
63
|
+
{
|
|
64
|
+
size: "xs",
|
|
65
|
+
variant: "soft",
|
|
66
|
+
tone: issueSeverity === "error" ? "danger" : "warning",
|
|
67
|
+
label: t("flow.node.issueCount", { count: issueCount }),
|
|
68
|
+
preserveLabelCase: true
|
|
69
|
+
}
|
|
70
|
+
),
|
|
71
|
+
disabled && /* @__PURE__ */ jsx(OutlinedChip, { size: "xs", variant: "soft", tone: "neutral", label: t("flow.node.disabled"), preserveLabelCase: true })
|
|
72
|
+
] }),
|
|
73
|
+
children
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
export {
|
|
79
|
+
FlowNodeShell as default
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=FlowNodeShell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlowNodeShell.js","sources":["../../../../src/components/UI/Flow/FlowNodeShell.tsx"],"sourcesContent":["import { ReactNode } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport OutlinedChip from '@/components/UI/Chips/OutlinedChip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\n\nimport type { TFlowIssueSeverity, TFlowRunStepStatus } from './flowTypes';\nimport { FLOW_RUN_STATUS_TONE, useFlowLabels } from './useFlowLabels';\n\nexport interface IFlowNodeShellProps {\n icon: SymbolCodepoints;\n title: string;\n summary?: ReactNode;\n categoryLabel?: string;\n selected?: boolean;\n disabled?: boolean;\n issueSeverity?: TFlowIssueSeverity;\n issueCount?: number;\n runStatus?: TFlowRunStepStatus;\n /** The ports, rendered by the canvas at the block's edges. */\n children?: ReactNode;\n className?: string;\n}\n\nconst RUN_BORDER_CLASS: Record<TFlowRunStepStatus, string> = {\n passed: 'border-success-border',\n wouldRun: 'border-info-border',\n skipped: 'border-border',\n blocked: 'border-warning-border',\n failed: 'border-danger-border',\n error: 'border-danger-border',\n};\n\n/** The look of one block: icon, title, a one-line summary and its status badges. */\nconst FlowNodeShell: React.FC<IFlowNodeShellProps> = ({\n icon,\n title,\n summary,\n categoryLabel,\n selected = false,\n disabled = false,\n issueSeverity,\n issueCount = 0,\n runStatus,\n children,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n\n const borderClass = runStatus\n ? RUN_BORDER_CLASS[runStatus]\n : issueSeverity === 'error'\n ? 'border-danger-border'\n : issueSeverity === 'warning'\n ? 'border-warning-border'\n : 'border-border';\n\n return (\n <div\n className={classNames(\n 'relative flex w-64 flex-col gap-y-2 rounded-xl border-2 bg-surface px-4 py-3 shadow-sm transition-colors duration-fast',\n borderClass,\n { 'ring-2 ring-ring ring-offset-2 ring-offset-canvas': selected, 'opacity-60': disabled },\n className,\n )}\n >\n <div className=\"flex items-center gap-x-2\">\n <span className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground-muted\">\n <MaterialSymbol icon={icon} size={18} aria-hidden />\n </span>\n <div className=\"flex min-w-0 flex-col\">\n {categoryLabel && (\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {categoryLabel}\n </span>\n )}\n <span className=\"truncate text-sm font-semibold text-foreground\">{title}</span>\n </div>\n </div>\n {summary && <div className=\"line-clamp-2 text-xs text-foreground-muted\">{summary}</div>}\n {(runStatus || issueCount > 0 || disabled) && (\n <div className=\"flex flex-wrap items-center gap-1\">\n {runStatus && (\n <OutlinedChip\n size=\"xs\"\n variant=\"soft\"\n tone={FLOW_RUN_STATUS_TONE[runStatus]}\n label={labels.runStatus[runStatus]}\n preserveLabelCase\n dot\n />\n )}\n {issueCount > 0 && (\n <OutlinedChip\n size=\"xs\"\n variant=\"soft\"\n tone={issueSeverity === 'error' ? 'danger' : 'warning'}\n label={t('flow.node.issueCount', { count: issueCount })}\n preserveLabelCase\n />\n )}\n {disabled && (\n <OutlinedChip size=\"xs\" variant=\"soft\" tone=\"neutral\" label={t('flow.node.disabled')} preserveLabelCase />\n )}\n </div>\n )}\n {children}\n </div>\n );\n};\n\nexport default FlowNodeShell;\n"],"names":[],"mappings":";;;;;;AAyBA,MAAM,mBAAuD;AAAA,EAC3D,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,OAAO;AACT;AAGA,MAAM,gBAA+C,CAAC;AAAA,EACpD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,WAAW;AAAA,EACX;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,SAAS,cAAA;AAEf,QAAM,cAAc,YAChB,iBAAiB,SAAS,IAC1B,kBAAkB,UAChB,yBACA,kBAAkB,YAChB,0BACA;AAER,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,EAAE,qDAAqD,UAAU,cAAc,SAAA;AAAA,QAC/E;AAAA,MAAA;AAAA,MAGF,UAAA;AAAA,QAAA,qBAAC,OAAA,EAAI,WAAU,6BACb,UAAA;AAAA,UAAA,oBAAC,QAAA,EAAK,WAAU,8FACd,UAAA,oBAAC,gBAAA,EAAe,MAAY,MAAM,IAAI,eAAW,KAAA,CAAC,EAAA,CACpD;AAAA,UACA,qBAAC,OAAA,EAAI,WAAU,yBACZ,UAAA;AAAA,YAAA,iBACC,oBAAC,QAAA,EAAK,WAAU,yEACb,UAAA,eACH;AAAA,YAEF,oBAAC,QAAA,EAAK,WAAU,kDAAkD,UAAA,MAAA,CAAM;AAAA,UAAA,EAAA,CAC1E;AAAA,QAAA,GACF;AAAA,QACC,WAAW,oBAAC,OAAA,EAAI,WAAU,8CAA8C,UAAA,SAAQ;AAAA,SAC/E,aAAa,aAAa,KAAK,aAC/B,qBAAC,OAAA,EAAI,WAAU,qCACZ,UAAA;AAAA,UAAA,aACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAM,qBAAqB,SAAS;AAAA,cACpC,OAAO,OAAO,UAAU,SAAS;AAAA,cACjC,mBAAiB;AAAA,cACjB,KAAG;AAAA,YAAA;AAAA,UAAA;AAAA,UAGN,aAAa,KACZ;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAM,kBAAkB,UAAU,WAAW;AAAA,cAC7C,OAAO,EAAE,wBAAwB,EAAE,OAAO,YAAY;AAAA,cACtD,mBAAiB;AAAA,YAAA;AAAA,UAAA;AAAA,UAGpB,YACC,oBAAC,cAAA,EAAa,MAAK,MAAK,SAAQ,QAAO,MAAK,WAAU,OAAO,EAAE,oBAAoB,GAAG,mBAAiB,KAAA,CAAC;AAAA,QAAA,GAE5G;AAAA,QAED;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGP;"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
export interface IFlowResizablePanelProps {
|
|
3
|
+
/** Which edge carries the drag handle: the one that faces the canvas. */
|
|
4
|
+
handleSide: 'left' | 'right';
|
|
5
|
+
/** Widths in px, like any pointer-driven geometry. */
|
|
6
|
+
defaultWidth: number;
|
|
7
|
+
minWidth: number;
|
|
8
|
+
maxWidth: number;
|
|
9
|
+
/** Remembers the width the user chose, per panel, across visits. */
|
|
10
|
+
storageKey?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A side panel of the flow editor that the user can widen or narrow by dragging the edge facing
|
|
16
|
+
* the canvas (arrow keys on the focused handle, double click resets). Below `lg` it takes the full
|
|
17
|
+
* width of its container and the handle hides; where it sits then is up to the parent layout.
|
|
18
|
+
*/
|
|
19
|
+
declare const FlowResizablePanel: React.FC<IFlowResizablePanelProps>;
|
|
20
|
+
export default FlowResizablePanel;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useState, useRef, useEffect } from "react";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { twMerge } from "tailwind-merge";
|
|
6
|
+
import ControlledButton from "../../Form/ControlledButton.js";
|
|
7
|
+
import ControlledTooltip from "../ControlledTooltip.js";
|
|
8
|
+
import { writeUiPreference, readUiPreference } from "../../../helpers/uiPreferences.js";
|
|
9
|
+
const PANEL_WIDTHS_KEY = "umode-flow-panel-widths";
|
|
10
|
+
const KEYBOARD_RESIZE_STEP = 24;
|
|
11
|
+
const readStoredWidths = () => readUiPreference(PANEL_WIDTHS_KEY) ?? {};
|
|
12
|
+
const FlowResizablePanel = ({
|
|
13
|
+
handleSide,
|
|
14
|
+
defaultWidth,
|
|
15
|
+
minWidth,
|
|
16
|
+
maxWidth,
|
|
17
|
+
storageKey,
|
|
18
|
+
className,
|
|
19
|
+
children
|
|
20
|
+
}) => {
|
|
21
|
+
const { t } = useTranslation("ui");
|
|
22
|
+
const clampWidth = useCallback(
|
|
23
|
+
(width2) => Math.min(maxWidth, Math.max(minWidth, Math.round(width2))),
|
|
24
|
+
[minWidth, maxWidth]
|
|
25
|
+
);
|
|
26
|
+
const [width, setWidth] = useState(() => {
|
|
27
|
+
const stored = storageKey ? readStoredWidths()[storageKey] : void 0;
|
|
28
|
+
return clampWidth(typeof stored === "number" ? stored : defaultWidth);
|
|
29
|
+
});
|
|
30
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
31
|
+
const resizeStartRef = useRef(null);
|
|
32
|
+
const widthRef = useRef(width);
|
|
33
|
+
widthRef.current = width;
|
|
34
|
+
const persistWidth = useCallback(
|
|
35
|
+
(nextWidth) => {
|
|
36
|
+
if (!storageKey) return;
|
|
37
|
+
writeUiPreference(PANEL_WIDTHS_KEY, { ...readStoredWidths(), [storageKey]: nextWidth });
|
|
38
|
+
},
|
|
39
|
+
[storageKey]
|
|
40
|
+
);
|
|
41
|
+
useEffect(() => setWidth((current) => clampWidth(current)), [clampWidth]);
|
|
42
|
+
const handlePointerDown = (event) => {
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
resizeStartRef.current = { pointerX: event.clientX, width: widthRef.current };
|
|
45
|
+
setIsResizing(true);
|
|
46
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
47
|
+
};
|
|
48
|
+
const handlePointerMove = (event) => {
|
|
49
|
+
const resizeStart = resizeStartRef.current;
|
|
50
|
+
if (!resizeStart) return;
|
|
51
|
+
const pointerDelta = event.clientX - resizeStart.pointerX;
|
|
52
|
+
setWidth(clampWidth(resizeStart.width + (handleSide === "right" ? pointerDelta : -pointerDelta)));
|
|
53
|
+
};
|
|
54
|
+
const handlePointerEnd = (event) => {
|
|
55
|
+
if (!resizeStartRef.current) return;
|
|
56
|
+
resizeStartRef.current = null;
|
|
57
|
+
setIsResizing(false);
|
|
58
|
+
persistWidth(widthRef.current);
|
|
59
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
60
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const handleKeyDown = (event) => {
|
|
64
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
const pointerDirection = event.key === "ArrowRight" ? 1 : -1;
|
|
67
|
+
const nextWidth = clampWidth(
|
|
68
|
+
widthRef.current + (handleSide === "right" ? pointerDirection : -pointerDirection) * KEYBOARD_RESIZE_STEP
|
|
69
|
+
);
|
|
70
|
+
setWidth(nextWidth);
|
|
71
|
+
persistWidth(nextWidth);
|
|
72
|
+
};
|
|
73
|
+
const handleReset = () => {
|
|
74
|
+
const nextWidth = clampWidth(defaultWidth);
|
|
75
|
+
setWidth(nextWidth);
|
|
76
|
+
persistWidth(nextWidth);
|
|
77
|
+
};
|
|
78
|
+
return /* @__PURE__ */ jsxs(
|
|
79
|
+
"div",
|
|
80
|
+
{
|
|
81
|
+
className: twMerge("relative w-full shrink-0 lg:w-[var(--flow-panel-width)]", className),
|
|
82
|
+
style: { "--flow-panel-width": `${width}px` },
|
|
83
|
+
children: [
|
|
84
|
+
children,
|
|
85
|
+
/* @__PURE__ */ jsx(ControlledTooltip, { content: t("flow.resizePanel"), placement: handleSide === "right" ? "right" : "left", children: /* @__PURE__ */ jsx(
|
|
86
|
+
ControlledButton,
|
|
87
|
+
{
|
|
88
|
+
defaultHtmlButton: true,
|
|
89
|
+
onPointerDown: handlePointerDown,
|
|
90
|
+
onPointerMove: handlePointerMove,
|
|
91
|
+
onPointerUp: handlePointerEnd,
|
|
92
|
+
onPointerCancel: handlePointerEnd,
|
|
93
|
+
onKeyDown: handleKeyDown,
|
|
94
|
+
onDoubleClick: handleReset,
|
|
95
|
+
className: classNames(
|
|
96
|
+
"group/resize absolute inset-y-0 z-10 hidden w-3 cursor-col-resize touch-none select-none items-center justify-center focus:outline-none lg:flex",
|
|
97
|
+
handleSide === "right" ? "-right-3" : "-left-3"
|
|
98
|
+
),
|
|
99
|
+
children: /* @__PURE__ */ jsx(
|
|
100
|
+
"span",
|
|
101
|
+
{
|
|
102
|
+
className: classNames(
|
|
103
|
+
"h-full w-0.5 rounded-full transition-colors duration-base",
|
|
104
|
+
isResizing ? "bg-brand-hover" : "bg-transparent group-hover/resize:bg-brand-hover"
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
) })
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
export {
|
|
115
|
+
FlowResizablePanel as default
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=FlowResizablePanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlowResizablePanel.js","sources":["../../../../src/components/UI/Flow/FlowResizablePanel.tsx"],"sourcesContent":["import { CSSProperties, ReactNode, useCallback, useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { readUiPreference, writeUiPreference } from '@/helpers/uiPreferences';\n\nconst PANEL_WIDTHS_KEY = 'umode-flow-panel-widths';\nconst KEYBOARD_RESIZE_STEP = 24;\n\nexport interface IFlowResizablePanelProps {\n /** Which edge carries the drag handle: the one that faces the canvas. */\n handleSide: 'left' | 'right';\n /** Widths in px, like any pointer-driven geometry. */\n defaultWidth: number;\n minWidth: number;\n maxWidth: number;\n /** Remembers the width the user chose, per panel, across visits. */\n storageKey?: string;\n className?: string;\n children: ReactNode;\n}\n\nconst readStoredWidths = (): Record<string, number> => readUiPreference<Record<string, number>>(PANEL_WIDTHS_KEY) ?? {};\n\n/**\n * A side panel of the flow editor that the user can widen or narrow by dragging the edge facing\n * the canvas (arrow keys on the focused handle, double click resets). Below `lg` it takes the full\n * width of its container and the handle hides; where it sits then is up to the parent layout.\n */\nconst FlowResizablePanel: React.FC<IFlowResizablePanelProps> = ({\n handleSide,\n defaultWidth,\n minWidth,\n maxWidth,\n storageKey,\n className,\n children,\n}) => {\n const { t } = useTranslation('ui');\n const clampWidth = useCallback(\n (width: number) => Math.min(maxWidth, Math.max(minWidth, Math.round(width))),\n [minWidth, maxWidth],\n );\n const [width, setWidth] = useState(() => {\n const stored = storageKey ? readStoredWidths()[storageKey] : undefined;\n return clampWidth(typeof stored === 'number' ? stored : defaultWidth);\n });\n const [isResizing, setIsResizing] = useState(false);\n const resizeStartRef = useRef<{ pointerX: number; width: number } | null>(null);\n const widthRef = useRef(width);\n widthRef.current = width;\n\n const persistWidth = useCallback(\n (nextWidth: number) => {\n if (!storageKey) return;\n writeUiPreference(PANEL_WIDTHS_KEY, { ...readStoredWidths(), [storageKey]: nextWidth });\n },\n [storageKey],\n );\n\n useEffect(() => setWidth((current) => clampWidth(current)), [clampWidth]);\n\n const handlePointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {\n event.preventDefault();\n resizeStartRef.current = { pointerX: event.clientX, width: widthRef.current };\n setIsResizing(true);\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const handlePointerMove = (event: React.PointerEvent<HTMLButtonElement>) => {\n const resizeStart = resizeStartRef.current;\n if (!resizeStart) return;\n const pointerDelta = event.clientX - resizeStart.pointerX;\n setWidth(clampWidth(resizeStart.width + (handleSide === 'right' ? pointerDelta : -pointerDelta)));\n };\n\n const handlePointerEnd = (event: React.PointerEvent<HTMLButtonElement>) => {\n if (!resizeStartRef.current) return;\n resizeStartRef.current = null;\n setIsResizing(false);\n persistWidth(widthRef.current);\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n };\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;\n event.preventDefault();\n const pointerDirection = event.key === 'ArrowRight' ? 1 : -1;\n const nextWidth = clampWidth(\n widthRef.current + (handleSide === 'right' ? pointerDirection : -pointerDirection) * KEYBOARD_RESIZE_STEP,\n );\n setWidth(nextWidth);\n persistWidth(nextWidth);\n };\n\n const handleReset = () => {\n const nextWidth = clampWidth(defaultWidth);\n setWidth(nextWidth);\n persistWidth(nextWidth);\n };\n\n return (\n <div\n className={twMerge('relative w-full shrink-0 lg:w-[var(--flow-panel-width)]', className)}\n style={{ '--flow-panel-width': `${width}px` } as CSSProperties}\n >\n {children}\n <ControlledTooltip content={t('flow.resizePanel')} placement={handleSide === 'right' ? 'right' : 'left'}>\n <ControlledButton\n defaultHtmlButton\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerEnd}\n onPointerCancel={handlePointerEnd}\n onKeyDown={handleKeyDown}\n onDoubleClick={handleReset}\n className={classNames(\n 'group/resize absolute inset-y-0 z-10 hidden w-3 cursor-col-resize touch-none select-none items-center justify-center focus:outline-none lg:flex',\n handleSide === 'right' ? '-right-3' : '-left-3',\n )}\n >\n <span\n className={classNames(\n 'h-full w-0.5 rounded-full transition-colors duration-base',\n isResizing ? 'bg-brand-hover' : 'bg-transparent group-hover/resize:bg-brand-hover',\n )}\n />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n );\n};\n\nexport default FlowResizablePanel;\n"],"names":["width"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PanelPosition } from '@xyflow/react';
|
|
2
|
+
interface IFlowZoomControlsProps {
|
|
3
|
+
onUndo?: () => void;
|
|
4
|
+
onRedo?: () => void;
|
|
5
|
+
canUndo?: boolean;
|
|
6
|
+
canRedo?: boolean;
|
|
7
|
+
onLayout?: () => void;
|
|
8
|
+
position?: PanelPosition;
|
|
9
|
+
}
|
|
10
|
+
/** Zoom, fit and history controls drawn with the library's own buttons. */
|
|
11
|
+
declare const FlowZoomControls: React.FC<IFlowZoomControlsProps>;
|
|
12
|
+
export default FlowZoomControls;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import ControlledButton from "../../Form/ControlledButton.js";
|
|
4
|
+
import ControlledTooltip from "../ControlledTooltip.js";
|
|
5
|
+
import { MaterialSymbol } from "../MaterialSymbol/index.js";
|
|
6
|
+
import { useReactFlow, Panel } from "@xyflow/react";
|
|
7
|
+
const ControlButton = ({ icon, label, onClick, disabled }) => /* @__PURE__ */ jsx(ControlledTooltip, { content: label, placement: "right", children: /* @__PURE__ */ jsx(
|
|
8
|
+
ControlledButton,
|
|
9
|
+
{
|
|
10
|
+
defaultHtmlButton: true,
|
|
11
|
+
onClick,
|
|
12
|
+
disabled,
|
|
13
|
+
className: "interactive flex size-8 items-center justify-center rounded-lg text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40",
|
|
14
|
+
children: /* @__PURE__ */ jsx(MaterialSymbol, { icon, size: 18, "aria-hidden": true })
|
|
15
|
+
}
|
|
16
|
+
) });
|
|
17
|
+
const FlowZoomControls = ({
|
|
18
|
+
onUndo,
|
|
19
|
+
onRedo,
|
|
20
|
+
canUndo,
|
|
21
|
+
canRedo,
|
|
22
|
+
onLayout,
|
|
23
|
+
position = "top-left"
|
|
24
|
+
}) => {
|
|
25
|
+
const { t } = useTranslation("ui");
|
|
26
|
+
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
|
27
|
+
return /* @__PURE__ */ jsxs(
|
|
28
|
+
Panel,
|
|
29
|
+
{
|
|
30
|
+
position,
|
|
31
|
+
className: "flex flex-col gap-y-1 rounded-xl border border-border bg-surface p-1 shadow-sm",
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ jsx(ControlButton, { icon: "add", label: t("flow.controls.zoomIn"), onClick: () => void zoomIn() }),
|
|
34
|
+
/* @__PURE__ */ jsx(ControlButton, { icon: "remove", label: t("flow.controls.zoomOut"), onClick: () => void zoomOut() }),
|
|
35
|
+
/* @__PURE__ */ jsx(
|
|
36
|
+
ControlButton,
|
|
37
|
+
{
|
|
38
|
+
icon: "fit_screen",
|
|
39
|
+
label: t("flow.controls.fitView"),
|
|
40
|
+
onClick: () => void fitView({ padding: 0.2 })
|
|
41
|
+
}
|
|
42
|
+
),
|
|
43
|
+
onLayout && /* @__PURE__ */ jsx(ControlButton, { icon: "account_tree", label: t("flow.controls.layout"), onClick: onLayout }),
|
|
44
|
+
onUndo && /* @__PURE__ */ jsx(ControlButton, { icon: "undo", label: t("flow.controls.undo"), onClick: onUndo, disabled: !canUndo }),
|
|
45
|
+
onRedo && /* @__PURE__ */ jsx(ControlButton, { icon: "redo", label: t("flow.controls.redo"), onClick: onRedo, disabled: !canRedo })
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
export {
|
|
51
|
+
FlowZoomControls as default
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=FlowZoomControls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlowZoomControls.js","sources":["../../../../src/components/UI/Flow/FlowZoomControls.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { Panel, type PanelPosition, useReactFlow } from '@xyflow/react';\n\ninterface IFlowZoomControlsProps {\n onUndo?: () => void;\n onRedo?: () => void;\n canUndo?: boolean;\n canRedo?: boolean;\n onLayout?: () => void;\n position?: PanelPosition;\n}\n\ninterface IControlButtonProps {\n icon: SymbolCodepoints;\n label: string;\n onClick: () => void;\n disabled?: boolean;\n}\n\nconst ControlButton: React.FC<IControlButtonProps> = ({ icon, label, onClick, disabled }) => (\n <ControlledTooltip content={label} placement=\"right\">\n <ControlledButton\n defaultHtmlButton\n onClick={onClick}\n disabled={disabled}\n className=\"interactive flex size-8 items-center justify-center rounded-lg text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\"\n >\n <MaterialSymbol icon={icon} size={18} aria-hidden />\n </ControlledButton>\n </ControlledTooltip>\n);\n\n/** Zoom, fit and history controls drawn with the library's own buttons. */\nconst FlowZoomControls: React.FC<IFlowZoomControlsProps> = ({\n onUndo,\n onRedo,\n canUndo,\n canRedo,\n onLayout,\n position = 'top-left',\n}) => {\n const { t } = useTranslation('ui');\n const { zoomIn, zoomOut, fitView } = useReactFlow();\n\n return (\n <Panel\n position={position}\n className=\"flex flex-col gap-y-1 rounded-xl border border-border bg-surface p-1 shadow-sm\"\n >\n <ControlButton icon=\"add\" label={t('flow.controls.zoomIn')} onClick={() => void zoomIn()} />\n <ControlButton icon=\"remove\" label={t('flow.controls.zoomOut')} onClick={() => void zoomOut()} />\n <ControlButton\n icon=\"fit_screen\"\n label={t('flow.controls.fitView')}\n onClick={() => void fitView({ padding: 0.2 })}\n />\n {onLayout && <ControlButton icon=\"account_tree\" label={t('flow.controls.layout')} onClick={onLayout} />}\n {onUndo && <ControlButton icon=\"undo\" label={t('flow.controls.undo')} onClick={onUndo} disabled={!canUndo} />}\n {onRedo && <ControlButton icon=\"redo\" label={t('flow.controls.redo')} onClick={onRedo} disabled={!canRedo} />}\n </Panel>\n );\n};\n\nexport default FlowZoomControls;\n"],"names":[],"mappings":";;;;;;AAuBA,MAAM,gBAA+C,CAAC,EAAE,MAAM,OAAO,SAAS,SAAA,MAC5E,oBAAC,mBAAA,EAAkB,SAAS,OAAO,WAAU,SAC3C,UAAA;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,mBAAiB;AAAA,IACjB;AAAA,IACA;AAAA,IACA,WAAU;AAAA,IAEV,8BAAC,gBAAA,EAAe,MAAY,MAAM,IAAI,eAAW,KAAA,CAAC;AAAA,EAAA;AACpD,GACF;AAIF,MAAM,mBAAqD,CAAC;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AACb,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,EAAE,QAAQ,SAAS,QAAA,IAAY,aAAA;AAErC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MAEV,UAAA;AAAA,QAAA,oBAAC,eAAA,EAAc,MAAK,OAAM,OAAO,EAAE,sBAAsB,GAAG,SAAS,MAAM,KAAK,OAAA,EAAO,CAAG;AAAA,QAC1F,oBAAC,eAAA,EAAc,MAAK,UAAS,OAAO,EAAE,uBAAuB,GAAG,SAAS,MAAM,KAAK,QAAA,EAAQ,CAAG;AAAA,QAC/F;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,OAAO,EAAE,uBAAuB;AAAA,YAChC,SAAS,MAAM,KAAK,QAAQ,EAAE,SAAS,KAAK;AAAA,UAAA;AAAA,QAAA;AAAA,QAE7C,YAAY,oBAAC,eAAA,EAAc,MAAK,gBAAe,OAAO,EAAE,sBAAsB,GAAG,SAAS,SAAA,CAAU;AAAA,QACpG,UAAU,oBAAC,eAAA,EAAc,MAAK,QAAO,OAAO,EAAE,oBAAoB,GAAG,SAAS,QAAQ,UAAU,CAAC,QAAA,CAAS;AAAA,QAC1G,UAAU,oBAAC,eAAA,EAAc,MAAK,QAAO,OAAO,EAAE,oBAAoB,GAAG,SAAS,QAAQ,UAAU,CAAC,QAAA,CAAS;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGjH;"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { IFlowBlockDefinition, TFlowPortId } from './flowTypes';
|
|
2
|
+
export interface IFlowBlockRegistry {
|
|
3
|
+
list: Array<IFlowBlockDefinition>;
|
|
4
|
+
get: (type: string) => IFlowBlockDefinition | undefined;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Erases the config type so blocks with different configs live in one registry. The
|
|
8
|
+
* canvas only hands each block the config it created, so the cast never crosses blocks.
|
|
9
|
+
*/
|
|
10
|
+
export declare const defineFlowBlock: <TConfig>(definition: IFlowBlockDefinition<TConfig>) => IFlowBlockDefinition;
|
|
11
|
+
export declare const createFlowBlockRegistry: (definitions: Array<IFlowBlockDefinition>) => IFlowBlockRegistry;
|
|
12
|
+
export declare const flowOutputsOf: (definition: IFlowBlockDefinition | undefined, config: unknown) => Array<TFlowPortId>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const defineFlowBlock = (definition) => definition;
|
|
2
|
+
const createFlowBlockRegistry = (definitions) => {
|
|
3
|
+
const safeDefinitions = Array.isArray(definitions) ? definitions : [];
|
|
4
|
+
const byType = new Map(safeDefinitions.map((definition) => [definition.type, definition]));
|
|
5
|
+
return {
|
|
6
|
+
list: safeDefinitions,
|
|
7
|
+
get: (type) => byType.get(type)
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
const flowOutputsOf = (definition, config) => {
|
|
11
|
+
if (!definition) return [];
|
|
12
|
+
const resolved = definition.resolveOutputs?.(config);
|
|
13
|
+
return Array.isArray(resolved) ? resolved : definition.outputs;
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
createFlowBlockRegistry,
|
|
17
|
+
defineFlowBlock,
|
|
18
|
+
flowOutputsOf
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=flowBlockRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flowBlockRegistry.js","sources":["../../../../src/components/UI/Flow/flowBlockRegistry.ts"],"sourcesContent":["import type { IFlowBlockDefinition, TFlowPortId } from './flowTypes';\n\nexport interface IFlowBlockRegistry {\n list: Array<IFlowBlockDefinition>;\n get: (type: string) => IFlowBlockDefinition | undefined;\n}\n\n/**\n * Erases the config type so blocks with different configs live in one registry. The\n * canvas only hands each block the config it created, so the cast never crosses blocks.\n */\nexport const defineFlowBlock = <TConfig>(definition: IFlowBlockDefinition<TConfig>): IFlowBlockDefinition =>\n definition as unknown as IFlowBlockDefinition;\n\nexport const createFlowBlockRegistry = (definitions: Array<IFlowBlockDefinition>): IFlowBlockRegistry => {\n const safeDefinitions = Array.isArray(definitions) ? definitions : [];\n const byType = new Map(safeDefinitions.map((definition) => [definition.type, definition]));\n\n return {\n list: safeDefinitions,\n get: (type) => byType.get(type),\n };\n};\n\nexport const flowOutputsOf = (definition: IFlowBlockDefinition | undefined, config: unknown): Array<TFlowPortId> => {\n if (!definition) return [];\n const resolved = definition.resolveOutputs?.(config);\n return Array.isArray(resolved) ? resolved : definition.outputs;\n};\n"],"names":[],"mappings":"AAWO,MAAM,kBAAkB,CAAU,eACvC;AAEK,MAAM,0BAA0B,CAAC,gBAAiE;AACvG,QAAM,kBAAkB,MAAM,QAAQ,WAAW,IAAI,cAAc,CAAA;AACnE,QAAM,SAAS,IAAI,IAAI,gBAAgB,IAAI,CAAC,eAAe,CAAC,WAAW,MAAM,UAAU,CAAC,CAAC;AAEzF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,KAAK,CAAC,SAAS,OAAO,IAAI,IAAI;AAAA,EAAA;AAElC;AAEO,MAAM,gBAAgB,CAAC,YAA8C,WAAwC;AAClH,MAAI,CAAC,WAAY,QAAO,CAAA;AACxB,QAAM,WAAW,WAAW,iBAAiB,MAAM;AACnD,SAAO,MAAM,QAAQ,QAAQ,IAAI,WAAW,WAAW;AACzD;"}
|