@umodeapporg/ui 0.23.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/assets/umode-ui.css +100 -0
  2. package/dist/components/UI/Flow/FlowBlockNode.d.ts +35 -0
  3. package/dist/components/UI/Flow/FlowBlockNode.js +84 -0
  4. package/dist/components/UI/Flow/FlowBlockNode.js.map +1 -0
  5. package/dist/components/UI/Flow/FlowCanvas.d.ts +21 -0
  6. package/dist/components/UI/Flow/FlowCanvas.js +252 -0
  7. package/dist/components/UI/Flow/FlowCanvas.js.map +1 -0
  8. package/dist/components/UI/Flow/FlowConnectionEdge.d.ts +18 -0
  9. package/dist/components/UI/Flow/FlowConnectionEdge.js +115 -0
  10. package/dist/components/UI/Flow/FlowConnectionEdge.js.map +1 -0
  11. package/dist/components/UI/Flow/FlowEditorProvider.d.ts +17 -0
  12. package/dist/components/UI/Flow/FlowEditorProvider.js +27 -0
  13. package/dist/components/UI/Flow/FlowEditorProvider.js.map +1 -0
  14. package/dist/components/UI/Flow/FlowInspector.d.ts +11 -0
  15. package/dist/components/UI/Flow/FlowInspector.js +124 -0
  16. package/dist/components/UI/Flow/FlowInspector.js.map +1 -0
  17. package/dist/components/UI/Flow/FlowIssueList.d.ts +8 -0
  18. package/dist/components/UI/Flow/FlowIssueList.js +55 -0
  19. package/dist/components/UI/Flow/FlowIssueList.js.map +1 -0
  20. package/dist/components/UI/Flow/FlowLaneMap.d.ts +30 -0
  21. package/dist/components/UI/Flow/FlowLaneMap.js +206 -0
  22. package/dist/components/UI/Flow/FlowLaneMap.js.map +1 -0
  23. package/dist/components/UI/Flow/FlowNodeShell.d.ts +20 -0
  24. package/dist/components/UI/Flow/FlowNodeShell.js +81 -0
  25. package/dist/components/UI/Flow/FlowNodeShell.js.map +1 -0
  26. package/dist/components/UI/Flow/FlowResizablePanel.d.ts +20 -0
  27. package/dist/components/UI/Flow/FlowResizablePanel.js +117 -0
  28. package/dist/components/UI/Flow/FlowResizablePanel.js.map +1 -0
  29. package/dist/components/UI/Flow/FlowZoomControls.d.ts +12 -0
  30. package/dist/components/UI/Flow/FlowZoomControls.js +53 -0
  31. package/dist/components/UI/Flow/FlowZoomControls.js.map +1 -0
  32. package/dist/components/UI/Flow/flowBlockRegistry.d.ts +12 -0
  33. package/dist/components/UI/Flow/flowBlockRegistry.js +20 -0
  34. package/dist/components/UI/Flow/flowBlockRegistry.js.map +1 -0
  35. package/dist/components/UI/Flow/flowEditorStore.d.ts +49 -0
  36. package/dist/components/UI/Flow/flowEditorStore.js +301 -0
  37. package/dist/components/UI/Flow/flowEditorStore.js.map +1 -0
  38. package/dist/components/UI/Flow/flowGraph.d.ts +31 -0
  39. package/dist/components/UI/Flow/flowGraph.js +155 -0
  40. package/dist/components/UI/Flow/flowGraph.js.map +1 -0
  41. package/dist/components/UI/Flow/flowTypes.d.ts +72 -0
  42. package/dist/components/UI/Flow/flowTypes.js +2 -0
  43. package/dist/components/UI/Flow/flowTypes.js.map +1 -0
  44. package/dist/components/UI/Flow/index.d.ts +12 -0
  45. package/dist/components/UI/Flow/index.js +39 -0
  46. package/dist/components/UI/Flow/index.js.map +1 -0
  47. package/dist/components/UI/Flow/useFlowLabels.d.ts +10 -0
  48. package/dist/components/UI/Flow/useFlowLabels.js +51 -0
  49. package/dist/components/UI/Flow/useFlowLabels.js.map +1 -0
  50. package/dist/locales/en/ui.json.d.ts +64 -0
  51. package/dist/locales/en/ui.json.js +3 -0
  52. package/dist/locales/en/ui.json.js.map +1 -1
  53. package/dist/locales/index.d.ts +128 -0
  54. package/dist/locales/pt/ui.json.d.ts +64 -0
  55. package/dist/locales/pt/ui.json.js +3 -0
  56. package/dist/locales/pt/ui.json.js.map +1 -1
  57. package/package.json +10 -1
@@ -0,0 +1,115 @@
1
+ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import classNames from "classnames";
4
+ import { useTranslation } from "react-i18next";
5
+ import ControlledButton from "../../Form/ControlledButton.js";
6
+ import ControlledMenu from "../ControlledMenu.js";
7
+ import { MaterialSymbol } from "../MaterialSymbol/index.js";
8
+ import { getSmoothStepPath, BaseEdge, EdgeLabelRenderer } from "@xyflow/react";
9
+ import OutlinedChip from "../Chips/OutlinedChip.js";
10
+ const HOVER_STROKE_WIDTH = 24;
11
+ const FlowConnectionEdge = ({
12
+ id,
13
+ sourceX,
14
+ sourceY,
15
+ targetX,
16
+ targetY,
17
+ sourcePosition,
18
+ targetPosition,
19
+ data,
20
+ markerEnd,
21
+ selected
22
+ }) => {
23
+ const { t } = useTranslation("ui");
24
+ const [hovered, setHovered] = useState(false);
25
+ const [menuOpen, setMenuOpen] = useState(false);
26
+ const [path, labelX, labelY] = getSmoothStepPath({
27
+ sourceX,
28
+ sourceY,
29
+ targetX,
30
+ targetY,
31
+ sourcePosition,
32
+ targetPosition,
33
+ borderRadius: 12
34
+ });
35
+ const insertableBlocks = Array.isArray(data?.insertableBlocks) ? data.insertableBlocks : [];
36
+ const canInsert = Boolean(data?.onInsert) && insertableBlocks.length > 0;
37
+ const showInsert = canInsert && (hovered || Boolean(selected) || menuOpen);
38
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
39
+ /* @__PURE__ */ jsx(
40
+ BaseEdge,
41
+ {
42
+ id,
43
+ path,
44
+ markerEnd,
45
+ className: classNames("umode-flow-edge-path", {
46
+ "umode-flow-edge-path--selected": selected,
47
+ "umode-flow-edge-path--traversed": data?.traversed,
48
+ "umode-flow-edge-path--error": data?.issueSeverity === "error",
49
+ "umode-flow-edge-path--warning": data?.issueSeverity === "warning"
50
+ })
51
+ }
52
+ ),
53
+ canInsert && /* @__PURE__ */ jsx(
54
+ "path",
55
+ {
56
+ d: path,
57
+ fill: "none",
58
+ stroke: "transparent",
59
+ strokeWidth: HOVER_STROKE_WIDTH,
60
+ onMouseEnter: () => setHovered(true),
61
+ onMouseLeave: () => setHovered(false)
62
+ }
63
+ ),
64
+ (data?.label || canInsert) && /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs(
65
+ "div",
66
+ {
67
+ className: classNames(
68
+ "nodrag nopan absolute flex items-center gap-x-1",
69
+ canInsert ? "pointer-events-auto" : "pointer-events-none"
70
+ ),
71
+ style: { transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` },
72
+ onMouseEnter: () => setHovered(true),
73
+ onMouseLeave: () => setHovered(false),
74
+ children: [
75
+ data?.label && /* @__PURE__ */ jsx(OutlinedChip, { label: data.label, tone: "neutral", variant: "outlined", size: "xs", preserveLabelCase: true }),
76
+ canInsert && /* @__PURE__ */ jsx(
77
+ "div",
78
+ {
79
+ className: classNames(
80
+ "transition-opacity duration-fast focus-within:opacity-100",
81
+ showInsert ? "opacity-100" : "opacity-0"
82
+ ),
83
+ children: /* @__PURE__ */ jsx(
84
+ ControlledMenu,
85
+ {
86
+ placement: "bottom",
87
+ triggerTooltip: t("flow.edge.insertBlock"),
88
+ onOpenChange: setMenuOpen,
89
+ items: insertableBlocks.map((block) => ({
90
+ key: block.type,
91
+ label: block.label,
92
+ icon: block.icon,
93
+ onClick: () => data?.onInsert?.(block.type)
94
+ })),
95
+ trigger: /* @__PURE__ */ jsx(
96
+ ControlledButton,
97
+ {
98
+ defaultHtmlButton: true,
99
+ className: "interactive flex size-6 items-center justify-center rounded-full border border-dashed border-brand-border bg-surface text-brand-emphasis shadow-sm hover:bg-brand-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
100
+ children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "add", size: 16, "aria-hidden": true })
101
+ }
102
+ )
103
+ }
104
+ )
105
+ }
106
+ )
107
+ ]
108
+ }
109
+ ) })
110
+ ] });
111
+ };
112
+ export {
113
+ FlowConnectionEdge as default
114
+ };
115
+ //# sourceMappingURL=FlowConnectionEdge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FlowConnectionEdge.js","sources":["../../../../src/components/UI/Flow/FlowConnectionEdge.tsx"],"sourcesContent":["import { useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledMenu from '@/components/UI/ControlledMenu';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { BaseEdge, type Edge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath } from '@xyflow/react';\n\nimport OutlinedChip from '../Chips/OutlinedChip';\n\nimport type { IFlowAddableBlock } from './FlowBlockNode';\nimport type { TFlowIssueSeverity } from './flowTypes';\n\nexport interface IFlowConnectionEdgeData extends Record<string, unknown> {\n label?: string;\n traversed?: boolean;\n issueSeverity?: TFlowIssueSeverity;\n /** What can go in the middle of this edge; empty in a read-only canvas. */\n insertableBlocks?: Array<IFlowAddableBlock>;\n onInsert?: (type: string) => void;\n}\n\nexport type TFlowConnectionEdge = Edge<IFlowConnectionEdgeData, 'flowConnection'>;\n\n/** Wide enough to hover a 1px line without hunting for it. */\nconst HOVER_STROKE_WIDTH = 24;\n\n/**\n * A smooth-step connection. Hovering it (or selecting it) shows a \"+\" at its middle that puts a\n * block between the two it connects, so a step can be added without rewiring by hand.\n */\nconst FlowConnectionEdge: React.FC<EdgeProps<TFlowConnectionEdge>> = ({\n id,\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n data,\n markerEnd,\n selected,\n}) => {\n const { t } = useTranslation('ui');\n const [hovered, setHovered] = useState(false);\n const [menuOpen, setMenuOpen] = useState(false);\n const [path, labelX, labelY] = getSmoothStepPath({\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n borderRadius: 12,\n });\n\n const insertableBlocks = Array.isArray(data?.insertableBlocks) ? data.insertableBlocks : [];\n const canInsert = Boolean(data?.onInsert) && insertableBlocks.length > 0;\n const showInsert = canInsert && (hovered || Boolean(selected) || menuOpen);\n\n return (\n <>\n <BaseEdge\n id={id}\n path={path}\n markerEnd={markerEnd}\n className={classNames('umode-flow-edge-path', {\n 'umode-flow-edge-path--selected': selected,\n 'umode-flow-edge-path--traversed': data?.traversed,\n 'umode-flow-edge-path--error': data?.issueSeverity === 'error',\n 'umode-flow-edge-path--warning': data?.issueSeverity === 'warning',\n })}\n />\n {canInsert && (\n <path\n d={path}\n fill=\"none\"\n stroke=\"transparent\"\n strokeWidth={HOVER_STROKE_WIDTH}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n />\n )}\n {(data?.label || canInsert) && (\n <EdgeLabelRenderer>\n <div\n className={classNames(\n 'nodrag nopan absolute flex items-center gap-x-1',\n canInsert ? 'pointer-events-auto' : 'pointer-events-none',\n )}\n style={{ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` }}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n >\n {data?.label && (\n <OutlinedChip label={data.label} tone=\"neutral\" variant=\"outlined\" size=\"xs\" preserveLabelCase />\n )}\n {canInsert && (\n <div\n className={classNames(\n 'transition-opacity duration-fast focus-within:opacity-100',\n showInsert ? 'opacity-100' : 'opacity-0',\n )}\n >\n <ControlledMenu\n placement=\"bottom\"\n triggerTooltip={t('flow.edge.insertBlock')}\n onOpenChange={setMenuOpen}\n items={insertableBlocks.map((block) => ({\n key: block.type,\n label: block.label,\n icon: block.icon,\n onClick: () => data?.onInsert?.(block.type),\n }))}\n trigger={\n <ControlledButton\n defaultHtmlButton\n className=\"interactive flex size-6 items-center justify-center rounded-full border border-dashed border-brand-border bg-surface text-brand-emphasis shadow-sm hover:bg-brand-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MaterialSymbol icon=\"add\" size={16} aria-hidden />\n </ControlledButton>\n }\n />\n </div>\n )}\n </div>\n </EdgeLabelRenderer>\n )}\n </>\n );\n};\n\nexport default FlowConnectionEdge;\n"],"names":[],"mappings":";;;;;;;;;AA0BA,MAAM,qBAAqB;AAM3B,MAAM,qBAA+D,CAAC;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,MAAM,QAAQ,MAAM,IAAI,kBAAkB;AAAA,IAC/C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,EAAA,CACf;AAED,QAAM,mBAAmB,MAAM,QAAQ,MAAM,gBAAgB,IAAI,KAAK,mBAAmB,CAAA;AACzF,QAAM,YAAY,QAAQ,MAAM,QAAQ,KAAK,iBAAiB,SAAS;AACvE,QAAM,aAAa,cAAc,WAAW,QAAQ,QAAQ,KAAK;AAEjE,SACE,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,WAAW,wBAAwB;AAAA,UAC5C,kCAAkC;AAAA,UAClC,mCAAmC,MAAM;AAAA,UACzC,+BAA+B,MAAM,kBAAkB;AAAA,UACvD,iCAAiC,MAAM,kBAAkB;AAAA,QAAA,CAC1D;AAAA,MAAA;AAAA,IAAA;AAAA,IAEF,aACC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,GAAG;AAAA,QACH,MAAK;AAAA,QACL,QAAO;AAAA,QACP,aAAa;AAAA,QACb,cAAc,MAAM,WAAW,IAAI;AAAA,QACnC,cAAc,MAAM,WAAW,KAAK;AAAA,MAAA;AAAA,IAAA;AAAA,KAGtC,MAAM,SAAS,cACf,oBAAC,mBAAA,EACC,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,YAAY,wBAAwB;AAAA,QAAA;AAAA,QAEtC,OAAO,EAAE,WAAW,mCAAmC,MAAM,OAAO,MAAM,MAAA;AAAA,QAC1E,cAAc,MAAM,WAAW,IAAI;AAAA,QACnC,cAAc,MAAM,WAAW,KAAK;AAAA,QAEnC,UAAA;AAAA,UAAA,MAAM,SACL,oBAAC,cAAA,EAAa,OAAO,KAAK,OAAO,MAAK,WAAU,SAAQ,YAAW,MAAK,MAAK,mBAAiB,MAAC;AAAA,UAEhG,aACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,aAAa,gBAAgB;AAAA,cAAA;AAAA,cAG/B,UAAA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,WAAU;AAAA,kBACV,gBAAgB,EAAE,uBAAuB;AAAA,kBACzC,cAAc;AAAA,kBACd,OAAO,iBAAiB,IAAI,CAAC,WAAW;AAAA,oBACtC,KAAK,MAAM;AAAA,oBACX,OAAO,MAAM;AAAA,oBACb,MAAM,MAAM;AAAA,oBACZ,SAAS,MAAM,MAAM,WAAW,MAAM,IAAI;AAAA,kBAAA,EAC1C;AAAA,kBACF,SACE;AAAA,oBAAC;AAAA,oBAAA;AAAA,sBACC,mBAAiB;AAAA,sBACjB,WAAU;AAAA,sBAEV,8BAAC,gBAAA,EAAe,MAAK,OAAM,MAAM,IAAI,eAAW,KAAA,CAAC;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBACnD;AAAA,cAAA;AAAA,YAEJ;AAAA,UAAA;AAAA,QACF;AAAA,MAAA;AAAA,IAAA,EAEJ,CACF;AAAA,EAAA,GAEJ;AAEJ;"}
@@ -0,0 +1,17 @@
1
+ import { ReactNode } from 'react';
2
+ import { IFlowBlockRegistry } from './flowBlockRegistry';
3
+ import { IFlowEditorState, TFlowEditorStore } from './flowEditorStore';
4
+ import { IFlowGraph } from './flowTypes';
5
+ interface IFlowEditorProviderProps {
6
+ /** Read once, when the provider mounts. Load a different flow with `replaceGraph` or a new `key`. */
7
+ initialGraph: IFlowGraph;
8
+ registry: IFlowBlockRegistry;
9
+ children: ReactNode;
10
+ }
11
+ /** Scopes one editor: its store, its block registry and the canvas viewport. */
12
+ declare const FlowEditorProvider: React.FC<IFlowEditorProviderProps>;
13
+ export declare const useFlowEditor: <TSelected>(selector: (state: IFlowEditorState) => TSelected) => TSelected;
14
+ /** The store itself, for reading state inside event handlers without subscribing. */
15
+ export declare const useFlowEditorStore: () => TFlowEditorStore;
16
+ export declare const useFlowBlockRegistry: () => IFlowBlockRegistry;
17
+ export default FlowEditorProvider;
@@ -0,0 +1,27 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useState, useMemo, useEffect, useContext } from "react";
3
+ import { useStore } from "zustand";
4
+ import { ReactFlowProvider } from "@xyflow/react";
5
+ import { createFlowEditorStore } from "./flowEditorStore.js";
6
+ const FlowEditorContext = createContext(null);
7
+ const FlowEditorProvider = ({ initialGraph, registry, children }) => {
8
+ const [store] = useState(() => createFlowEditorStore(initialGraph, registry));
9
+ const contextValue = useMemo(() => ({ store, registry }), [store, registry]);
10
+ useEffect(() => store.getState().setRegistry(registry), [store, registry]);
11
+ return /* @__PURE__ */ jsx(FlowEditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(ReactFlowProvider, { children }) });
12
+ };
13
+ const useFlowEditorContext = () => {
14
+ const context = useContext(FlowEditorContext);
15
+ if (!context) throw new Error("Flow editor components must be rendered inside FlowEditorProvider");
16
+ return context;
17
+ };
18
+ const useFlowEditor = (selector) => useStore(useFlowEditorContext().store, selector);
19
+ const useFlowEditorStore = () => useFlowEditorContext().store;
20
+ const useFlowBlockRegistry = () => useFlowEditorContext().registry;
21
+ export {
22
+ FlowEditorProvider as default,
23
+ useFlowBlockRegistry,
24
+ useFlowEditor,
25
+ useFlowEditorStore
26
+ };
27
+ //# sourceMappingURL=FlowEditorProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FlowEditorProvider.js","sources":["../../../../src/components/UI/Flow/FlowEditorProvider.tsx"],"sourcesContent":["import { createContext, ReactNode, useContext, useEffect, useMemo, useState } from 'react';\nimport { useStore } from 'zustand';\n\nimport { ReactFlowProvider } from '@xyflow/react';\n\nimport type { IFlowBlockRegistry } from './flowBlockRegistry';\nimport { createFlowEditorStore, IFlowEditorState, TFlowEditorStore } from './flowEditorStore';\nimport type { IFlowGraph } from './flowTypes';\n\ninterface IFlowEditorContextValue {\n store: TFlowEditorStore;\n registry: IFlowBlockRegistry;\n}\n\nconst FlowEditorContext = createContext<IFlowEditorContextValue | null>(null);\n\ninterface IFlowEditorProviderProps {\n /** Read once, when the provider mounts. Load a different flow with `replaceGraph` or a new `key`. */\n initialGraph: IFlowGraph;\n registry: IFlowBlockRegistry;\n children: ReactNode;\n}\n\n/** Scopes one editor: its store, its block registry and the canvas viewport. */\nconst FlowEditorProvider: React.FC<IFlowEditorProviderProps> = ({ initialGraph, registry, children }) => {\n const [store] = useState(() => createFlowEditorStore(initialGraph, registry));\n const contextValue = useMemo(() => ({ store, registry }), [store, registry]);\n\n useEffect(() => store.getState().setRegistry(registry), [store, registry]);\n\n return (\n <FlowEditorContext.Provider value={contextValue}>\n <ReactFlowProvider>{children}</ReactFlowProvider>\n </FlowEditorContext.Provider>\n );\n};\n\nconst useFlowEditorContext = (): IFlowEditorContextValue => {\n const context = useContext(FlowEditorContext);\n if (!context) throw new Error('Flow editor components must be rendered inside FlowEditorProvider');\n return context;\n};\n\nexport const useFlowEditor = <TSelected,>(selector: (state: IFlowEditorState) => TSelected): TSelected =>\n useStore(useFlowEditorContext().store, selector);\n\n/** The store itself, for reading state inside event handlers without subscribing. */\nexport const useFlowEditorStore = (): TFlowEditorStore => useFlowEditorContext().store;\n\nexport const useFlowBlockRegistry = (): IFlowBlockRegistry => useFlowEditorContext().registry;\n\nexport default FlowEditorProvider;\n"],"names":[],"mappings":";;;;;AAcA,MAAM,oBAAoB,cAA8C,IAAI;AAU5E,MAAM,qBAAyD,CAAC,EAAE,cAAc,UAAU,eAAe;AACvG,QAAM,CAAC,KAAK,IAAI,SAAS,MAAM,sBAAsB,cAAc,QAAQ,CAAC;AAC5E,QAAM,eAAe,QAAQ,OAAO,EAAE,OAAO,aAAa,CAAC,OAAO,QAAQ,CAAC;AAE3E,YAAU,MAAM,MAAM,WAAW,YAAY,QAAQ,GAAG,CAAC,OAAO,QAAQ,CAAC;AAEzE,SACE,oBAAC,kBAAkB,UAAlB,EAA2B,OAAO,cACjC,UAAA,oBAAC,mBAAA,EAAmB,SAAA,CAAS,EAAA,CAC/B;AAEJ;AAEA,MAAM,uBAAuB,MAA+B;AAC1D,QAAM,UAAU,WAAW,iBAAiB;AAC5C,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,mEAAmE;AACjG,SAAO;AACT;AAEO,MAAM,gBAAgB,CAAa,aACxC,SAAS,qBAAA,EAAuB,OAAO,QAAQ;AAG1C,MAAM,qBAAqB,MAAwB,uBAAuB;AAE1E,MAAM,uBAAuB,MAA0B,uBAAuB;"}
@@ -0,0 +1,11 @@
1
+ import { IFlowIssue } from './flowTypes';
2
+ interface IFlowInspectorProps {
3
+ issues?: Array<IFlowIssue>;
4
+ readOnly?: boolean;
5
+ /** Renders nothing while no block is selected, instead of the empty state. */
6
+ hideWhenEmpty?: boolean;
7
+ className?: string;
8
+ }
9
+ /** The side panel of the selected block: its name, its own settings panel and its issues. */
10
+ declare const FlowInspector: React.FC<IFlowInspectorProps>;
11
+ export default FlowInspector;
@@ -0,0 +1,124 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useTranslation } from "react-i18next";
3
+ import { twMerge } from "tailwind-merge";
4
+ import ControlledButton from "../../Form/ControlledButton.js";
5
+ import ControlledInput from "../../Form/ControlledInput.js";
6
+ import ControlledTooltip from "../ControlledTooltip.js";
7
+ import { MaterialSymbol } from "../MaterialSymbol/index.js";
8
+ import NoDataFound from "../NoDataFound/index.js";
9
+ import { useFlowBlockRegistry, useFlowEditorStore, useFlowEditor } from "./FlowEditorProvider.js";
10
+ import { useFlowLabels } from "./useFlowLabels.js";
11
+ const FlowInspector = ({
12
+ issues = [],
13
+ readOnly = false,
14
+ hideWhenEmpty = false,
15
+ className
16
+ }) => {
17
+ const { t } = useTranslation("ui");
18
+ const labels = useFlowLabels();
19
+ const registry = useFlowBlockRegistry();
20
+ const store = useFlowEditorStore();
21
+ const selectedNodeIds = useFlowEditor((state) => state.selectedNodeIds);
22
+ const selectedNode = useFlowEditor(
23
+ (state) => state.selectedNodeIds.length === 1 ? state.graph.nodes.find((node) => node.nodeId === state.selectedNodeIds[0]) : void 0
24
+ );
25
+ const shellClass = twMerge(
26
+ "flex h-full min-h-0 flex-col gap-y-3 rounded-xl border border-border bg-surface p-4",
27
+ className
28
+ );
29
+ if (!selectedNode || selectedNodeIds.length !== 1) {
30
+ if (hideWhenEmpty) return null;
31
+ return /* @__PURE__ */ jsx("aside", { className: shellClass, "aria-label": t("flow.inspector.title"), children: /* @__PURE__ */ jsx(NoDataFound, { size: "sm", variant: "waiting", message: t("flow.inspector.empty") }) });
32
+ }
33
+ const definition = registry.get(selectedNode.type);
34
+ const nodeIssues = (Array.isArray(issues) ? issues : []).filter((issue) => issue.nodeId === selectedNode.nodeId);
35
+ const ConfigPanel = definition?.ConfigPanel;
36
+ return /* @__PURE__ */ jsxs("aside", { className: shellClass, "aria-label": t("flow.inspector.title"), children: [
37
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
38
+ /* @__PURE__ */ jsx("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground-muted", children: /* @__PURE__ */ jsx(
39
+ MaterialSymbol,
40
+ {
41
+ icon: definition?.resolveIcon?.(selectedNode.config) ?? definition?.icon ?? "help",
42
+ size: 18,
43
+ "aria-hidden": true
44
+ }
45
+ ) }),
46
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col", children: [
47
+ definition && /* @__PURE__ */ jsx("span", { className: "text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: labels.categories[definition.category] }),
48
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-foreground", children: definition?.resolveLabel?.(selectedNode.config) ?? definition?.label ?? selectedNode.type })
49
+ ] }),
50
+ /* @__PURE__ */ jsx(ControlledTooltip, { content: t("flow.inspector.close"), placement: "left", children: /* @__PURE__ */ jsx(
51
+ ControlledButton,
52
+ {
53
+ defaultHtmlButton: true,
54
+ onClick: () => store.getState().setSelection({ nodeIds: [], edgeIds: [] }),
55
+ className: "interactive ml-auto flex size-8 shrink-0 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",
56
+ children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "close", size: 18, "aria-hidden": true })
57
+ }
58
+ ) })
59
+ ] }),
60
+ /* @__PURE__ */ jsx(
61
+ ControlledInput,
62
+ {
63
+ label: t("flow.inspector.label"),
64
+ value: selectedNode.label ?? "",
65
+ disabled: readOnly,
66
+ onChange: (event) => store.getState().updateNodeLabel(selectedNode.nodeId, event.target.value)
67
+ }
68
+ ),
69
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-y-3 overflow-y-auto", children: [
70
+ ConfigPanel ? /* @__PURE__ */ jsx(
71
+ ConfigPanel,
72
+ {
73
+ config: selectedNode.config,
74
+ readOnly,
75
+ onChange: (next) => store.getState().updateNodeConfig(selectedNode.nodeId, next)
76
+ }
77
+ ) : /* @__PURE__ */ jsx(NoDataFound, { compact: true, variant: "error", message: t("flow.inspector.unknownBlock") }),
78
+ nodeIssues.length > 0 && /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-y-1", children: nodeIssues.map((issue) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-x-2", children: [
79
+ /* @__PURE__ */ jsx(
80
+ MaterialSymbol,
81
+ {
82
+ icon: issue.severity === "error" ? "error" : "warning",
83
+ size: 16,
84
+ className: issue.severity === "error" ? "text-danger-emphasis" : "text-warning-emphasis",
85
+ "aria-hidden": true
86
+ }
87
+ ),
88
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-foreground-muted", children: labels.issueMessage(issue) })
89
+ ] }, `${issue.code}|${issue.edgeId ?? ""}`)) })
90
+ ] }),
91
+ !readOnly && /* @__PURE__ */ jsxs("div", { className: "flex gap-x-2", children: [
92
+ definition?.category !== "trigger" && /* @__PURE__ */ jsxs(
93
+ ControlledButton,
94
+ {
95
+ size: "sm",
96
+ variant: "outlined",
97
+ tone: "neutral",
98
+ onClick: () => store.getState().duplicateSelected(),
99
+ children: [
100
+ /* @__PURE__ */ jsx(MaterialSymbol, { icon: "content_copy", size: 16, "aria-hidden": true }),
101
+ t("flow.inspector.duplicate")
102
+ ]
103
+ }
104
+ ),
105
+ /* @__PURE__ */ jsxs(
106
+ ControlledButton,
107
+ {
108
+ size: "sm",
109
+ variant: "outlined",
110
+ tone: "danger",
111
+ onClick: () => store.getState().removeSelected(),
112
+ children: [
113
+ /* @__PURE__ */ jsx(MaterialSymbol, { icon: "delete", size: 16, "aria-hidden": true }),
114
+ t("flow.inspector.delete")
115
+ ]
116
+ }
117
+ )
118
+ ] })
119
+ ] });
120
+ };
121
+ export {
122
+ FlowInspector as default
123
+ };
124
+ //# sourceMappingURL=FlowInspector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FlowInspector.js","sources":["../../../../src/components/UI/Flow/FlowInspector.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\n\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport type { IFlowIssue } from './flowTypes';\nimport { useFlowLabels } from './useFlowLabels';\n\ninterface IFlowInspectorProps {\n issues?: Array<IFlowIssue>;\n readOnly?: boolean;\n /** Renders nothing while no block is selected, instead of the empty state. */\n hideWhenEmpty?: boolean;\n className?: string;\n}\n\n/** The side panel of the selected block: its name, its own settings panel and its issues. */\nconst FlowInspector: React.FC<IFlowInspectorProps> = ({\n issues = [],\n readOnly = false,\n hideWhenEmpty = false,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n const selectedNodeIds = useFlowEditor((state) => state.selectedNodeIds);\n const selectedNode = useFlowEditor((state) =>\n state.selectedNodeIds.length === 1\n ? state.graph.nodes.find((node) => node.nodeId === state.selectedNodeIds[0])\n : undefined,\n );\n\n const shellClass = twMerge(\n 'flex h-full min-h-0 flex-col gap-y-3 rounded-xl border border-border bg-surface p-4',\n className,\n );\n\n if (!selectedNode || selectedNodeIds.length !== 1) {\n if (hideWhenEmpty) return null;\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\n <NoDataFound size=\"sm\" variant=\"waiting\" message={t('flow.inspector.empty')} />\n </aside>\n );\n }\n\n const definition = registry.get(selectedNode.type);\n const nodeIssues = (Array.isArray(issues) ? issues : []).filter((issue) => issue.nodeId === selectedNode.nodeId);\n const ConfigPanel = definition?.ConfigPanel;\n\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\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\n icon={definition?.resolveIcon?.(selectedNode.config) ?? definition?.icon ?? 'help'}\n size={18}\n aria-hidden\n />\n </span>\n <div className=\"flex min-w-0 flex-col\">\n {definition && (\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {labels.categories[definition.category]}\n </span>\n )}\n <span className=\"truncate text-sm font-semibold text-foreground\">\n {definition?.resolveLabel?.(selectedNode.config) ?? definition?.label ?? selectedNode.type}\n </span>\n </div>\n <ControlledTooltip content={t('flow.inspector.close')} placement=\"left\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => store.getState().setSelection({ nodeIds: [], edgeIds: [] })}\n className=\"interactive ml-auto flex size-8 shrink-0 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\"\n >\n <MaterialSymbol icon=\"close\" size={18} aria-hidden />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n\n <ControlledInput\n label={t('flow.inspector.label')}\n value={selectedNode.label ?? ''}\n disabled={readOnly}\n onChange={(event) => store.getState().updateNodeLabel(selectedNode.nodeId, event.target.value)}\n />\n\n <div className=\"flex min-h-0 flex-1 flex-col gap-y-3 overflow-y-auto\">\n {ConfigPanel ? (\n <ConfigPanel\n config={selectedNode.config}\n readOnly={readOnly}\n onChange={(next) => store.getState().updateNodeConfig(selectedNode.nodeId, next)}\n />\n ) : (\n <NoDataFound compact variant=\"error\" message={t('flow.inspector.unknownBlock')} />\n )}\n\n {nodeIssues.length > 0 && (\n <ul className=\"flex flex-col gap-y-1\">\n {nodeIssues.map((issue) => (\n <li key={`${issue.code}|${issue.edgeId ?? ''}`} className=\"flex items-start gap-x-2\">\n <MaterialSymbol\n icon={issue.severity === 'error' ? 'error' : 'warning'}\n size={16}\n className={issue.severity === 'error' ? 'text-danger-emphasis' : 'text-warning-emphasis'}\n aria-hidden\n />\n <span className=\"text-xs text-foreground-muted\">{labels.issueMessage(issue)}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n\n {!readOnly && (\n <div className=\"flex gap-x-2\">\n {definition?.category !== 'trigger' && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"neutral\"\n onClick={() => store.getState().duplicateSelected()}\n >\n <MaterialSymbol icon=\"content_copy\" size={16} aria-hidden />\n {t('flow.inspector.duplicate')}\n </ControlledButton>\n )}\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"danger\"\n onClick={() => store.getState().removeSelected()}\n >\n <MaterialSymbol icon=\"delete\" size={16} aria-hidden />\n {t('flow.inspector.delete')}\n </ControlledButton>\n </div>\n )}\n </aside>\n );\n};\n\nexport default FlowInspector;\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { IFlowIssue } from './flowTypes';
2
+ interface IFlowIssueListProps {
3
+ issues: Array<IFlowIssue>;
4
+ className?: string;
5
+ }
6
+ /** Every issue of the flow; clicking one selects and centers the block it points to. */
7
+ declare const FlowIssueList: React.FC<IFlowIssueListProps>;
8
+ export default FlowIssueList;
@@ -0,0 +1,55 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useTranslation } from "react-i18next";
3
+ import { twMerge } from "tailwind-merge";
4
+ import ControlledButton from "../../Form/ControlledButton.js";
5
+ import { MaterialSymbol } from "../MaterialSymbol/index.js";
6
+ import NoDataFound from "../NoDataFound/index.js";
7
+ import { useReactFlow } from "@xyflow/react";
8
+ import { useFlowEditorStore, useFlowEditor } from "./FlowEditorProvider.js";
9
+ import { useFlowLabels } from "./useFlowLabels.js";
10
+ const issueKey = (issue) => `${issue.code}|${issue.nodeId ?? ""}|${issue.edgeId ?? ""}`;
11
+ const FlowIssueList = ({ issues, className }) => {
12
+ const { t } = useTranslation("ui");
13
+ const labels = useFlowLabels();
14
+ const store = useFlowEditorStore();
15
+ const nodes = useFlowEditor((state) => state.graph.nodes);
16
+ const { setCenter } = useReactFlow();
17
+ const safeIssues = Array.isArray(issues) ? issues : [];
18
+ const uniqueIssues = safeIssues.filter(
19
+ (issue, index) => safeIssues.findIndex((other) => issueKey(other) === issueKey(issue)) === index
20
+ );
21
+ const focusNode = (nodeId) => {
22
+ const node = nodes.find((candidate) => candidate.nodeId === nodeId);
23
+ if (!node) return;
24
+ store.getState().setSelection({ nodeIds: [node.nodeId], edgeIds: [] });
25
+ void setCenter(node.position.x + 128, node.position.y + 48, { zoom: 1, duration: 200 });
26
+ };
27
+ return /* @__PURE__ */ jsxs("section", { className: twMerge("flex flex-col gap-y-2", className), children: [
28
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-foreground", children: t("flow.issues.title") }),
29
+ uniqueIssues.length === 0 ? /* @__PURE__ */ jsx(NoDataFound, { compact: true, variant: "success", message: t("flow.issues.none") }) : /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-y-1", children: uniqueIssues.map((issue) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
30
+ ControlledButton,
31
+ {
32
+ defaultHtmlButton: true,
33
+ disabled: !issue.nodeId,
34
+ onClick: () => focusNode(issue.nodeId),
35
+ className: "interactive flex w-full items-start gap-x-2 rounded-lg px-2 py-1.5 text-left hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default disabled:hover:bg-transparent",
36
+ children: [
37
+ /* @__PURE__ */ jsx(
38
+ MaterialSymbol,
39
+ {
40
+ icon: issue.severity === "error" ? "error" : "warning",
41
+ size: 16,
42
+ className: issue.severity === "error" ? "text-danger-emphasis" : "text-warning-emphasis",
43
+ "aria-hidden": true
44
+ }
45
+ ),
46
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-foreground-muted", children: labels.issueMessage(issue) })
47
+ ]
48
+ }
49
+ ) }, issueKey(issue))) })
50
+ ] });
51
+ };
52
+ export {
53
+ FlowIssueList as default
54
+ };
55
+ //# sourceMappingURL=FlowIssueList.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FlowIssueList.js","sources":["../../../../src/components/UI/Flow/FlowIssueList.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { useReactFlow } from '@xyflow/react';\n\nimport { useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport type { IFlowIssue } from './flowTypes';\nimport { useFlowLabels } from './useFlowLabels';\n\ninterface IFlowIssueListProps {\n issues: Array<IFlowIssue>;\n className?: string;\n}\n\nconst issueKey = (issue: IFlowIssue): string => `${issue.code}|${issue.nodeId ?? ''}|${issue.edgeId ?? ''}`;\n\n/** Every issue of the flow; clicking one selects and centers the block it points to. */\nconst FlowIssueList: React.FC<IFlowIssueListProps> = ({ issues, className }) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const store = useFlowEditorStore();\n const nodes = useFlowEditor((state) => state.graph.nodes);\n const { setCenter } = useReactFlow();\n const safeIssues = Array.isArray(issues) ? issues : [];\n const uniqueIssues = safeIssues.filter(\n (issue, index) => safeIssues.findIndex((other) => issueKey(other) === issueKey(issue)) === index,\n );\n\n const focusNode = (nodeId: string | undefined) => {\n const node = nodes.find((candidate) => candidate.nodeId === nodeId);\n if (!node) return;\n store.getState().setSelection({ nodeIds: [node.nodeId], edgeIds: [] });\n void setCenter(node.position.x + 128, node.position.y + 48, { zoom: 1, duration: 200 });\n };\n\n return (\n <section className={twMerge('flex flex-col gap-y-2', className)}>\n <span className=\"text-sm font-semibold text-foreground\">{t('flow.issues.title')}</span>\n {uniqueIssues.length === 0 ? (\n <NoDataFound compact variant=\"success\" message={t('flow.issues.none')} />\n ) : (\n <ul className=\"flex flex-col gap-y-1\">\n {uniqueIssues.map((issue) => (\n <li key={issueKey(issue)}>\n <ControlledButton\n defaultHtmlButton\n disabled={!issue.nodeId}\n onClick={() => focusNode(issue.nodeId)}\n className=\"interactive flex w-full items-start gap-x-2 rounded-lg px-2 py-1.5 text-left hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default disabled:hover:bg-transparent\"\n >\n <MaterialSymbol\n icon={issue.severity === 'error' ? 'error' : 'warning'}\n size={16}\n className={issue.severity === 'error' ? 'text-danger-emphasis' : 'text-warning-emphasis'}\n aria-hidden\n />\n <span className=\"text-xs text-foreground-muted\">{labels.issueMessage(issue)}</span>\n </ControlledButton>\n </li>\n ))}\n </ul>\n )}\n </section>\n );\n};\n\nexport default FlowIssueList;\n"],"names":[],"mappings":";;;;;;;;;AAiBA,MAAM,WAAW,CAAC,UAA8B,GAAG,MAAM,IAAI,IAAI,MAAM,UAAU,EAAE,IAAI,MAAM,UAAU,EAAE;AAGzG,MAAM,gBAA+C,CAAC,EAAE,QAAQ,gBAAgB;AAC9E,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,SAAS,cAAA;AACf,QAAM,QAAQ,mBAAA;AACd,QAAM,QAAQ,cAAc,CAAC,UAAU,MAAM,MAAM,KAAK;AACxD,QAAM,EAAE,UAAA,IAAc,aAAA;AACtB,QAAM,aAAa,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAA;AACpD,QAAM,eAAe,WAAW;AAAA,IAC9B,CAAC,OAAO,UAAU,WAAW,UAAU,CAAC,UAAU,SAAS,KAAK,MAAM,SAAS,KAAK,CAAC,MAAM;AAAA,EAAA;AAG7F,QAAM,YAAY,CAAC,WAA+B;AAChD,UAAM,OAAO,MAAM,KAAK,CAAC,cAAc,UAAU,WAAW,MAAM;AAClE,QAAI,CAAC,KAAM;AACX,UAAM,SAAA,EAAW,aAAa,EAAE,SAAS,CAAC,KAAK,MAAM,GAAG,SAAS,CAAA,GAAI;AACrE,SAAK,UAAU,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,IAAI,IAAI,EAAE,MAAM,GAAG,UAAU,KAAK;AAAA,EACxF;AAEA,8BACG,WAAA,EAAQ,WAAW,QAAQ,yBAAyB,SAAS,GAC5D,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAK,WAAU,yCAAyC,UAAA,EAAE,mBAAmB,GAAE;AAAA,IAC/E,aAAa,WAAW,IACvB,oBAAC,eAAY,SAAO,MAAC,SAAQ,WAAU,SAAS,EAAE,kBAAkB,EAAA,CAAG,IAEvE,oBAAC,MAAA,EAAG,WAAU,yBACX,uBAAa,IAAI,CAAC,UACjB,oBAAC,MAAA,EACC,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,mBAAiB;AAAA,QACjB,UAAU,CAAC,MAAM;AAAA,QACjB,SAAS,MAAM,UAAU,MAAM,MAAM;AAAA,QACrC,WAAU;AAAA,QAEV,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAM,MAAM,aAAa,UAAU,UAAU;AAAA,cAC7C,MAAM;AAAA,cACN,WAAW,MAAM,aAAa,UAAU,yBAAyB;AAAA,cACjE,eAAW;AAAA,YAAA;AAAA,UAAA;AAAA,8BAEZ,QAAA,EAAK,WAAU,iCAAiC,UAAA,OAAO,aAAa,KAAK,EAAA,CAAE;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,EAC9E,GAdO,SAAS,KAAK,CAevB,CACD,EAAA,CACH;AAAA,EAAA,GAEJ;AAEJ;"}
@@ -0,0 +1,30 @@
1
+ import { ReactNode } from 'react';
2
+ export interface IFlowLane {
3
+ laneId: string;
4
+ title: string;
5
+ subtitle?: string;
6
+ /** Counters or chips under the title (rules acting on entry, stage requirements). */
7
+ badges?: ReactNode;
8
+ }
9
+ export type TFlowArcTone = 'default' | 'warning' | 'danger';
10
+ export interface IFlowArc {
11
+ arcId: string;
12
+ sourceLaneId: string;
13
+ targetLaneId: string;
14
+ label: string;
15
+ /** `over` arcs above the lanes, `under` below them; the host decides what each side means. */
16
+ side: 'over' | 'under';
17
+ tone?: TFlowArcTone;
18
+ dashed?: boolean;
19
+ }
20
+ interface IFlowLaneMapProps {
21
+ lanes: Array<IFlowLane>;
22
+ arcs: Array<IFlowArc>;
23
+ onArcClick?: (arcId: string) => void;
24
+ onLaneClick?: (laneId: string) => void;
25
+ selectedArcId?: string | null;
26
+ ariaLabel?: string;
27
+ className?: string;
28
+ }
29
+ declare const FlowLaneMap: React.FC<IFlowLaneMapProps>;
30
+ export default FlowLaneMap;