@umodeapporg/ui 0.22.3 → 0.23.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.
- package/dist/assets/umode-ui.css +105 -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 +22 -0
- package/dist/components/UI/Flow/FlowCanvas.js +263 -0
- package/dist/components/UI/Flow/FlowCanvas.js.map +1 -0
- package/dist/components/UI/Flow/FlowConnectionEdge.d.ts +10 -0
- package/dist/components/UI/Flow/FlowConnectionEdge.js +54 -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 +124 -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/FlowPalette.d.ts +7 -0
- package/dist/components/UI/Flow/FlowPalette.js +90 -0
- package/dist/components/UI/Flow/FlowPalette.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 +15 -0
- package/dist/components/UI/Flow/flowBlockRegistry.js.map +1 -0
- package/dist/components/UI/Flow/flowConstants.d.ts +2 -0
- package/dist/components/UI/Flow/flowConstants.js +5 -0
- package/dist/components/UI/Flow/flowConstants.js.map +1 -0
- package/dist/components/UI/Flow/flowEditorStore.d.ts +44 -0
- package/dist/components/UI/Flow/flowEditorStore.js +258 -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 +156 -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 +14 -0
- package/dist/components/UI/Flow/index.js +43 -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 +67 -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 +134 -0
- package/dist/locales/pt/ui.json.d.ts +67 -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,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,7 @@
|
|
|
1
|
+
interface IFlowPaletteProps {
|
|
2
|
+
readOnly?: boolean;
|
|
3
|
+
className?: string;
|
|
4
|
+
}
|
|
5
|
+
/** The blocks a flow can use, grouped by category. Drag one onto the canvas, or click to add it below the selection. */
|
|
6
|
+
declare const FlowPalette: React.FC<IFlowPaletteProps>;
|
|
7
|
+
export default FlowPalette;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo } from "react";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import { twMerge } from "tailwind-merge";
|
|
5
|
+
import ControlledButton from "../../Form/ControlledButton.js";
|
|
6
|
+
import UnifiedSearchBar from "../../Search/UnifiedSearchBar.js";
|
|
7
|
+
import { MaterialSymbol } from "../MaterialSymbol/index.js";
|
|
8
|
+
import NoDataFound from "../NoDataFound/index.js";
|
|
9
|
+
import { FLOW_BLOCK_DRAG_TYPE } from "./flowConstants.js";
|
|
10
|
+
import { useFlowBlockRegistry, useFlowEditorStore, useFlowEditor } from "./FlowEditorProvider.js";
|
|
11
|
+
import { useFlowLabels } from "./useFlowLabels.js";
|
|
12
|
+
const CATEGORY_ORDER = ["trigger", "condition", "action"];
|
|
13
|
+
const FlowPalette = ({ readOnly = false, className }) => {
|
|
14
|
+
const { t } = useTranslation("ui");
|
|
15
|
+
const labels = useFlowLabels();
|
|
16
|
+
const registry = useFlowBlockRegistry();
|
|
17
|
+
const store = useFlowEditorStore();
|
|
18
|
+
const nodes = useFlowEditor((state) => state.graph.nodes);
|
|
19
|
+
const [visibleBlocks, setVisibleBlocks] = useState(registry.list);
|
|
20
|
+
const instancesByType = useMemo(() => {
|
|
21
|
+
const counts = /* @__PURE__ */ new Map();
|
|
22
|
+
nodes.forEach((node) => counts.set(node.type, (counts.get(node.type) ?? 0) + 1));
|
|
23
|
+
return counts;
|
|
24
|
+
}, [nodes]);
|
|
25
|
+
const startDrag = (event, type) => {
|
|
26
|
+
event.dataTransfer.setData(FLOW_BLOCK_DRAG_TYPE, type);
|
|
27
|
+
event.dataTransfer.effectAllowed = "copy";
|
|
28
|
+
};
|
|
29
|
+
return /* @__PURE__ */ jsxs(
|
|
30
|
+
"aside",
|
|
31
|
+
{
|
|
32
|
+
className: twMerge(
|
|
33
|
+
"flex h-full min-h-0 flex-col gap-y-3 rounded-xl border border-border bg-surface p-3",
|
|
34
|
+
className
|
|
35
|
+
),
|
|
36
|
+
children: [
|
|
37
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-foreground", children: t("flow.palette.title") }),
|
|
38
|
+
/* @__PURE__ */ jsx(
|
|
39
|
+
UnifiedSearchBar,
|
|
40
|
+
{
|
|
41
|
+
mode: "default",
|
|
42
|
+
label: t("flow.palette.search"),
|
|
43
|
+
searchableItems: registry.list,
|
|
44
|
+
searchKeys: ["label", "description"],
|
|
45
|
+
setFilteredItems: setVisibleBlocks,
|
|
46
|
+
minSearchLength: 1,
|
|
47
|
+
debounceTime: 150
|
|
48
|
+
}
|
|
49
|
+
),
|
|
50
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-y-4 overflow-y-auto", children: [
|
|
51
|
+
visibleBlocks.length === 0 && /* @__PURE__ */ jsx(NoDataFound, { compact: true, variant: "search", message: t("flow.palette.empty") }),
|
|
52
|
+
CATEGORY_ORDER.map((category) => {
|
|
53
|
+
const blocks = visibleBlocks.filter((block) => block.category === category);
|
|
54
|
+
if (blocks.length === 0) return null;
|
|
55
|
+
return /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-y-1", children: [
|
|
56
|
+
/* @__PURE__ */ jsx("span", { className: "text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: labels.categories[category] }),
|
|
57
|
+
blocks.map((block) => {
|
|
58
|
+
const limitReached = block.maxInstances !== void 0 && (instancesByType.get(block.type) ?? 0) >= block.maxInstances;
|
|
59
|
+
const disabled = readOnly || limitReached;
|
|
60
|
+
return /* @__PURE__ */ jsxs(
|
|
61
|
+
ControlledButton,
|
|
62
|
+
{
|
|
63
|
+
defaultHtmlButton: true,
|
|
64
|
+
draggable: !disabled,
|
|
65
|
+
disabled,
|
|
66
|
+
onDragStart: (event) => startDrag(event, block.type),
|
|
67
|
+
onClick: () => store.getState().addNode(block.type),
|
|
68
|
+
className: "interactive flex items-start gap-x-2 rounded-lg border border-transparent px-2 py-2 text-left hover:border-border hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-60",
|
|
69
|
+
children: [
|
|
70
|
+
/* @__PURE__ */ jsx("span", { className: "flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-foreground-muted", children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: block.icon, size: 16, "aria-hidden": true }) }),
|
|
71
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col", children: [
|
|
72
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: block.label }),
|
|
73
|
+
limitReached ? /* @__PURE__ */ jsx("span", { className: "text-xs text-foreground-subtle", children: t("flow.palette.limitReached") }) : block.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-foreground-subtle", children: block.description })
|
|
74
|
+
] })
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
block.type
|
|
78
|
+
);
|
|
79
|
+
})
|
|
80
|
+
] }, category);
|
|
81
|
+
})
|
|
82
|
+
] })
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
export {
|
|
88
|
+
FlowPalette as default
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=FlowPalette.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlowPalette.js","sources":["../../../../src/components/UI/Flow/FlowPalette.tsx"],"sourcesContent":["import { DragEvent, useMemo, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport UnifiedSearchBar from '@/components/Search/UnifiedSearchBar';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\n\nimport { FLOW_BLOCK_DRAG_TYPE } from './flowConstants';\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport type { IFlowBlockDefinition, TFlowBlockCategory } from './flowTypes';\nimport { useFlowLabels } from './useFlowLabels';\n\nconst CATEGORY_ORDER: Array<TFlowBlockCategory> = ['trigger', 'condition', 'action'];\n\ninterface IFlowPaletteProps {\n readOnly?: boolean;\n className?: string;\n}\n\n/** The blocks a flow can use, grouped by category. Drag one onto the canvas, or click to add it below the selection. */\nconst FlowPalette: React.FC<IFlowPaletteProps> = ({ readOnly = false, className }) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n const nodes = useFlowEditor((state) => state.graph.nodes);\n const [visibleBlocks, setVisibleBlocks] = useState<Array<IFlowBlockDefinition>>(registry.list);\n\n const instancesByType = useMemo(() => {\n const counts = new Map<string, number>();\n nodes.forEach((node) => counts.set(node.type, (counts.get(node.type) ?? 0) + 1));\n return counts;\n }, [nodes]);\n\n const startDrag = (event: DragEvent<HTMLButtonElement>, type: string) => {\n event.dataTransfer.setData(FLOW_BLOCK_DRAG_TYPE, type);\n event.dataTransfer.effectAllowed = 'copy';\n };\n\n return (\n <aside\n className={twMerge(\n 'flex h-full min-h-0 flex-col gap-y-3 rounded-xl border border-border bg-surface p-3',\n className,\n )}\n >\n <span className=\"text-sm font-semibold text-foreground\">{t('flow.palette.title')}</span>\n <UnifiedSearchBar<IFlowBlockDefinition>\n mode=\"default\"\n label={t('flow.palette.search')}\n searchableItems={registry.list}\n searchKeys={['label', 'description']}\n setFilteredItems={setVisibleBlocks}\n minSearchLength={1}\n debounceTime={150}\n />\n <div className=\"flex min-h-0 flex-1 flex-col gap-y-4 overflow-y-auto\">\n {visibleBlocks.length === 0 && <NoDataFound compact variant=\"search\" message={t('flow.palette.empty')} />}\n {CATEGORY_ORDER.map((category) => {\n const blocks = visibleBlocks.filter((block) => block.category === category);\n if (blocks.length === 0) return null;\n return (\n <section key={category} className=\"flex flex-col gap-y-1\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {labels.categories[category]}\n </span>\n {blocks.map((block) => {\n const limitReached =\n block.maxInstances !== undefined && (instancesByType.get(block.type) ?? 0) >= block.maxInstances;\n const disabled = readOnly || limitReached;\n return (\n <ControlledButton\n key={block.type}\n defaultHtmlButton\n draggable={!disabled}\n disabled={disabled}\n onDragStart={(event) => startDrag(event, block.type)}\n onClick={() => store.getState().addNode(block.type)}\n className=\"interactive flex items-start gap-x-2 rounded-lg border border-transparent px-2 py-2 text-left hover:border-border hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-60\"\n >\n <span className=\"flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-foreground-muted\">\n <MaterialSymbol icon={block.icon} size={16} aria-hidden />\n </span>\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"text-sm font-medium text-foreground\">{block.label}</span>\n {limitReached ? (\n <span className=\"text-xs text-foreground-subtle\">{t('flow.palette.limitReached')}</span>\n ) : (\n block.description && <span className=\"text-xs text-foreground-subtle\">{block.description}</span>\n )}\n </span>\n </ControlledButton>\n );\n })}\n </section>\n );\n })}\n </div>\n </aside>\n );\n};\n\nexport default FlowPalette;\n"],"names":[],"mappings":"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"}
|
|
@@ -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 and the handle hides: stacked under the canvas there is nothing to resize against.
|
|
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 and the handle hides: stacked under the canvas there is nothing to resize against.\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, TFlowBlockCategory } from './flowTypes';
|
|
2
|
+
export interface IFlowBlockRegistry {
|
|
3
|
+
list: Array<IFlowBlockDefinition>;
|
|
4
|
+
get: (type: string) => IFlowBlockDefinition | undefined;
|
|
5
|
+
byCategory: (category: TFlowBlockCategory) => Array<IFlowBlockDefinition>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Erases the config type so blocks with different configs live in one registry. The
|
|
9
|
+
* canvas only hands each block the config it created, so the cast never crosses blocks.
|
|
10
|
+
*/
|
|
11
|
+
export declare const defineFlowBlock: <TConfig>(definition: IFlowBlockDefinition<TConfig>) => IFlowBlockDefinition;
|
|
12
|
+
export declare const createFlowBlockRegistry: (definitions: Array<IFlowBlockDefinition>) => IFlowBlockRegistry;
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
byCategory: (category) => safeDefinitions.filter((definition) => definition.category === category)
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export {
|
|
12
|
+
createFlowBlockRegistry,
|
|
13
|
+
defineFlowBlock
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=flowBlockRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flowBlockRegistry.js","sources":["../../../../src/components/UI/Flow/flowBlockRegistry.ts"],"sourcesContent":["import type { IFlowBlockDefinition, TFlowBlockCategory } from './flowTypes';\n\nexport interface IFlowBlockRegistry {\n list: Array<IFlowBlockDefinition>;\n get: (type: string) => IFlowBlockDefinition | undefined;\n byCategory: (category: TFlowBlockCategory) => Array<IFlowBlockDefinition>;\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 byCategory: (category) => safeDefinitions.filter((definition) => definition.category === category),\n };\n};\n"],"names":[],"mappings":"AAYO,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,IAC9B,YAAY,CAAC,aAAa,gBAAgB,OAAO,CAAC,eAAe,WAAW,aAAa,QAAQ;AAAA,EAAA;AAErG;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flowConstants.js","sources":["../../../../src/components/UI/Flow/flowConstants.ts"],"sourcesContent":["/** The MIME type a palette item puts on the drag so the canvas knows which block was dropped. */\nexport const FLOW_BLOCK_DRAG_TYPE = 'application/x-umode-flow-block';\n"],"names":[],"mappings":"AACO,MAAM,uBAAuB;"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { StoreApi } from 'zustand/vanilla';
|
|
2
|
+
import { IFlowBlockRegistry } from './flowBlockRegistry';
|
|
3
|
+
import { IFlowGraph, IFlowNode, IFlowPosition, TFlowPortId } from './flowTypes';
|
|
4
|
+
export interface IFlowEditorState {
|
|
5
|
+
graph: IFlowGraph;
|
|
6
|
+
selectedNodeIds: Array<string>;
|
|
7
|
+
selectedEdgeIds: Array<string>;
|
|
8
|
+
past: Array<IFlowGraph>;
|
|
9
|
+
future: Array<IFlowGraph>;
|
|
10
|
+
clipboard: Array<IFlowNode>;
|
|
11
|
+
isDirty: boolean;
|
|
12
|
+
replaceGraph: (graph: IFlowGraph, options?: {
|
|
13
|
+
keepHistory?: boolean;
|
|
14
|
+
}) => void;
|
|
15
|
+
markClean: () => void;
|
|
16
|
+
/** Snapshot before a gesture that changes the graph in many small steps (a drag). */
|
|
17
|
+
recordHistory: () => void;
|
|
18
|
+
addNode: (type: string, position?: IFlowPosition) => string | null;
|
|
19
|
+
/** Adds a block right of `sourceNodeId` and wires `sourcePort` to it in one undo step. */
|
|
20
|
+
addNodeFromPort: (type: string, sourceNodeId: string, sourcePort: TFlowPortId) => string | null;
|
|
21
|
+
updateNodeConfig: (nodeId: string, config: unknown) => void;
|
|
22
|
+
updateNodeLabel: (nodeId: string, label: string) => void;
|
|
23
|
+
moveNodes: (positions: Record<string, IFlowPosition>) => void;
|
|
24
|
+
connect: (source: string, sourcePort: TFlowPortId, target: string) => boolean;
|
|
25
|
+
removeEdges: (edgeIds: Array<string>) => void;
|
|
26
|
+
removeSelected: () => void;
|
|
27
|
+
duplicateSelected: () => void;
|
|
28
|
+
copySelected: () => void;
|
|
29
|
+
paste: () => void;
|
|
30
|
+
setSelection: (selection: {
|
|
31
|
+
nodeIds?: Array<string>;
|
|
32
|
+
edgeIds?: Array<string>;
|
|
33
|
+
}) => void;
|
|
34
|
+
undo: () => void;
|
|
35
|
+
redo: () => void;
|
|
36
|
+
/** The host may rebuild its registry (labels after a language switch); gestures always read the latest. */
|
|
37
|
+
setRegistry: (registry: IFlowBlockRegistry) => void;
|
|
38
|
+
}
|
|
39
|
+
export type TFlowEditorStore = StoreApi<IFlowEditorState>;
|
|
40
|
+
/**
|
|
41
|
+
* One store per editor instance (two canvases on a page never share state). The graph
|
|
42
|
+
* is the source of truth; the canvas only renders it and reports gestures back.
|
|
43
|
+
*/
|
|
44
|
+
export declare const createFlowEditorStore: (initialGraph: IFlowGraph, registry: IFlowBlockRegistry) => TFlowEditorStore;
|