@statelyai/flow-react 0.7.2 → 0.8.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 +51 -45
- package/dist/esm/index.d.ts +15 -4
- package/dist/esm/index.js +130 -21
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The React package owns React lifecycle, context, selector subscriptions, and a c
|
|
|
17
17
|
|
|
18
18
|
<!-- transient entity rendering split between src/StatelyFlow.tsx and flow-dom src/entity-geometry.ts -->
|
|
19
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,
|
|
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, label positions, and multi-selection bounds 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.
|
|
21
21
|
|
|
22
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`.
|
|
23
23
|
|
|
@@ -27,40 +27,41 @@ DOM measurement is one-directional. Nodes, ports, and edge labels report their r
|
|
|
27
27
|
|
|
28
28
|
<!-- exported React adapter API from src/index.ts -->
|
|
29
29
|
|
|
30
|
-
| Export
|
|
31
|
-
|
|
|
32
|
-
| `createFlowInstance(options)`
|
|
33
|
-
| `useFlow(options)`
|
|
34
|
-
| `useCreateFlowInstance(options)`
|
|
35
|
-
| `useFlowSelector(selector, options?)`
|
|
36
|
-
| `useFlowValue(selector, isEqual?)`
|
|
37
|
-
| `useFlowContext()`
|
|
38
|
-
| `useConnection()`
|
|
39
|
-
| `useFlowConnections(query)`
|
|
40
|
-
| `useFlowView()`
|
|
41
|
-
| `useKeyPress(keys)`
|
|
42
|
-
| `useNodesInitialized()`
|
|
43
|
-
| `useFlowNode(id, selector?, isEqual?)`
|
|
44
|
-
| `useFlowEdge(id, selector?, isEqual?)`
|
|
45
|
-
| `useFlowPort(id, selector?, isEqual?)`
|
|
46
|
-
| `useFlowViewport()`
|
|
47
|
-
| `useFlowMeasurement(id)`
|
|
48
|
-
| `FlowInstanceProvider`
|
|
49
|
-
| `useFlowInstance()`
|
|
50
|
-
| `useOptionalFlowInstance()`
|
|
51
|
-
| `StatelyFlow`
|
|
52
|
-
| `createEntityGeometryObserver(store)`
|
|
53
|
-
| `PanePortal` / `ViewportPortal`
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
30
|
+
| Export | Description |
|
|
31
|
+
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
32
|
+
| `createFlowInstance(options)` | Creates the shared instance used by React and custom renderers |
|
|
33
|
+
| `useFlow(options)` | TipTap-style hook for creating a stable flow controller |
|
|
34
|
+
| `useCreateFlowInstance(options)` | Deprecated alias for `useFlow(options)` |
|
|
35
|
+
| `useFlowSelector(selector, options?)` | Subscribe to any part of the flow store from React |
|
|
36
|
+
| `useFlowValue(selector, isEqual?)` | Short selector hook for the nearest provider |
|
|
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 |
|
|
43
|
+
| `useFlowNode(id, selector?, isEqual?)` | Subscribe to node data plus derived render fields |
|
|
44
|
+
| `useFlowEdge(id, selector?, isEqual?)` | Subscribe to edge data plus derived render fields |
|
|
45
|
+
| `useFlowPort(id, selector?, isEqual?)` | Subscribe to port data plus derived render fields |
|
|
46
|
+
| `useFlowViewport()` | Subscribe to viewport state |
|
|
47
|
+
| `useFlowMeasurement(id)` | Subscribe to measured bounds for an entity ref |
|
|
48
|
+
| `FlowInstanceProvider` | Provides a `FlowInstance` through React context |
|
|
49
|
+
| `useFlowInstance()` | Reads the nearest `FlowInstance` |
|
|
50
|
+
| `useOptionalFlowInstance()` | Reads the nearest `FlowInstance` or returns `null` |
|
|
51
|
+
| `StatelyFlow` | Convenience component that renders nodes, edges, backgrounds, projections, and overlays |
|
|
52
|
+
| `createEntityGeometryObserver(store)` | Batches transient node/label rectangles, multi-entity bounds, 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
|
+
| `useFitViewExclusion()` | Register an overlay element whose measured edge band fit and center commands avoid |
|
|
55
|
+
| `EntityAnchor` | Position custom controls on entity sides or free-standing canvas bounds with screen-stable offsets and inverse scaling |
|
|
56
|
+
| `RenderEdgeLabelProps` | Edge, path, bounds, interaction state, and projected attrs passed to `renderEdgeLabel` |
|
|
57
|
+
| `Panel` | Positioned overlay panel for controls and custom UI |
|
|
58
|
+
| `NodeResizer` | Resize handles for custom node renderers; constraints live on `node.resize` |
|
|
59
|
+
| `ReactActiveGraphProjection` | React-typed active projection for `<StatelyFlow projections={...} />` or controller-owned projections |
|
|
60
|
+
| `ReactGraphNodeAttrs` / `ReactGraphEdgeAttrs` | React-typed visual attrs exposed to custom node/edge renderers |
|
|
61
|
+
| `FlowColorModeScope` | Applies the current flow color mode as DOM data attributes and `color-scheme` |
|
|
62
|
+
| `useFlowColorMode()` | Reads the store color mode and resolves `system` to `light` or `dark` |
|
|
63
|
+
| `useApplyFlowColorMode(element)` | Applies flow color mode attributes to an existing DOM element from inside a flow |
|
|
64
|
+
| `FLOW_BASE_CSS` / `useFlowBaseStyles()` | Base data-attribute stylesheet injected by `StatelyFlow` |
|
|
64
65
|
|
|
65
66
|
`@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)`.
|
|
66
67
|
|
|
@@ -70,7 +71,11 @@ Color mode is store data, not engine behavior. `@statelyai/flow-dom` exports sma
|
|
|
70
71
|
|
|
71
72
|
<!-- StatelyFlow renderEdgeLabel and layers props from src/StatelyFlow.tsx plus portal exports from src/LayerPortals.tsx -->
|
|
72
73
|
|
|
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
|
+
`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 interaction content and `layers.pane` for screen-coordinate content. Descendants can target the same named layers with `ViewportPortal` and `PanePortal`. Flow enforces paint bands (`edges` → `entities` → `labels` → `interaction` → `guides` → `chrome`) so entity-local z-index cannot cover labels, selection chrome, anchors, or portaled toolbars. Override the namespaced `--flow-layer-*` variables only when intentionally changing that contract. 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.
|
|
75
|
+
|
|
76
|
+
Attach the ref from `useFitViewExclusion({ side, gap })` to floating controls that cover the canvas. The hook measures the element relative to the Flow root at command time, including content portaled outside the root, so responsive toolbars and panel-relative chrome do not need guessed fit padding.
|
|
77
|
+
|
|
78
|
+
Set `showSelectionBounds` to render a zoom-stable, square frame around established multi-selections. Its geometry follows drag, resize, and layout frames through Flow DOM without React commits; theme `[data-flow-selection-bounds]`, `--flow-selection-color`, or the screen-pixel `--flow-selection-bounds-outset`. Set `preserveViewportCenterOnResize` when root container resizes should retain the world point at its center.
|
|
74
79
|
|
|
75
80
|
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
81
|
|
|
@@ -78,7 +83,9 @@ For controlled reconnect gestures, `reconnectMode({ commit: false, preview: fals
|
|
|
78
83
|
|
|
79
84
|
`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
85
|
|
|
81
|
-
|
|
86
|
+
<!-- namespaced selector roles and entity state attributes from src/StatelyFlow.tsx -->
|
|
87
|
+
|
|
88
|
+
`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`, `.stately-flow__selection-bounds`, 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. Focus presentation is consumer-owned and visually off by default: set `--flow-focus-outline` or target `:focus-visible` for browser focus, and use `[data-flow-focused="true"]` for synchronized Flow focus state.
|
|
82
89
|
|
|
83
90
|
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
91
|
|
|
@@ -96,14 +103,14 @@ import {
|
|
|
96
103
|
StatelyFlow,
|
|
97
104
|
useFlow,
|
|
98
105
|
useFlowSelector,
|
|
99
|
-
} from
|
|
106
|
+
} from '@statelyai/flow-react';
|
|
100
107
|
|
|
101
108
|
const graph = createGraph({
|
|
102
109
|
nodes: [
|
|
103
|
-
createNode({ id:
|
|
104
|
-
createNode({ id:
|
|
110
|
+
createNode({ id: 'a', x: 0, y: 0, data: { label: 'Start' } }),
|
|
111
|
+
createNode({ id: 'b', x: 240, y: 120, data: { label: 'Done' } }),
|
|
105
112
|
],
|
|
106
|
-
edges: [createEdge({ id:
|
|
113
|
+
edges: [createEdge({ id: 'a-b', sourceId: 'a', targetId: 'b' })],
|
|
107
114
|
});
|
|
108
115
|
|
|
109
116
|
function Flow() {
|
|
@@ -116,7 +123,7 @@ function Flow() {
|
|
|
116
123
|
{
|
|
117
124
|
selector: (context) => context.selection.size,
|
|
118
125
|
onChange: (selectionSize) => {
|
|
119
|
-
console.log(
|
|
126
|
+
console.log('selection size', selectionSize);
|
|
120
127
|
},
|
|
121
128
|
},
|
|
122
129
|
],
|
|
@@ -127,9 +134,9 @@ function Flow() {
|
|
|
127
134
|
flow={flow}
|
|
128
135
|
projections={[
|
|
129
136
|
{
|
|
130
|
-
projection: { id:
|
|
137
|
+
projection: { id: 'coverage', nodes: { a: 'visited' } },
|
|
131
138
|
mapper: {
|
|
132
|
-
getNodeAttrs: () => ({ color:
|
|
139
|
+
getNodeAttrs: () => ({ color: '#16a34a', badge: 'visited' }),
|
|
133
140
|
},
|
|
134
141
|
},
|
|
135
142
|
]}
|
|
@@ -140,7 +147,6 @@ function Flow() {
|
|
|
140
147
|
);
|
|
141
148
|
}
|
|
142
149
|
```
|
|
143
|
-
|
|
144
150
|
## Storybook
|
|
145
151
|
|
|
146
152
|
```bash
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Background, BackgroundProps, BackgroundVariant, CrossBackground, DotsBackground, LinesBackground } from "./backgrounds.js";
|
|
2
2
|
import { FlowView, ResolvedFlowColorMode } from "@statelyai/flow-dom";
|
|
3
|
-
import { ActiveGraphProjection, AnchorSpec, ConnectionEndedEvent, ConnectionState, CreateEdgeInput, CreateFlowOptions, EdgePathData, FlowColorMode, FlowCommitEvent, FlowConnection, FlowController, FlowEdge, FlowGraph, FlowInputMode, FlowKeymap, FlowNode, FlowProfile, FlowStoreContext, GraphEdgeAttrs, GraphNodeAttrs, LayoutRequestedEvent, Point, Rect, ResizeHandle, Viewport, VisualPort } from "@statelyai/flow";
|
|
4
|
-
import { CSSProperties, ComponentPropsWithoutRef, HTMLAttributes, ReactNode, Ref, RefObject } from "react";
|
|
3
|
+
import { ActiveGraphProjection, AnchorSpec, ConnectionEndedEvent, ConnectionState, CreateEdgeInput, CreateFlowOptions, EdgePathData, FitViewExclusionSide, FlowColorMode, FlowCommitEvent, FlowConnection, FlowController, FlowEdge, FlowGraph, FlowInputMode, FlowKeymap, FlowNode, FlowProfile, FlowStoreContext, GraphEdgeAttrs, GraphNodeAttrs, LayoutRequestedEvent, Point, Rect, ResizeHandle, Viewport, VisualPort } from "@statelyai/flow";
|
|
4
|
+
import { CSSProperties, ComponentPropsWithoutRef, HTMLAttributes, ReactNode, Ref, RefCallback, RefObject } from "react";
|
|
5
5
|
export * from "@statelyai/flow-dom";
|
|
6
6
|
//#region src/createFlowInstance.d.ts
|
|
7
7
|
type FlowSelector<T> = (context: FlowStoreContext) => T;
|
|
@@ -203,6 +203,10 @@ type StatelyFlowBaseProps<TNodeData = unknown, TEdgeData = unknown> = {
|
|
|
203
203
|
renderEdge?: (props: RenderEdgeProps<TEdgeData>) => ReactNode;
|
|
204
204
|
renderEdgeLabel?: (props: RenderEdgeLabelProps<TEdgeData>) => ReactNode;
|
|
205
205
|
renderConnectionLine?: (props: RenderConnectionLineProps) => ReactNode;
|
|
206
|
+
/** Render a zoom-stable frame around established multi-selections. */
|
|
207
|
+
showSelectionBounds?: boolean;
|
|
208
|
+
/** Keep the same world point centered when the root container resizes. */
|
|
209
|
+
preserveViewportCenterOnResize?: boolean;
|
|
206
210
|
/**
|
|
207
211
|
* Only mount nodes/edges that intersect the viewport (plus an overscan
|
|
208
212
|
* margin). Recommended for large graphs.
|
|
@@ -262,6 +266,12 @@ declare function ViewportPortal(props: {
|
|
|
262
266
|
declare function PanePortal(props: {
|
|
263
267
|
children: ReactNode;
|
|
264
268
|
}): import("react").ReactPortal | null;
|
|
269
|
+
type UseFitViewExclusionOptions = {
|
|
270
|
+
side?: FitViewExclusionSide;
|
|
271
|
+
gap?: number;
|
|
272
|
+
};
|
|
273
|
+
/** Register an overlay element whose measured edge band fit commands avoid. */
|
|
274
|
+
declare function useFitViewExclusion(options?: UseFitViewExclusionOptions): RefCallback<HTMLElement>;
|
|
265
275
|
//#endregion
|
|
266
276
|
//#region src/FlowToolbar.d.ts
|
|
267
277
|
/** Border-box size of an element, kept current via ResizeObserver. */
|
|
@@ -461,9 +471,10 @@ declare function NodeResizer(props: NodeResizerProps): import("react").JSX.Eleme
|
|
|
461
471
|
* inline `pointer-events` on the same element — inline wins over this rule.
|
|
462
472
|
* - `data-flow-selection-box` — the box-select marquee (styled here; geometry
|
|
463
473
|
* is set inline by the renderer).
|
|
474
|
+
* - `data-flow-selection-bounds` — the established multi-selection frame.
|
|
464
475
|
*/
|
|
465
476
|
declare const FLOW_BASE_STYLES_ID = "flow-base-styles";
|
|
466
|
-
declare const FLOW_BASE_CSS = "\n[data-flow-root] {\n --flow-selection-color: #2563eb;\n --flow-selection-bg: rgba(37, 99, 235, 0.12);\n touch-action: none;\n}\n\n[data-flow-interactive-when-selected] {\n pointer-events: none;\n}\n[data-flow-selected=\"true\"] [data-flow-interactive-when-selected] {\n pointer-events: auto;\n}\n\n[data-flow-selection-box] {\n position: absolute;\n box-sizing: border-box;\n border: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);\n border-radius: calc(
|
|
477
|
+
declare const FLOW_BASE_CSS = "\n[data-flow-root] {\n --flow-selection-color: #2563eb;\n --flow-selection-bg: rgba(37, 99, 235, 0.12);\n --flow-selection-bounds-outset: 1px;\n --flow-layer-edges: 0;\n --flow-layer-entities: 10;\n --flow-layer-labels: 20;\n --flow-layer-interaction: 30;\n --flow-layer-guides: 40;\n --flow-layer-chrome: 50;\n touch-action: none;\n}\n\n[data-flow-root]:focus {\n outline: var(--flow-focus-outline, none);\n}\n\n[data-flow-interactive-when-selected] {\n pointer-events: none;\n}\n[data-flow-selected=\"true\"] [data-flow-interactive-when-selected] {\n pointer-events: auto;\n}\n\n[data-flow-selection-box] {\n position: absolute;\n box-sizing: border-box;\n border: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);\n border-radius: 0;\n background: var(--flow-selection-bg);\n pointer-events: none;\n}\n\n[data-flow-selection-bounds] {\n position: absolute;\n box-sizing: border-box;\n outline: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);\n outline-offset: calc(var(--flow-selection-bounds-outset) / var(--flow-zoom, 1));\n border-radius: 0;\n pointer-events: none;\n}\n";
|
|
467
478
|
/** Inject the base stylesheet once per document (deduped by element id). */
|
|
468
479
|
declare function useFlowBaseStyles(): void;
|
|
469
480
|
//#endregion
|
|
@@ -480,4 +491,4 @@ declare function useFlowColorMode(): {
|
|
|
480
491
|
};
|
|
481
492
|
declare function useApplyFlowColorMode(element: HTMLElement | null): void;
|
|
482
493
|
//#endregion
|
|
483
|
-
export { Background, type BackgroundProps, type BackgroundVariant, Controls, type ControlsProps, type CreateFlowInstanceOptions, CrossBackground, DotsBackground, DragHandle, type DragHandleProps, EntityAnchor, type EntityAnchorAlign, type EntityAnchorPoint, type EntityAnchorProps, type EntityAnchorSide, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, type FlowColorModeScopeProps, type FlowEdgeBinding, type FlowEventListeners, type FlowInstance, FlowInstanceProvider, type FlowInstanceSettings, type FlowNodeBinding, type FlowPortBinding, type FlowSelector, type FlowSelectorEquality, type FlowStoreListener, FlowToolbar, type FlowToolbarProps, type FlowToolbarState, LinesBackground, MiniMap, type MiniMapProps, NodeResizer, type NodeResizerProps, PanePortal, Panel, type PanelPosition, type PanelProps, Port, type PortProps, type ReactActiveGraphProjection, type ReactGraphEdgeAttrs, type ReactGraphNodeAttrs, type RenderConnectionLineProps, type RenderEdgeLabelProps, type RenderEdgeProps, type RenderNodeProps, StatelyFlow, type StatelyFlowProps, type StatelyFlowRootProps, type StatelyFlowStyle, type UseCreateFlowInstanceOptions, type UseFlowInputOptions, type UseFlowOptions, type UseFlowToolbarOptions, ViewportPortal, createFlowInstance, useApplyFlowColorMode, useConnection, useCreateFlowInstance, useFlow, useFlowBaseStyles, useFlowColorMode, useFlowConnections, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowView, useFlowViewport, useKeyPress, useMeasuredSize, useNodesInitialized, useOptionalFlowInstance };
|
|
494
|
+
export { Background, type BackgroundProps, type BackgroundVariant, Controls, type ControlsProps, type CreateFlowInstanceOptions, CrossBackground, DotsBackground, DragHandle, type DragHandleProps, EntityAnchor, type EntityAnchorAlign, type EntityAnchorPoint, type EntityAnchorProps, type EntityAnchorSide, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, type FlowColorModeScopeProps, type FlowEdgeBinding, type FlowEventListeners, type FlowInstance, FlowInstanceProvider, type FlowInstanceSettings, type FlowNodeBinding, type FlowPortBinding, type FlowSelector, type FlowSelectorEquality, type FlowStoreListener, FlowToolbar, type FlowToolbarProps, type FlowToolbarState, LinesBackground, MiniMap, type MiniMapProps, NodeResizer, type NodeResizerProps, PanePortal, Panel, type PanelPosition, type PanelProps, Port, type PortProps, type ReactActiveGraphProjection, type ReactGraphEdgeAttrs, type ReactGraphNodeAttrs, type RenderConnectionLineProps, type RenderEdgeLabelProps, type RenderEdgeProps, type RenderNodeProps, StatelyFlow, type StatelyFlowProps, type StatelyFlowRootProps, type StatelyFlowStyle, type UseCreateFlowInstanceOptions, type UseFitViewExclusionOptions, type UseFlowInputOptions, type UseFlowOptions, type UseFlowToolbarOptions, ViewportPortal, createFlowInstance, useApplyFlowColorMode, useConnection, useCreateFlowInstance, useFitViewExclusion, useFlow, useFlowBaseStyles, useFlowColorMode, useFlowConnections, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowView, useFlowViewport, useKeyPress, useMeasuredSize, useNodesInitialized, useOptionalFlowInstance };
|
package/dist/esm/index.js
CHANGED
|
@@ -375,15 +375,27 @@ function useFlowView() {
|
|
|
375
375
|
* inline `pointer-events` on the same element — inline wins over this rule.
|
|
376
376
|
* - `data-flow-selection-box` — the box-select marquee (styled here; geometry
|
|
377
377
|
* is set inline by the renderer).
|
|
378
|
+
* - `data-flow-selection-bounds` — the established multi-selection frame.
|
|
378
379
|
*/
|
|
379
380
|
const FLOW_BASE_STYLES_ID = "flow-base-styles";
|
|
380
381
|
const FLOW_BASE_CSS = `
|
|
381
382
|
[data-flow-root] {
|
|
382
383
|
--flow-selection-color: #2563eb;
|
|
383
384
|
--flow-selection-bg: rgba(37, 99, 235, 0.12);
|
|
385
|
+
--flow-selection-bounds-outset: 1px;
|
|
386
|
+
--flow-layer-edges: 0;
|
|
387
|
+
--flow-layer-entities: 10;
|
|
388
|
+
--flow-layer-labels: 20;
|
|
389
|
+
--flow-layer-interaction: 30;
|
|
390
|
+
--flow-layer-guides: 40;
|
|
391
|
+
--flow-layer-chrome: 50;
|
|
384
392
|
touch-action: none;
|
|
385
393
|
}
|
|
386
394
|
|
|
395
|
+
[data-flow-root]:focus {
|
|
396
|
+
outline: var(--flow-focus-outline, none);
|
|
397
|
+
}
|
|
398
|
+
|
|
387
399
|
[data-flow-interactive-when-selected] {
|
|
388
400
|
pointer-events: none;
|
|
389
401
|
}
|
|
@@ -395,10 +407,19 @@ const FLOW_BASE_CSS = `
|
|
|
395
407
|
position: absolute;
|
|
396
408
|
box-sizing: border-box;
|
|
397
409
|
border: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);
|
|
398
|
-
border-radius:
|
|
410
|
+
border-radius: 0;
|
|
399
411
|
background: var(--flow-selection-bg);
|
|
400
412
|
pointer-events: none;
|
|
401
413
|
}
|
|
414
|
+
|
|
415
|
+
[data-flow-selection-bounds] {
|
|
416
|
+
position: absolute;
|
|
417
|
+
box-sizing: border-box;
|
|
418
|
+
outline: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);
|
|
419
|
+
outline-offset: calc(var(--flow-selection-bounds-outset) / var(--flow-zoom, 1));
|
|
420
|
+
border-radius: 0;
|
|
421
|
+
pointer-events: none;
|
|
422
|
+
}
|
|
402
423
|
`;
|
|
403
424
|
/** Inject the base stylesheet once per document (deduped by element id). */
|
|
404
425
|
function useFlowBaseStyles() {
|
|
@@ -444,6 +465,43 @@ function PanePortal(props) {
|
|
|
444
465
|
const target = useFlowLayerTargets().pane;
|
|
445
466
|
return target ? createPortal(props.children, target) : null;
|
|
446
467
|
}
|
|
468
|
+
/** Register an overlay element whose measured edge band fit commands avoid. */
|
|
469
|
+
function useFitViewExclusion(options = {}) {
|
|
470
|
+
const flow = useFlowInstance();
|
|
471
|
+
const root = useFlowLayerTargets().root;
|
|
472
|
+
const [element, setElement] = useState(null);
|
|
473
|
+
const ref = useCallback((next) => setElement(next), []);
|
|
474
|
+
useLayoutEffect(() => {
|
|
475
|
+
if (!root || !element) return;
|
|
476
|
+
element.setAttribute("data-flow-fit-exclusion", options.side ?? "nearest");
|
|
477
|
+
const unregister = flow.viewport.registerFitExclusion({
|
|
478
|
+
side: options.side,
|
|
479
|
+
gap: options.gap,
|
|
480
|
+
getRect: () => {
|
|
481
|
+
if (!element.isConnected || !root.isConnected) return null;
|
|
482
|
+
const rootRect = root.getBoundingClientRect();
|
|
483
|
+
const rect = element.getBoundingClientRect();
|
|
484
|
+
return {
|
|
485
|
+
x: rect.left - rootRect.left,
|
|
486
|
+
y: rect.top - rootRect.top,
|
|
487
|
+
width: rect.width,
|
|
488
|
+
height: rect.height
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
return () => {
|
|
493
|
+
unregister();
|
|
494
|
+
element.removeAttribute("data-flow-fit-exclusion");
|
|
495
|
+
};
|
|
496
|
+
}, [
|
|
497
|
+
element,
|
|
498
|
+
flow,
|
|
499
|
+
options.gap,
|
|
500
|
+
options.side,
|
|
501
|
+
root
|
|
502
|
+
]);
|
|
503
|
+
return ref;
|
|
504
|
+
}
|
|
447
505
|
//#endregion
|
|
448
506
|
//#region src/StatelyFlow.tsx
|
|
449
507
|
function stringArraysEqual(a, b) {
|
|
@@ -459,6 +517,11 @@ function nodePaintEntriesEqual(a, b) {
|
|
|
459
517
|
function classNames$1(...values) {
|
|
460
518
|
return values.filter(Boolean).join(" ");
|
|
461
519
|
}
|
|
520
|
+
function clearEntityFocusOnBlur(flow, entityId, event) {
|
|
521
|
+
const nextTarget = event.relatedTarget;
|
|
522
|
+
if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) return;
|
|
523
|
+
if (flow.getSnapshot().focusedEntityId === entityId) flow.internals.store.trigger.clearFocus();
|
|
524
|
+
}
|
|
462
525
|
function assignRef(ref, value) {
|
|
463
526
|
if (typeof ref === "function") ref(value);
|
|
464
527
|
else if (ref) ref.current = value;
|
|
@@ -495,17 +558,24 @@ function OwnedStatelyFlow(props) {
|
|
|
495
558
|
}
|
|
496
559
|
function StatelyFlowView(props) {
|
|
497
560
|
const containerRef = useRef(null);
|
|
561
|
+
const [rootLayer, setRootLayer] = useState(null);
|
|
498
562
|
const setContainerRef = useCallback((element) => {
|
|
499
563
|
containerRef.current = element;
|
|
564
|
+
setRootLayer(element);
|
|
500
565
|
assignRef(props.rootRef, element);
|
|
501
566
|
}, [props.rootRef]);
|
|
502
567
|
const viewportRef = useRef(null);
|
|
503
568
|
const [viewportLayer, setViewportLayer] = useState(null);
|
|
504
569
|
const [paneLayer, setPaneLayer] = useState(null);
|
|
505
570
|
const layerTargets = useMemo(() => ({
|
|
571
|
+
root: rootLayer,
|
|
506
572
|
viewport: viewportLayer,
|
|
507
573
|
pane: paneLayer
|
|
508
|
-
}), [
|
|
574
|
+
}), [
|
|
575
|
+
rootLayer,
|
|
576
|
+
viewportLayer,
|
|
577
|
+
paneLayer
|
|
578
|
+
]);
|
|
509
579
|
const measurementObserver = useMemo(() => createMeasurementObserver(props.flow.internals.store, { container: () => containerRef.current }), [props.flow]);
|
|
510
580
|
const geometryObserver = useMemo(() => createEntityGeometryObserver(props.flow.internals.store, { getEdgeDependents: props.flow.internals.engine.getEdgeSourceDependents }), [props.flow]);
|
|
511
581
|
useLayoutEffect(() => () => {
|
|
@@ -519,8 +589,8 @@ function StatelyFlowView(props) {
|
|
|
519
589
|
useLayoutEffect(() => {
|
|
520
590
|
const element = containerRef.current;
|
|
521
591
|
if (!element) return;
|
|
522
|
-
return attachContainer(props.flow.internals.store, element);
|
|
523
|
-
}, [props.flow]);
|
|
592
|
+
return attachContainer(props.flow.internals.store, element, { preserveViewportCenter: props.preserveViewportCenterOnResize });
|
|
593
|
+
}, [props.flow, props.preserveViewportCenterOnResize]);
|
|
524
594
|
useFlowInput(props.flow, containerRef, {
|
|
525
595
|
modes: props.inputModes,
|
|
526
596
|
profile: props.settings?.store?.profile,
|
|
@@ -576,7 +646,10 @@ function StatelyFlowView(props) {
|
|
|
576
646
|
ref: viewportRef,
|
|
577
647
|
className: "stately-flow__viewport",
|
|
578
648
|
"data-flow-viewport": true,
|
|
579
|
-
style:
|
|
649
|
+
style: {
|
|
650
|
+
...viewportStyle(),
|
|
651
|
+
isolation: "isolate"
|
|
652
|
+
},
|
|
580
653
|
children: [
|
|
581
654
|
/* @__PURE__ */ jsx(EdgeLayer, {
|
|
582
655
|
geometryObserver,
|
|
@@ -598,6 +671,7 @@ function StatelyFlowView(props) {
|
|
|
598
671
|
projections,
|
|
599
672
|
onlyRenderVisible: props.onlyRenderVisible
|
|
600
673
|
}),
|
|
674
|
+
props.showSelectionBounds ? /* @__PURE__ */ jsx(SelectionBoundsLayer, { geometryObserver }) : null,
|
|
601
675
|
/* @__PURE__ */ jsx(SelectionBoxLayer, {}),
|
|
602
676
|
/* @__PURE__ */ jsx("div", {
|
|
603
677
|
ref: setViewportLayer,
|
|
@@ -607,6 +681,7 @@ function StatelyFlowView(props) {
|
|
|
607
681
|
style: {
|
|
608
682
|
position: "absolute",
|
|
609
683
|
inset: 0,
|
|
684
|
+
zIndex: "var(--flow-layer-interaction, 30)",
|
|
610
685
|
pointerEvents: "none"
|
|
611
686
|
},
|
|
612
687
|
children: props.layers?.viewport
|
|
@@ -621,6 +696,7 @@ function StatelyFlowView(props) {
|
|
|
621
696
|
style: {
|
|
622
697
|
position: "absolute",
|
|
623
698
|
inset: 0,
|
|
699
|
+
zIndex: "var(--flow-layer-chrome, 50)",
|
|
624
700
|
pointerEvents: "none"
|
|
625
701
|
},
|
|
626
702
|
children: props.layers?.pane
|
|
@@ -668,6 +744,7 @@ function SnapLinesLayer() {
|
|
|
668
744
|
inset: 0,
|
|
669
745
|
width: "100%",
|
|
670
746
|
height: "100%",
|
|
747
|
+
zIndex: "var(--flow-layer-guides, 40)",
|
|
671
748
|
overflow: "visible",
|
|
672
749
|
pointerEvents: "none"
|
|
673
750
|
},
|
|
@@ -782,7 +859,39 @@ function SelectionBoxLayer() {
|
|
|
782
859
|
left: Math.min(a.x, b.x),
|
|
783
860
|
top: Math.min(a.y, b.y),
|
|
784
861
|
width: Math.abs(b.x - a.x),
|
|
785
|
-
height: Math.abs(b.y - a.y)
|
|
862
|
+
height: Math.abs(b.y - a.y),
|
|
863
|
+
zIndex: "var(--flow-layer-interaction, 30)"
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
function SelectionBoundsLayer(props) {
|
|
868
|
+
const flow = useFlowInstance();
|
|
869
|
+
const selectedIds = useFlowSelector((context) => [...context.selection], { isEqual: stringArraysEqual });
|
|
870
|
+
const ref = useRef(null);
|
|
871
|
+
const bounds = selectedIds.length > 1 ? flow.getSelectionBounds() : null;
|
|
872
|
+
useLayoutEffect(() => {
|
|
873
|
+
const element = ref.current;
|
|
874
|
+
if (!element || selectedIds.length < 2) return;
|
|
875
|
+
return props.geometryObserver.observeBounds(element, {
|
|
876
|
+
ids: selectedIds,
|
|
877
|
+
getRect: () => flow.getSelectionBounds()
|
|
878
|
+
});
|
|
879
|
+
}, [
|
|
880
|
+
flow,
|
|
881
|
+
props.geometryObserver,
|
|
882
|
+
selectedIds
|
|
883
|
+
]);
|
|
884
|
+
if (!bounds) return null;
|
|
885
|
+
return /* @__PURE__ */ jsx("div", {
|
|
886
|
+
ref,
|
|
887
|
+
"aria-hidden": "true",
|
|
888
|
+
className: "stately-flow__selection-bounds",
|
|
889
|
+
"data-flow-selection-bounds": "",
|
|
890
|
+
style: {
|
|
891
|
+
transform: `translate(${bounds.x}px, ${bounds.y}px)`,
|
|
892
|
+
width: bounds.width,
|
|
893
|
+
height: bounds.height,
|
|
894
|
+
zIndex: "var(--flow-layer-interaction, 30)"
|
|
786
895
|
}
|
|
787
896
|
});
|
|
788
897
|
}
|
|
@@ -800,6 +909,7 @@ function EdgeLayer(props) {
|
|
|
800
909
|
inset: 0,
|
|
801
910
|
width: "100%",
|
|
802
911
|
height: "100%",
|
|
912
|
+
zIndex: "var(--flow-layer-edges, 0)",
|
|
803
913
|
overflow: "visible",
|
|
804
914
|
pointerEvents: "none"
|
|
805
915
|
},
|
|
@@ -860,23 +970,20 @@ const EdgeView = memo(function EdgeView(props) {
|
|
|
860
970
|
"aria-label": props.flow.internals.engine.getAriaLabel(props.id),
|
|
861
971
|
"aria-selected": selected,
|
|
862
972
|
tabIndex: focusable ? 0 : void 0,
|
|
863
|
-
onFocus: () => props.flow.internals.store.trigger.focus({ entityId: props.id })
|
|
973
|
+
onFocus: () => props.flow.internals.store.trigger.focus({ entityId: props.id }),
|
|
974
|
+
onBlur: (event) => clearEntityFocusOnBlur(props.flow, props.id, event)
|
|
864
975
|
};
|
|
865
976
|
if (props.renderEdge) return /* @__PURE__ */ jsxs("g", {
|
|
866
977
|
ref,
|
|
867
978
|
...focusProps,
|
|
868
979
|
"data-entity-id": props.id,
|
|
869
980
|
"data-flow-selected": selected ? "true" : void 0,
|
|
981
|
+
"data-flow-focused": focused ? "true" : void 0,
|
|
870
982
|
className: classNames$1("stately-flow__edge", attrs.className),
|
|
871
|
-
style: {
|
|
872
|
-
pointerEvents: "auto",
|
|
873
|
-
outline: focused ? "2px solid #2563eb" : "none"
|
|
874
|
-
},
|
|
983
|
+
style: { pointerEvents: "auto" },
|
|
875
984
|
children: [/* @__PURE__ */ jsx(EdgeHitTarget, {
|
|
876
|
-
flow: props.flow,
|
|
877
985
|
edge,
|
|
878
|
-
pathData
|
|
879
|
-
selected
|
|
986
|
+
pathData
|
|
880
987
|
}), props.renderEdge({
|
|
881
988
|
flow: props.flow,
|
|
882
989
|
edge,
|
|
@@ -892,11 +999,11 @@ const EdgeView = memo(function EdgeView(props) {
|
|
|
892
999
|
...focusProps,
|
|
893
1000
|
"data-entity-id": props.id,
|
|
894
1001
|
"data-flow-selected": selected ? "true" : void 0,
|
|
1002
|
+
"data-flow-focused": focused ? "true" : void 0,
|
|
895
1003
|
className: classNames$1("stately-flow__edge", attrs.className),
|
|
896
1004
|
style: {
|
|
897
1005
|
pointerEvents: "auto",
|
|
898
|
-
opacity: attrs.opacity
|
|
899
|
-
outline: focused ? "2px solid #2563eb" : "none"
|
|
1006
|
+
opacity: attrs.opacity
|
|
900
1007
|
},
|
|
901
1008
|
children: /* @__PURE__ */ jsx(DefaultEdge, {
|
|
902
1009
|
flow: props.flow,
|
|
@@ -968,7 +1075,7 @@ function DefaultEdge(props) {
|
|
|
968
1075
|
})] });
|
|
969
1076
|
}
|
|
970
1077
|
function EdgeHitTarget(props) {
|
|
971
|
-
return /* @__PURE__ */
|
|
1078
|
+
return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
|
|
972
1079
|
return /* @__PURE__ */ jsx("path", {
|
|
973
1080
|
d: pathToSVG(segment),
|
|
974
1081
|
"data-flow-edge-segment": index,
|
|
@@ -979,7 +1086,7 @@ function EdgeHitTarget(props) {
|
|
|
979
1086
|
strokeWidth: 18,
|
|
980
1087
|
pointerEvents: "stroke"
|
|
981
1088
|
}, index);
|
|
982
|
-
})
|
|
1089
|
+
}) });
|
|
983
1090
|
}
|
|
984
1091
|
function EdgeEndpointHandles(props) {
|
|
985
1092
|
if (!props.selected) return null;
|
|
@@ -1025,6 +1132,7 @@ function EdgeLabelLayer(props) {
|
|
|
1025
1132
|
style: {
|
|
1026
1133
|
position: "absolute",
|
|
1027
1134
|
inset: 0,
|
|
1135
|
+
zIndex: "var(--flow-layer-labels, 20)",
|
|
1028
1136
|
pointerEvents: "none"
|
|
1029
1137
|
},
|
|
1030
1138
|
children: useMemo(() => {
|
|
@@ -1202,6 +1310,7 @@ function NodeLayer(props) {
|
|
|
1202
1310
|
style: {
|
|
1203
1311
|
position: "absolute",
|
|
1204
1312
|
inset: 0,
|
|
1313
|
+
zIndex: "var(--flow-layer-entities, 10)",
|
|
1205
1314
|
transformOrigin: "0 0",
|
|
1206
1315
|
pointerEvents: "none"
|
|
1207
1316
|
},
|
|
@@ -1345,6 +1454,7 @@ const NodeView = memo(function NodeView(props) {
|
|
|
1345
1454
|
ref,
|
|
1346
1455
|
"data-entity-id": props.id,
|
|
1347
1456
|
"data-flow-selected": selected ? "true" : void 0,
|
|
1457
|
+
"data-flow-focused": focused ? "true" : void 0,
|
|
1348
1458
|
className: classNames$1("stately-flow__node", attrs.className),
|
|
1349
1459
|
role: "group",
|
|
1350
1460
|
"aria-roledescription": "flow node",
|
|
@@ -1352,6 +1462,7 @@ const NodeView = memo(function NodeView(props) {
|
|
|
1352
1462
|
"aria-label": props.flow.internals.engine.getAriaLabel(props.id) + (selected ? ", selected" : ""),
|
|
1353
1463
|
tabIndex: focusable ? 0 : void 0,
|
|
1354
1464
|
onFocus: () => props.flow.internals.store.trigger.focus({ entityId: props.id }),
|
|
1465
|
+
onBlur: (event) => clearEntityFocusOnBlur(props.flow, props.id, event),
|
|
1355
1466
|
style: {
|
|
1356
1467
|
...nodePositionStyle(bounds, {
|
|
1357
1468
|
autoSize: contentOwnsSize,
|
|
@@ -1361,8 +1472,6 @@ const NodeView = memo(function NodeView(props) {
|
|
|
1361
1472
|
pointerEvents: "auto",
|
|
1362
1473
|
userSelect: "none",
|
|
1363
1474
|
zIndex: props.orderIndex,
|
|
1364
|
-
outline: focused ? "2px solid #2563eb" : "none",
|
|
1365
|
-
outlineOffset: 2,
|
|
1366
1475
|
opacity: attrs.opacity
|
|
1367
1476
|
},
|
|
1368
1477
|
children: props.renderNode ? props.renderNode({
|
|
@@ -2098,4 +2207,4 @@ function useApplyFlowColorMode(element) {
|
|
|
2098
2207
|
]);
|
|
2099
2208
|
}
|
|
2100
2209
|
//#endregion
|
|
2101
|
-
export { Background, Controls, CrossBackground, DotsBackground, DragHandle, EntityAnchor, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, FlowInstanceProvider, FlowToolbar, LinesBackground, MiniMap, NodeResizer, PanePortal, Panel, Port, StatelyFlow, ViewportPortal, createFlowInstance, useApplyFlowColorMode, useConnection, useCreateFlowInstance, useFlow, useFlowBaseStyles, useFlowColorMode, useFlowConnections, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowView, useFlowViewport, useKeyPress, useMeasuredSize, useNodesInitialized, useOptionalFlowInstance };
|
|
2210
|
+
export { Background, Controls, CrossBackground, DotsBackground, DragHandle, EntityAnchor, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, FlowInstanceProvider, FlowToolbar, LinesBackground, MiniMap, NodeResizer, PanePortal, Panel, Port, StatelyFlow, ViewportPortal, createFlowInstance, useApplyFlowColorMode, useConnection, useCreateFlowInstance, useFitViewExclusion, useFlow, useFlowBaseStyles, useFlowColorMode, useFlowConnections, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowView, useFlowViewport, useKeyPress, useMeasuredSize, useNodesInitialized, useOptionalFlowInstance };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/flow-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Thin React adapter for @statelyai/flow with core and DOM re-exports.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"use-sync-external-store": "^1.6.0",
|
|
37
|
-
"@statelyai/flow": "0.
|
|
38
|
-
"@statelyai/flow-dom": "3.
|
|
37
|
+
"@statelyai/flow": "0.7.0",
|
|
38
|
+
"@statelyai/flow-dom": "3.3.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": "^18.2.0 || ^19.0.0",
|