@statelyai/flow-react 0.5.0 → 0.7.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/README.md +56 -11
- package/dist/esm/backgrounds.d.ts +13 -7
- package/dist/esm/backgrounds.js +1 -95
- package/dist/esm/backgrounds2.js +124 -0
- package/dist/esm/index.d.ts +207 -36
- package/dist/esm/index.js +1110 -221
- package/package.json +17 -13
- package/dist/esm/FlowInstanceContext.js +0 -20
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- description from package.json -->
|
|
4
4
|
|
|
5
|
-
Thin React adapter for `@statelyai/flow` and
|
|
5
|
+
Thin React adapter for `@statelyai/flow` with core and DOM re-exports.
|
|
6
6
|
|
|
7
7
|
## Philosophy
|
|
8
8
|
|
|
9
|
-
`@statelyai/flow-react` should be completely event-driven and composable. React should not hide a second flow model behind component internals; it should adapt the same `FlowInstance` that consumers can create, inspect, subscribe to, and trigger directly.
|
|
9
|
+
`@statelyai/flow-react` should be completely event-driven and composable. React should not hide a second flow model behind component internals; it should adapt the same `FlowInstance` that consumers can create, inspect, subscribe to, and trigger directly. It re-exports the lower layers so React consumers can install and import from one package.
|
|
10
10
|
|
|
11
11
|
The adapter abstracts over two lower layers:
|
|
12
12
|
|
|
@@ -15,11 +15,13 @@ The adapter abstracts over two lower layers:
|
|
|
15
15
|
|
|
16
16
|
The React package owns React lifecycle, context, selector subscriptions, and a convenience renderer. It should be flexible enough for consumers to build their own stateful flow component from the instance alone.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
<!-- transient entity rendering split between src/StatelyFlow.tsx and flow-dom src/entity-geometry.ts -->
|
|
19
|
+
|
|
20
|
+
Optimization is part of the architecture, not a later cleanup pass. Components subscribe only to durable content and interaction state. During drag, resize, and animated layout sessions, `@statelyai/flow-dom` batches node rectangles, edge paths, endpoint handles, and label positions into imperative animation-frame writes, so React commits only at transient-session boundaries. Default edge paths expose `data-flow-edge-segment`; custom edge renderers can use the same indexed attribute to join the imperative path updates. Router-provided `PathData.svgPath` geometry is honored by both initial React rendering and those imperative updates.
|
|
19
21
|
|
|
20
22
|
The graph data structure is the source of truth for graph content, not for transient UI state. Core node and edge data should not grow flags like `selected: true` or `dragging: true`; selection, focus, dragging, hover, connection state, and similar interaction state live in the flow store and are derived into render props such as `selected` or `dragging`.
|
|
21
23
|
|
|
22
|
-
DOM measurement is one-directional
|
|
24
|
+
DOM measurement is one-directional. Nodes, ports, and edge labels report their rendered size to the engine. For `autoSize` nodes and `label.autoSize` edge labels, the adapter leaves DOM width/height content-driven and Flow keeps measurements as derived session geometry; it never writes them into authored dimensions. Explicitly resizing an `autoSize` node switches its wrapper to the committed dimensions while preserving the content measurement as derived data. Graph-owned entities retain their existing controlled-size behavior.
|
|
23
25
|
|
|
24
26
|
## Core API
|
|
25
27
|
|
|
@@ -29,36 +31,72 @@ DOM measurement is one-directional by default. Nodes, ports, and edge labels rep
|
|
|
29
31
|
| ------------------------------------- | -------------------------------------------------------------------------------- |
|
|
30
32
|
| `createFlowInstance(options)` | Creates the shared instance used by React and custom renderers |
|
|
31
33
|
| `useFlow(options)` | TipTap-style hook for creating a stable flow controller |
|
|
32
|
-
| `useCreateFlowInstance(options)` |
|
|
34
|
+
| `useCreateFlowInstance(options)` | Deprecated alias for `useFlow(options)` |
|
|
33
35
|
| `useFlowSelector(selector, options?)` | Subscribe to any part of the flow store from React |
|
|
34
36
|
| `useFlowValue(selector, isEqual?)` | Short selector hook for the nearest provider |
|
|
35
37
|
| `useFlowContext()` | Subscribe to the full flow store context |
|
|
38
|
+
| `useConnection()` | Subscribe to the active connection gesture state |
|
|
39
|
+
| `useFlowConnections(query)` | Subscribe to stable, port-aware connections for one node |
|
|
40
|
+
| `useFlowView()` | Access view-owned image export and measurement refresh |
|
|
41
|
+
| `useKeyPress(keys)` | Track whether one or more keys are currently pressed |
|
|
42
|
+
| `useNodesInitialized()` | Check whether visible auto-size nodes have measurements |
|
|
36
43
|
| `useFlowNode(id, selector?, isEqual?)` | Subscribe to node data plus derived render fields |
|
|
37
44
|
| `useFlowEdge(id, selector?, isEqual?)` | Subscribe to edge data plus derived render fields |
|
|
38
45
|
| `useFlowPort(id, selector?, isEqual?)` | Subscribe to port data plus derived render fields |
|
|
39
46
|
| `useFlowViewport()` | Subscribe to viewport state |
|
|
40
|
-
| `useFlowMeasurement(id)` | Subscribe to measured bounds for an
|
|
47
|
+
| `useFlowMeasurement(id)` | Subscribe to measured bounds for an entity ref |
|
|
41
48
|
| `FlowInstanceProvider` | Provides a `FlowInstance` through React context |
|
|
42
49
|
| `useFlowInstance()` | Reads the nearest `FlowInstance` |
|
|
43
50
|
| `useOptionalFlowInstance()` | Reads the nearest `FlowInstance` or returns `null` |
|
|
44
|
-
| `StatelyFlow` | Convenience component that renders nodes, edges, backgrounds, and overlays
|
|
51
|
+
| `StatelyFlow` | Convenience component that renders nodes, edges, backgrounds, projections, and overlays |
|
|
52
|
+
| `createEntityGeometryObserver(store)` | Batches transient node/label rectangles and edge paths into imperative animation-frame DOM writes |
|
|
53
|
+
| `PanePortal` / `ViewportPortal` | Render custom content into fixed screen-coordinate or transformed canvas-coordinate layers |
|
|
54
|
+
| `EntityAnchor` | Position custom controls on entity sides or free-standing canvas bounds with screen-stable offsets and inverse scaling |
|
|
55
|
+
| `RenderEdgeLabelProps` | Edge, path, bounds, interaction state, and projected attrs passed to `renderEdgeLabel` |
|
|
56
|
+
| `Panel` | Positioned overlay panel for controls and custom UI |
|
|
57
|
+
| `NodeResizer` | Resize handles for custom node renderers; constraints live on `node.resize` |
|
|
58
|
+
| `ReactActiveGraphProjection` | React-typed active projection for `<StatelyFlow projections={...} />` or controller-owned projections |
|
|
59
|
+
| `ReactGraphNodeAttrs` / `ReactGraphEdgeAttrs` | React-typed visual attrs exposed to custom node/edge renderers |
|
|
45
60
|
| `FlowColorModeScope` | Applies the current flow color mode as DOM data attributes and `color-scheme` |
|
|
46
61
|
| `useFlowColorMode()` | Reads the store color mode and resolves `system` to `light` or `dark` |
|
|
47
62
|
| `useApplyFlowColorMode(element)` | Applies flow color mode attributes to an existing DOM element from inside a flow |
|
|
63
|
+
| `FLOW_BASE_CSS` / `useFlowBaseStyles()` | Base data-attribute stylesheet injected by `StatelyFlow` |
|
|
48
64
|
|
|
49
|
-
`@statelyai/flow-react/backgrounds`
|
|
65
|
+
`@statelyai/flow-react` and `@statelyai/flow-react/backgrounds` export `Background`, `DotsBackground`, `LinesBackground`, and `CrossBackground` helpers for viewport-aware canvas backgrounds. Backgrounds are plain React components, so consumers can also build custom ones with `useFlowSelector((context) => context.viewport)`.
|
|
50
66
|
|
|
51
67
|
Color mode is store data, not engine behavior. `@statelyai/flow-dom` exports small helpers for resolving and applying `light`, `dark`, and `system`, while React exposes hooks/components that make those values easy to use with CSS variables.
|
|
52
68
|
|
|
69
|
+
`initialGraph` seeds a graph that the adapter-owned flow instance owns. For caller ownership, create a controller and pass `flow={flow}`. There is no controlled `graph` prop; observe `flow.on('commit', ...)` or `onCommit` for persistence and synchronization. Runtime `settings`, `inputModes`, and `projections` update the existing flow instance. If `projections` is omitted, `StatelyFlow` subscribes to the instance's `flow.projections` lifecycle.
|
|
70
|
+
|
|
71
|
+
<!-- StatelyFlow renderEdgeLabel and layers props from src/StatelyFlow.tsx plus portal exports from src/LayerPortals.tsx -->
|
|
72
|
+
|
|
73
|
+
`renderEdgeLabel` replaces the built-in label content while retaining Flow's positioned, measured, interactive label wrapper. Set `edge.label.autoSize: true` for content-driven custom labels; the wrapper then leaves width/height unset while its measurement drives stable routed geometry. Render props include the edge's current `pathData`: the same `EdgePathData` object already routed by `StatelyFlow`, including both source-to-label and label-to-target segments. Transient drag geometry remains on Flow's imperative DOM path, and the renderer receives current committed geometry at gesture boundaries. Use `layers.viewport` for canvas-coordinate content and `layers.pane` for screen-coordinate content. Descendants can target the same named layers with `ViewportPortal` and `PanePortal`. Layer wrappers ignore pointer input; opt individual controls in with `pointerEvents: "auto"` and mark controls inside labels or canvas layers with `data-flow-interactive` when Flow gestures should ignore them.
|
|
74
|
+
|
|
75
|
+
Custom node and edge renderers should branch on the application-defined `entity.kind`. Keep `entity.type` as the structural `"node"` or `"edge"` discriminant so core selection, drag, connection, and CRUD behavior remains valid.
|
|
76
|
+
|
|
77
|
+
For controlled reconnect gestures, `reconnectMode({ commit: false, preview: false })` keeps Flow's input lifecycle while suppressing the adapter's projected preview edge, so an external domain store can render its own temporary geometry without duplicate paths.
|
|
78
|
+
|
|
79
|
+
`EntityAnchor` can be nested inside a node or edge-label renderer with `side`, a child `anchor` point, cross-axis `align`, and an outward `offset`. The nested anchor follows the parent entity's imperative drag geometry without React commits. With `inverseScale`, its size and offset remain constant in screen pixels and its transform origin matches the selected child anchor. The existing `bounds` plus `AnchorSpec` form remains available for free-standing canvas-space overlays.
|
|
80
|
+
|
|
81
|
+
`StatelyFlow` exposes stable, namespaced selector roles while keeping `data-flow-*` attributes authoritative: `.stately-flow` / `.stately-flow__pane` (interactive root), `.stately-flow__viewport` (transformed canvas), `.stately-flow__edges`, `.stately-flow__nodes`, `.stately-flow__edge-labels`, their singular entity classes, `.stately-flow__background`, `.stately-flow__viewport-layer`, `.stately-flow__pane-layer`, and `.stately-flow__overlays`. Consumer `className` values are merged with these roles.
|
|
82
|
+
|
|
83
|
+
Use `rootRef` and `rootProps` to access that input-owned root or attach ordinary div attributes and events such as context menu, double-click, and drag/drop. `rootProps.className` and `rootProps.style` merge with the top-level values; Flow's input runtime remains the sole gesture owner.
|
|
84
|
+
|
|
85
|
+
Keyboard handling is scoped to the `StatelyFlow` root by default so multiple flows on one page do not all receive Delete/undo/arrow keys. Pass `keyboardTarget="window"` to restore global handling. Pass `settings={{ keymap }}` to rebind built-in shortcuts. Double-click zoom is enabled in the default input modes; Shift double-click zooms out.
|
|
86
|
+
|
|
87
|
+
Alignment and equal-spacing guides stay a constant screen size across zoom and expose `--flow-snap-line-color` and `--flow-spacing-guide-color` for theming. Equal-spacing guides include their rounded canvas distance.
|
|
88
|
+
|
|
53
89
|
## Usage
|
|
54
90
|
|
|
55
91
|
```tsx
|
|
56
92
|
import {
|
|
93
|
+
createEdge,
|
|
94
|
+
createGraph,
|
|
95
|
+
createNode,
|
|
57
96
|
StatelyFlow,
|
|
58
97
|
useFlow,
|
|
59
98
|
useFlowSelector,
|
|
60
99
|
} from "@statelyai/flow-react";
|
|
61
|
-
import { createEdge, createGraph, createNode } from "@statelyai/flow";
|
|
62
100
|
|
|
63
101
|
const graph = createGraph({
|
|
64
102
|
nodes: [
|
|
@@ -70,7 +108,7 @@ const graph = createGraph({
|
|
|
70
108
|
|
|
71
109
|
function Flow() {
|
|
72
110
|
const flow = useFlow({
|
|
73
|
-
graph,
|
|
111
|
+
initialGraph: graph,
|
|
74
112
|
settings: {
|
|
75
113
|
store: { snapToGrid: true, snapGrid: [16, 16] },
|
|
76
114
|
},
|
|
@@ -87,7 +125,14 @@ function Flow() {
|
|
|
87
125
|
return (
|
|
88
126
|
<StatelyFlow
|
|
89
127
|
flow={flow}
|
|
90
|
-
|
|
128
|
+
projections={[
|
|
129
|
+
{
|
|
130
|
+
projection: { id: "coverage", nodes: { a: "visited" } },
|
|
131
|
+
mapper: {
|
|
132
|
+
getNodeAttrs: () => ({ color: "#16a34a", badge: "visited" }),
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
]}
|
|
91
136
|
renderNode={({ node, selected }) => (
|
|
92
137
|
<div data-selected={selected}>{node.data?.label ?? node.id}</div>
|
|
93
138
|
)}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import * as react0 from "react";
|
|
2
1
|
import { CSSProperties } from "react";
|
|
3
|
-
|
|
4
2
|
//#region src/backgrounds.d.ts
|
|
5
3
|
type BackgroundVariant = "dots" | "lines" | "cross";
|
|
6
4
|
type BackgroundProps = {
|
|
@@ -14,15 +12,23 @@ type BackgroundProps = {
|
|
|
14
12
|
/**
|
|
15
13
|
* Minimum on-screen spacing (px) before the grid subdivides. The effective
|
|
16
14
|
* spacing is kept in `[minSpacing, 2 * minSpacing)` by snapping `gap` to a
|
|
17
|
-
* power-of-two multiple, so the grid never looks crowded or sparse.
|
|
15
|
+
* power-of-two multiple, so the grid never looks crowded or sparse. Only used
|
|
16
|
+
* when `adaptive` is true.
|
|
18
17
|
*/
|
|
19
18
|
minSpacing?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the grid subdivides/coarsens with zoom to keep a visually constant
|
|
21
|
+
* density (Figma-style). Defaults to `true`. When `false`, the grid is a plain
|
|
22
|
+
* `gap`-spaced lattice that scales linearly with zoom — no subdivision — so it
|
|
23
|
+
* gets denser as you zoom out and sparser as you zoom in.
|
|
24
|
+
*/
|
|
25
|
+
adaptive?: boolean;
|
|
20
26
|
className?: string;
|
|
21
27
|
style?: CSSProperties;
|
|
22
28
|
};
|
|
23
|
-
declare function Background(props: BackgroundProps):
|
|
24
|
-
declare function DotsBackground(props: Omit<BackgroundProps, "variant">):
|
|
25
|
-
declare function LinesBackground(props: Omit<BackgroundProps, "variant">):
|
|
26
|
-
declare function CrossBackground(props: Omit<BackgroundProps, "variant">):
|
|
29
|
+
declare function Background(props: BackgroundProps): import("react").JSX.Element;
|
|
30
|
+
declare function DotsBackground(props: Omit<BackgroundProps, "variant">): import("react").JSX.Element;
|
|
31
|
+
declare function LinesBackground(props: Omit<BackgroundProps, "variant">): import("react").JSX.Element;
|
|
32
|
+
declare function CrossBackground(props: Omit<BackgroundProps, "variant">): import("react").JSX.Element;
|
|
27
33
|
//#endregion
|
|
28
34
|
export { Background, BackgroundProps, BackgroundVariant, CrossBackground, DotsBackground, LinesBackground };
|
package/dist/esm/backgrounds.js
CHANGED
|
@@ -1,96 +1,2 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
3
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
-
//#region src/backgrounds.tsx
|
|
5
|
-
function Background(props) {
|
|
6
|
-
const instance = useFlowInstance();
|
|
7
|
-
const variant = props.variant ?? "dots";
|
|
8
|
-
const gap = props.gap ?? 24;
|
|
9
|
-
const size = props.size ?? 1;
|
|
10
|
-
const color = props.color ?? "#cbd5e1";
|
|
11
|
-
const minSpacing = props.minSpacing ?? 24;
|
|
12
|
-
const image = useMemo(() => getBackgroundImage(variant, color, size), [
|
|
13
|
-
variant,
|
|
14
|
-
color,
|
|
15
|
-
size
|
|
16
|
-
]);
|
|
17
|
-
const majorRef = useRef(null);
|
|
18
|
-
const minorRef = useRef(null);
|
|
19
|
-
useLayoutEffect(() => {
|
|
20
|
-
const major = majorRef.current;
|
|
21
|
-
const minor = minorRef.current;
|
|
22
|
-
if (!major || !minor) return;
|
|
23
|
-
const phase = variant === "lines" ? 0 : .5;
|
|
24
|
-
const apply = (viewport) => {
|
|
25
|
-
const screenGap = gap * viewport.zoom;
|
|
26
|
-
const majorGap = screenGap * 2 ** Math.ceil(Math.log2(minSpacing / screenGap));
|
|
27
|
-
const minorGap = majorGap / 2;
|
|
28
|
-
const t = majorGap / minSpacing - 1;
|
|
29
|
-
major.style.backgroundSize = `${majorGap}px ${majorGap}px`;
|
|
30
|
-
major.style.backgroundPosition = `${viewport.x - phase * majorGap}px ${viewport.y - phase * majorGap}px`;
|
|
31
|
-
minor.style.backgroundSize = `${minorGap}px ${minorGap}px`;
|
|
32
|
-
minor.style.backgroundPosition = `${viewport.x - phase * minorGap}px ${viewport.y - phase * minorGap}px`;
|
|
33
|
-
minor.style.opacity = String(t);
|
|
34
|
-
};
|
|
35
|
-
apply(instance.getSnapshot().viewport);
|
|
36
|
-
return instance.listen((context) => context.viewport, (viewport) => apply(viewport));
|
|
37
|
-
}, [
|
|
38
|
-
instance,
|
|
39
|
-
gap,
|
|
40
|
-
minSpacing,
|
|
41
|
-
variant
|
|
42
|
-
]);
|
|
43
|
-
const layerStyle = {
|
|
44
|
-
position: "absolute",
|
|
45
|
-
inset: 0,
|
|
46
|
-
backgroundImage: image
|
|
47
|
-
};
|
|
48
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
49
|
-
"aria-hidden": true,
|
|
50
|
-
className: props.className,
|
|
51
|
-
style: {
|
|
52
|
-
position: "absolute",
|
|
53
|
-
inset: 0,
|
|
54
|
-
backgroundColor: props.backgroundColor,
|
|
55
|
-
pointerEvents: "none",
|
|
56
|
-
...props.style
|
|
57
|
-
},
|
|
58
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
59
|
-
ref: minorRef,
|
|
60
|
-
style: {
|
|
61
|
-
...layerStyle,
|
|
62
|
-
opacity: 0
|
|
63
|
-
}
|
|
64
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
65
|
-
ref: majorRef,
|
|
66
|
-
style: layerStyle
|
|
67
|
-
})]
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
function DotsBackground(props) {
|
|
71
|
-
return /* @__PURE__ */ jsx(Background, {
|
|
72
|
-
...props,
|
|
73
|
-
variant: "dots"
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
function LinesBackground(props) {
|
|
77
|
-
return /* @__PURE__ */ jsx(Background, {
|
|
78
|
-
...props,
|
|
79
|
-
variant: "lines"
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
function CrossBackground(props) {
|
|
83
|
-
return /* @__PURE__ */ jsx(Background, {
|
|
84
|
-
...props,
|
|
85
|
-
variant: "cross"
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
function getBackgroundImage(variant, color, size) {
|
|
89
|
-
switch (variant) {
|
|
90
|
-
case "dots": return `radial-gradient(circle, ${color} ${size}px, transparent ${size}px)`;
|
|
91
|
-
case "lines": return [`linear-gradient(${color} ${size}px, transparent ${size}px)`, `linear-gradient(90deg, ${color} ${size}px, transparent ${size}px)`].join(", ");
|
|
92
|
-
case "cross": return `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M50 38v24M38 50h24" stroke="${color}" stroke-width="${Math.max(1, size)}" stroke-linecap="round"/></svg>`)}")`;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
//#endregion
|
|
1
|
+
import { i as LinesBackground, n as CrossBackground, r as DotsBackground, t as Background } from "./backgrounds2.js";
|
|
96
2
|
export { Background, CrossBackground, DotsBackground, LinesBackground };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createContext, useContext, useLayoutEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/FlowInstanceContext.tsx
|
|
4
|
+
const FlowInstanceContext = createContext(null);
|
|
5
|
+
function FlowInstanceProvider(props) {
|
|
6
|
+
return /* @__PURE__ */ jsx(FlowInstanceContext.Provider, {
|
|
7
|
+
value: props.flow,
|
|
8
|
+
children: props.children
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function useFlowInstance() {
|
|
12
|
+
const flow = useContext(FlowInstanceContext);
|
|
13
|
+
if (!flow) throw new Error("Flow instance is missing. Wrap this tree in <FlowInstanceProvider flow={flow}>.");
|
|
14
|
+
return flow;
|
|
15
|
+
}
|
|
16
|
+
function useOptionalFlowInstance() {
|
|
17
|
+
return useContext(FlowInstanceContext);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/backgrounds.tsx
|
|
21
|
+
function classNames(...values) {
|
|
22
|
+
return values.filter(Boolean).join(" ");
|
|
23
|
+
}
|
|
24
|
+
function Background(props) {
|
|
25
|
+
const instance = useFlowInstance();
|
|
26
|
+
const variant = props.variant ?? "dots";
|
|
27
|
+
const gap = props.gap ?? 24;
|
|
28
|
+
const size = props.size ?? 1;
|
|
29
|
+
const color = props.color ?? "#cbd5e1";
|
|
30
|
+
const minSpacing = props.minSpacing ?? 24;
|
|
31
|
+
const adaptive = props.adaptive ?? true;
|
|
32
|
+
const image = useMemo(() => getBackgroundImage(variant, color, size), [
|
|
33
|
+
variant,
|
|
34
|
+
color,
|
|
35
|
+
size
|
|
36
|
+
]);
|
|
37
|
+
const majorRef = useRef(null);
|
|
38
|
+
const minorRef = useRef(null);
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const major = majorRef.current;
|
|
41
|
+
const minor = minorRef.current;
|
|
42
|
+
if (!major || !minor) return;
|
|
43
|
+
const phase = variant === "lines" ? 0 : .5;
|
|
44
|
+
const apply = (viewport) => {
|
|
45
|
+
if (!adaptive) {
|
|
46
|
+
const fixedGap = gap * viewport.zoom;
|
|
47
|
+
major.style.backgroundSize = `${fixedGap}px ${fixedGap}px`;
|
|
48
|
+
major.style.backgroundPosition = `${viewport.x - phase * fixedGap}px ${viewport.y - phase * fixedGap}px`;
|
|
49
|
+
minor.style.opacity = "0";
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const screenGap = gap * viewport.zoom;
|
|
53
|
+
const majorGap = screenGap * 2 ** Math.ceil(Math.log2(minSpacing / screenGap));
|
|
54
|
+
const minorGap = majorGap / 2;
|
|
55
|
+
const t = majorGap / minSpacing - 1;
|
|
56
|
+
major.style.backgroundSize = `${majorGap}px ${majorGap}px`;
|
|
57
|
+
major.style.backgroundPosition = `${viewport.x - phase * majorGap}px ${viewport.y - phase * majorGap}px`;
|
|
58
|
+
minor.style.backgroundSize = `${minorGap}px ${minorGap}px`;
|
|
59
|
+
minor.style.backgroundPosition = `${viewport.x - phase * minorGap}px ${viewport.y - phase * minorGap}px`;
|
|
60
|
+
minor.style.opacity = String(t);
|
|
61
|
+
};
|
|
62
|
+
apply(instance.getSnapshot().viewport);
|
|
63
|
+
return instance.listen((context) => context.viewport, (viewport) => apply(viewport));
|
|
64
|
+
}, [
|
|
65
|
+
instance,
|
|
66
|
+
gap,
|
|
67
|
+
minSpacing,
|
|
68
|
+
variant,
|
|
69
|
+
adaptive
|
|
70
|
+
]);
|
|
71
|
+
const layerStyle = {
|
|
72
|
+
position: "absolute",
|
|
73
|
+
inset: 0,
|
|
74
|
+
backgroundImage: image
|
|
75
|
+
};
|
|
76
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
77
|
+
"aria-hidden": true,
|
|
78
|
+
className: classNames("stately-flow__background", props.className),
|
|
79
|
+
style: {
|
|
80
|
+
position: "absolute",
|
|
81
|
+
inset: 0,
|
|
82
|
+
backgroundColor: props.backgroundColor,
|
|
83
|
+
pointerEvents: "none",
|
|
84
|
+
...props.style
|
|
85
|
+
},
|
|
86
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
87
|
+
ref: minorRef,
|
|
88
|
+
style: {
|
|
89
|
+
...layerStyle,
|
|
90
|
+
opacity: 0
|
|
91
|
+
}
|
|
92
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
93
|
+
ref: majorRef,
|
|
94
|
+
style: layerStyle
|
|
95
|
+
})]
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function DotsBackground(props) {
|
|
99
|
+
return /* @__PURE__ */ jsx(Background, {
|
|
100
|
+
...props,
|
|
101
|
+
variant: "dots"
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function LinesBackground(props) {
|
|
105
|
+
return /* @__PURE__ */ jsx(Background, {
|
|
106
|
+
...props,
|
|
107
|
+
variant: "lines"
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function CrossBackground(props) {
|
|
111
|
+
return /* @__PURE__ */ jsx(Background, {
|
|
112
|
+
...props,
|
|
113
|
+
variant: "cross"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function getBackgroundImage(variant, color, size) {
|
|
117
|
+
switch (variant) {
|
|
118
|
+
case "dots": return `radial-gradient(circle, ${color} ${size}px, transparent ${size}px)`;
|
|
119
|
+
case "lines": return [`linear-gradient(${color} ${size}px, transparent ${size}px)`, `linear-gradient(90deg, ${color} ${size}px, transparent ${size}px)`].join(", ");
|
|
120
|
+
case "cross": return `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M50 38v24M38 50h24" stroke="${color}" stroke-width="${Math.max(1, size)}" stroke-linecap="round"/></svg>`)}")`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { FlowInstanceProvider as a, LinesBackground as i, CrossBackground as n, useFlowInstance as o, DotsBackground as r, useOptionalFlowInstance as s, Background as t };
|