@statelyai/flow-react 0.9.7 → 0.10.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 CHANGED
@@ -151,8 +151,72 @@ function Flow() {
151
151
  );
152
152
  }
153
153
  ```
154
+ ## Named appearance renderers
155
+
156
+ <!-- appearanceRenderers and renderer props from src/StatelyFlow.tsx; border observation from src/useAppearanceMeasurement.ts -->
157
+
158
+ Pass reusable renderers through `appearanceRenderers`; core resolves the renderer
159
+ key from the entity reference or host default. Existing `renderNode` and
160
+ `renderEdgeLabel` callbacks remain the fallback when a key has no renderer.
161
+
162
+ ```tsx
163
+ <StatelyFlow
164
+ flow={flow}
165
+ appearanceRenderers={{
166
+ asset: {
167
+ renderNode: ({ node, appearance }) => (
168
+ <AssetView data={node.data} options={appearance?.reference?.options} />
169
+ ),
170
+ renderEdgeLabel: ({ edge, appearance }) => (
171
+ <AssetView data={edge.label!.data} options={appearance?.reference?.options} />
172
+ ),
173
+ },
174
+ }}
175
+ />
176
+ ```
177
+
178
+ Put `data-flow-connection-bounds` on the image-border element inside a custom
179
+ renderer. Flow measures it relative to the entity wrapper, independently of the
180
+ full caption/image box, and refreshes on renderer commits, resizing, and image
181
+ loading. The wrapper owns selection and primary drag. Use `draggable={false}`
182
+ and `pointerEvents: 'none'` on primary imagery; use `Port` for authored ports,
183
+ which automatically filters ports forbidden by registered policies. Avoid
184
+ independent hover dimming or gesture handlers on primary imagery.
185
+
186
+ Core policies live in `useFlow({ settings: { engine: { appearances } } })` or
187
+ `createFlowInstance`. Renderer functions remain React-only and never enter saved
188
+ references. See **Features/Appearances → Asset Presentations** in Storybook for
189
+ above/below captions, image-border connections, switching, and edge labels.
190
+
154
191
  ## Storybook
155
192
 
156
193
  ```bash
157
194
  pnpm --filter @statelyai/flow-react storybook
158
195
  ```
196
+
197
+ <!-- connection selector measurement from ../flow-dom/src/appearance-bounds.ts and src/useAppearanceMeasurement.ts -->
198
+
199
+ Appearance definitions can choose the measured border without requiring a reserved
200
+ attribute in custom content:
201
+
202
+ ```ts
203
+ engine: {
204
+ appearances: {
205
+ definitions: {
206
+ asset: {
207
+ sizing: 'content',
208
+ connection: {
209
+ bounds: { selector: '[data-asset-body]' },
210
+ sides: ['left', 'right', 'bottom'],
211
+ },
212
+ },
213
+ },
214
+ defaults: { node: { name: 'asset' } },
215
+ },
216
+ }
217
+ ```
218
+
219
+ Render one descendant with `data-asset-body`. Full visual bounds still include
220
+ captions. Connection bounds track subtree changes and resizing; selectors that
221
+ match zero or multiple descendants use visual bounds. Existing
222
+ `data-flow-connection-bounds` renderers continue to work without configuration.
@@ -1,6 +1,6 @@
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, FitViewExclusionSide, FlowColorMode, FlowCommitEvent, FlowConnection, FlowController, FlowEdge, FlowGraph, FlowInputMode, FlowKeymap, FlowNode, FlowProfile, FlowStoreContext, GraphEdgeAttrs, GraphNodeAttrs, LayoutRequestedEvent, Point, Rect, Viewport, VisualPort } from "@statelyai/flow";
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, ResolvedFlowAppearance, Viewport, VisualPort } from "@statelyai/flow";
4
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
@@ -167,6 +167,7 @@ type ReactGraphNodeAttrs = GraphNodeAttrs<CSSProperties, ReactNode>;
167
167
  type ReactGraphEdgeAttrs = GraphEdgeAttrs<CSSProperties, ReactNode>;
168
168
  type ReactActiveGraphProjection = ActiveGraphProjection<any, any, CSSProperties, ReactNode>;
169
169
  type RenderNodeProps<TNodeData = unknown> = {
170
+ appearance?: ResolvedFlowAppearance;
170
171
  flow: FlowInstance<TNodeData>;
171
172
  node: FlowNode<TNodeData>;
172
173
  bounds: Rect;
@@ -194,6 +195,7 @@ type RenderEdgeProps<TEdgeData = unknown> = {
194
195
  type RenderEdgeLabelProps<TEdgeData = unknown> = RenderEdgeProps<TEdgeData> & {
195
196
  bounds: Rect;
196
197
  dragging: boolean;
198
+ appearance?: ResolvedFlowAppearance;
197
199
  };
198
200
  type RenderConnectionLineProps = {
199
201
  connection: ConnectionState;
@@ -207,7 +209,13 @@ type StatelyFlowRootProps = Omit<ComponentPropsWithoutRef<"div">, "children" | "
207
209
  style?: StatelyFlowStyle;
208
210
  [dataAttribute: `data-${string}`]: string | number | boolean | null | undefined;
209
211
  };
212
+ type ReactAppearanceRenderers<TNodeData = unknown, TEdgeData = unknown> = Record<string, {
213
+ renderNode?: (props: RenderNodeProps<TNodeData>) => ReactNode;
214
+ renderEdgeLabel?: (props: RenderEdgeLabelProps<TEdgeData>) => ReactNode;
215
+ }>;
210
216
  type StatelyFlowBaseProps<TNodeData = unknown, TEdgeData = unknown> = {
217
+ /** Reusable renderers keyed by the core-resolved renderer name. */
218
+ appearanceRenderers?: ReactAppearanceRenderers<TNodeData, TEdgeData>;
211
219
  settings?: FlowInstanceSettings<TNodeData, TEdgeData>;
212
220
  listeners?: FlowStoreListener[];
213
221
  events?: FlowEventListeners;
@@ -433,7 +441,7 @@ type PortProps = {
433
441
  * observe results via `flow.on("connectionEnded", …)` or the store's
434
442
  * `connection` state.
435
443
  */
436
- declare function Port(props: PortProps): import("react").JSX.Element;
444
+ declare function Port(props: PortProps): import("react").JSX.Element | null;
437
445
  //#endregion
438
446
  //#region src/DragHandle.d.ts
439
447
  type DragHandleProps = HTMLAttributes<HTMLDivElement> & {
@@ -497,4 +505,4 @@ declare function useFlowColorMode(): {
497
505
  };
498
506
  declare function useApplyFlowColorMode(element: HTMLElement | null): void;
499
507
  //#endregion
500
- 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, 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 };
508
+ 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, PanePortal, Panel, type PanelPosition, type PanelProps, Port, type PortProps, type ReactActiveGraphProjection, type ReactAppearanceRenderers, 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
@@ -1,6 +1,6 @@
1
1
  import { a as FlowInstanceProvider, i as LinesBackground, n as CrossBackground, o as useFlowInstance, r as DotsBackground, s as useOptionalFlowInstance, t as Background } from "./backgrounds2.js";
2
- import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, clientToCanvas, computeRenderableIds, createEntityGeometryObserver, createFlowView, createMeasurementObserver, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
3
- import { CONNECTION_PREVIEW_EDGE_ID, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getMinimapViewportCenter, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, getVisibleBounds, markerToSVG, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
2
+ import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, clientToCanvas, computeRenderableIds, createEntityGeometryObserver, createFlowView, createMeasurementObserver, nodePositionStyle, observeAppearanceBounds, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
3
+ import { CONNECTION_PREVIEW_EDGE_ID, appearanceMeasurementId, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getMinimapViewportCenter, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, getVisibleBounds, markerToSVG, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
4
4
  import { Fragment, createContext, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
@@ -372,6 +372,27 @@ function useFlowView() {
372
372
  return view;
373
373
  }
374
374
  //#endregion
375
+ //#region src/useAppearanceMeasurement.ts
376
+ function useAppearanceMeasurement(ref, id, appearance, observer) {
377
+ const flow = useFlowInstance();
378
+ const key = appearance ? appearanceMeasurementId(id, appearance) : void 0;
379
+ useLayoutEffect(() => () => {
380
+ if (key && flow.getSnapshot().measurements.has(key)) flow.internals.store.trigger.removeMeasurements({ ids: [key] });
381
+ }, [flow, key]);
382
+ useLayoutEffect(() => {
383
+ const wrapper = ref.current;
384
+ if (!wrapper || !key || !appearance?.registered) return;
385
+ return observeAppearanceBounds(flow.internals.store, wrapper, key, observer, { selector: appearance.connectionSelector });
386
+ }, [
387
+ flow,
388
+ ref,
389
+ key,
390
+ observer,
391
+ appearance?.registered,
392
+ appearance?.connectionSelector
393
+ ]);
394
+ }
395
+ //#endregion
375
396
  //#region src/baseStyles.ts
376
397
  /**
377
398
  * Minimal base stylesheet for the flow canvas. Intentionally tiny and
@@ -718,6 +739,7 @@ function StatelyFlowView(props) {
718
739
  }),
719
740
  /* @__PURE__ */ jsx(NodeLayer, {
720
741
  geometryObserver,
742
+ appearanceRenderers: props.appearanceRenderers,
721
743
  renderNode: props.renderNode,
722
744
  projections,
723
745
  onlyRenderVisible: props.onlyRenderVisible
@@ -725,6 +747,7 @@ function StatelyFlowView(props) {
725
747
  /* @__PURE__ */ jsx(SnapLinesLayer, {}),
726
748
  /* @__PURE__ */ jsx(EdgeLabelLayer, {
727
749
  geometryObserver,
750
+ appearanceRenderers: props.appearanceRenderers,
728
751
  renderEdgeLabel: props.renderEdgeLabel,
729
752
  projections,
730
753
  onlyRenderVisible: props.onlyRenderVisible
@@ -994,6 +1017,7 @@ const EdgeView = memo(function EdgeView(props) {
994
1017
  const ref = useRef(null);
995
1018
  const { graph, edge, pathData, selected, focused, hovered, highlights } = useFlowEntity(props.id, (context) => ({
996
1019
  graph: context.graph,
1020
+ configRevision: context.configRevision,
997
1021
  edge: props.flow.getEdge(props.id) ?? null,
998
1022
  pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
999
1023
  geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
@@ -1002,7 +1026,7 @@ const EdgeView = memo(function EdgeView(props) {
1002
1026
  hovered: context.hoveredEntityIds.has(props.id),
1003
1027
  highlights: props.flow.internals.engine.getHighlights(props.id)
1004
1028
  }), { isEqual: (a, b) => {
1005
- if (!(a.selected === b.selected && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
1029
+ if (!(a.configRevision === b.configRevision && a.selected === b.selected && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
1006
1030
  if (a.geometryActive !== b.geometryActive) return b.geometryActive ? edgeRenderContentEqual(a.edge, b.edge) : false;
1007
1031
  if (a.geometryActive) return edgeRenderContentEqual(a.edge, b.edge);
1008
1032
  return a.graph === b.graph && a.edge === b.edge && a.pathData === b.pathData;
@@ -1266,6 +1290,7 @@ function EdgeLabelLayer(props) {
1266
1290
  geometryObserver: props.geometryObserver,
1267
1291
  id,
1268
1292
  projections: props.projections,
1293
+ appearanceRenderers: props.appearanceRenderers,
1269
1294
  renderEdgeLabel
1270
1295
  }, id);
1271
1296
  })
@@ -1274,6 +1299,7 @@ function EdgeLabelLayer(props) {
1274
1299
  const EdgeLabelItem = memo(function EdgeLabelItem(props) {
1275
1300
  const { graph, edge, pathData, selected, dragging, hovered, highlights } = useFlowEntity(props.id, (context) => ({
1276
1301
  graph: context.graph,
1302
+ configRevision: context.configRevision,
1277
1303
  edge: props.flow.getEdge(props.id) ?? null,
1278
1304
  pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
1279
1305
  geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
@@ -1282,7 +1308,7 @@ const EdgeLabelItem = memo(function EdgeLabelItem(props) {
1282
1308
  hovered: context.hoveredEntityIds.has(props.id),
1283
1309
  highlights: props.flow.internals.engine.getHighlights(props.id)
1284
1310
  }), { isEqual: (a, b) => {
1285
- if (!(a.selected === b.selected && a.dragging === b.dragging && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
1311
+ if (!(a.configRevision === b.configRevision && a.selected === b.selected && a.dragging === b.dragging && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
1286
1312
  if (a.geometryActive !== b.geometryActive) return b.geometryActive ? edgeRenderContentEqual(a.edge, b.edge) : false;
1287
1313
  if (a.geometryActive) return edgeRenderContentEqual(a.edge, b.edge);
1288
1314
  return a.graph === b.graph && a.edge === b.edge && a.pathData === b.pathData;
@@ -1307,12 +1333,16 @@ const EdgeLabelItem = memo(function EdgeLabelItem(props) {
1307
1333
  hovered,
1308
1334
  highlights,
1309
1335
  projections: props.projections,
1336
+ appearanceRenderers: props.appearanceRenderers,
1310
1337
  renderEdgeLabel: props.renderEdgeLabel
1311
1338
  });
1312
1339
  });
1313
1340
  function EdgeLabelView(props) {
1314
1341
  const ref = useRef(null);
1315
1342
  const observer = useMeasurementObserver();
1343
+ const appearance = props.edge.label ? props.flow.internals.engine.resolveAppearance(props.edge.label) : void 0;
1344
+ const renderEdgeLabel = appearance?.renderer && props.appearanceRenderers?.[appearance.renderer]?.renderEdgeLabel || props.renderEdgeLabel;
1345
+ useAppearanceMeasurement(ref, props.edge.id, appearance, observer);
1316
1346
  useLayoutEffect(() => {
1317
1347
  const element = ref.current;
1318
1348
  if (!element) return;
@@ -1324,7 +1354,7 @@ function EdgeLabelView(props) {
1324
1354
  if (!element) return;
1325
1355
  return props.geometryObserver.observeRect(element, {
1326
1356
  id: props.edge.id,
1327
- autoSize: props.edge.label?.autoSize,
1357
+ autoSize: appearance?.autoSize,
1328
1358
  getRect: () => {
1329
1359
  const pathData = props.flow.getAllEdgePaths().get(props.edge.id);
1330
1360
  if (!pathData) return null;
@@ -1340,14 +1370,15 @@ function EdgeLabelView(props) {
1340
1370
  });
1341
1371
  }, [
1342
1372
  props.edge.id,
1343
- props.edge.label?.autoSize,
1373
+ appearance?.autoSize,
1344
1374
  props.flow,
1345
1375
  props.geometryObserver,
1346
1376
  props.projections
1347
1377
  ]);
1348
1378
  const text = (props.edge.label?.data)?.text;
1349
1379
  const label = props.attrs.label ?? String(text ?? props.edge.id);
1350
- const customLabel = props.renderEdgeLabel?.({
1380
+ const customLabel = renderEdgeLabel?.({
1381
+ appearance,
1351
1382
  flow: props.flow,
1352
1383
  edge: props.edge,
1353
1384
  pathData: props.pathData,
@@ -1358,7 +1389,7 @@ function EdgeLabelView(props) {
1358
1389
  hovered: props.hovered,
1359
1390
  highlights: props.highlights
1360
1391
  });
1361
- const defaultStyle = props.renderEdgeLabel ? void 0 : {
1392
+ const defaultStyle = renderEdgeLabel ? void 0 : {
1362
1393
  padding: "4px 8px",
1363
1394
  border: `1px solid ${props.selected ? "#2563eb" : props.attrs.color ?? "#cbd5e1"}`,
1364
1395
  borderRadius: 999,
@@ -1373,6 +1404,7 @@ function EdgeLabelView(props) {
1373
1404
  ref,
1374
1405
  "data-entity-id": props.edge.id,
1375
1406
  "data-flow-edge-label": "",
1407
+ "data-flow-appearance": appearance?.registered ? appearance.reference?.name : void 0,
1376
1408
  "data-flow-selected": props.selected ? "true" : void 0,
1377
1409
  "data-flow-dragging": props.dragging ? "true" : void 0,
1378
1410
  className: classNames$1("stately-flow__edge-label", props.attrs.className),
@@ -1381,8 +1413,8 @@ function EdgeLabelView(props) {
1381
1413
  left: 0,
1382
1414
  top: 0,
1383
1415
  transform: `translate(${props.bounds.x}px, ${props.bounds.y}px)`,
1384
- width: props.edge.label?.autoSize ? void 0 : props.bounds.width,
1385
- height: props.edge.label?.autoSize ? void 0 : props.bounds.height,
1416
+ width: appearance?.autoSize ? void 0 : props.bounds.width,
1417
+ height: appearance?.autoSize ? void 0 : props.bounds.height,
1386
1418
  boxSizing: "border-box",
1387
1419
  ...defaultStyle,
1388
1420
  pointerEvents: "auto",
@@ -1390,7 +1422,7 @@ function EdgeLabelView(props) {
1390
1422
  opacity: props.attrs.opacity,
1391
1423
  ...props.attrs.style
1392
1424
  },
1393
- children: props.renderEdgeLabel ? customLabel : label
1425
+ children: renderEdgeLabel ? customLabel : label
1394
1426
  });
1395
1427
  }
1396
1428
  function projectedEdgeLabelBounds(pathData, label) {
@@ -1439,6 +1471,7 @@ function NodeLayer(props) {
1439
1471
  zIndex,
1440
1472
  observer,
1441
1473
  projections: props.projections,
1474
+ appearanceRenderers: props.appearanceRenderers,
1442
1475
  renderNode: props.renderNode
1443
1476
  }, id);
1444
1477
  })
@@ -1483,6 +1516,7 @@ const NodeView = memo(function NodeView(props) {
1483
1516
  const current = props.flow.getNode(props.id) ?? null;
1484
1517
  return {
1485
1518
  graph: context.graph,
1519
+ configRevision: context.configRevision,
1486
1520
  node: current,
1487
1521
  geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
1488
1522
  hidden: !current || props.flow.internals.engine.resolveEntityConfig(current).hidden,
@@ -1494,12 +1528,15 @@ const NodeView = memo(function NodeView(props) {
1494
1528
  bounds: context.resizingEntityId === props.id && context.resizeCorners ? props.flow.internals.engine.getResizeBounds() ?? props.flow.internals.engine.visualBounds(props.id) : props.flow.internals.engine.visualBounds(props.id)
1495
1529
  };
1496
1530
  }, { isEqual: (a, b) => {
1497
- if (!(a.hidden === b.hidden && a.selected === b.selected && a.dragging === b.dragging && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
1531
+ if (!(a.configRevision === b.configRevision && a.hidden === b.hidden && a.selected === b.selected && a.dragging === b.dragging && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
1498
1532
  if (a.geometryActive !== b.geometryActive) return b.geometryActive ? nodeRenderContentEqual(a.node, b.node) : false;
1499
1533
  if (a.geometryActive) return nodeRenderContentEqual(a.node, b.node);
1500
1534
  return a.graph === b.graph && a.node === b.node && rectsShallowEqual(a.bounds, b.bounds);
1501
1535
  } });
1502
- const contentOwnsSize = Boolean(node?.autoSize && node.style?.customWidth === void 0 && node.style?.customHeight === void 0);
1536
+ const appearance = node ? props.flow.internals.engine.resolveAppearance(node) : void 0;
1537
+ useAppearanceMeasurement(ref, props.id, appearance, props.observer);
1538
+ const renderNode = appearance?.renderer && props.appearanceRenderers?.[appearance.renderer]?.renderNode || props.renderNode;
1539
+ const contentOwnsSize = Boolean(node && props.flow.internals.engine.resolveAppearance(node).autoSize && node.style?.customWidth === void 0 && node.style?.customHeight === void 0);
1503
1540
  useLayoutEffect(() => {
1504
1541
  const element = ref.current;
1505
1542
  if (!element) return;
@@ -1546,6 +1583,7 @@ const NodeView = memo(function NodeView(props) {
1546
1583
  },
1547
1584
  children: /* @__PURE__ */ jsx("div", {
1548
1585
  ref,
1586
+ "data-flow-appearance": appearance?.registered ? appearance.reference?.name : void 0,
1549
1587
  "data-entity-id": props.id,
1550
1588
  "data-flow-selected": selected ? "true" : void 0,
1551
1589
  "data-flow-focused": focused ? "true" : void 0,
@@ -1571,13 +1609,14 @@ const NodeView = memo(function NodeView(props) {
1571
1609
  zIndex: props.zIndex,
1572
1610
  opacity: attrs.opacity
1573
1611
  },
1574
- children: props.renderNode ? /* @__PURE__ */ jsx("div", {
1612
+ children: renderNode ? /* @__PURE__ */ jsx("div", {
1575
1613
  "data-flow-custom-node": true,
1576
1614
  style: {
1577
1615
  display: "contents",
1578
1616
  pointerEvents: "none"
1579
1617
  },
1580
- children: props.renderNode({
1618
+ children: renderNode({
1619
+ appearance,
1581
1620
  flow: props.flow,
1582
1621
  node,
1583
1622
  bounds,
@@ -2298,6 +2337,7 @@ function Port(props) {
2298
2337
  const observer = useMeasurementObserver();
2299
2338
  const measured = props.measured ?? true;
2300
2339
  const interaction = props.interaction ?? "connect";
2340
+ const permitted = interaction !== "connect" || !flow.internals.engine.resolveAppearance(node).registered || flow.internals.engine.isPortAllowed(node, props.name);
2301
2341
  const port = node.ports?.find((candidate) => candidate.name === props.name);
2302
2342
  const portId = `${node.id}:${props.name}`;
2303
2343
  const focusable = flow.internals.engine.resolveEntityConfig(node).focusable;
@@ -2318,7 +2358,8 @@ function Port(props) {
2318
2358
  observer,
2319
2359
  node.id,
2320
2360
  props.name,
2321
- measured
2361
+ measured,
2362
+ permitted
2322
2363
  ]);
2323
2364
  useLayoutEffect(() => {
2324
2365
  const element = ref.current;
@@ -2332,6 +2373,7 @@ function Port(props) {
2332
2373
  if (connection.targetId !== node.id || connection.targetPort !== props.name) return null;
2333
2374
  return connection.isValid === false ? "invalid" : "valid";
2334
2375
  }, { flow });
2376
+ if (!permitted) return null;
2335
2377
  return /* @__PURE__ */ jsx("div", {
2336
2378
  ref,
2337
2379
  className: props.className,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/flow-react",
3
- "version": "0.9.7",
3
+ "version": "0.10.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.11.0",
38
- "@statelyai/flow-dom": "3.4.7"
37
+ "@statelyai/flow": "0.12.0",
38
+ "@statelyai/flow-dom": "3.5.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^18.2.0 || ^19.0.0",