@statelyai/flow-react 0.9.5 → 0.9.7

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
@@ -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 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.
20
+ Optimization is part of the architecture, not a later cleanup pass. Components subscribe only to durable content and interaction state. During drag and animated layout sessions, `@statelyai/flow-dom` batches node rectangles, default group child-area and header 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 group headers keep projected styles on a separate element from imperative geometry, preserving projected transforms during drag. Default edge paths expose `data-flow-edge-segment`; custom edge renderers can use the same indexed attribute to join the imperative path updates. Existing custom paths whose `d` exactly matches a routed segment are also bound automatically, including duplicate hit strokes; unrelated decoration paths are left alone. 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
 
package/dist/esm/index.js CHANGED
@@ -1588,6 +1588,7 @@ const NodeView = memo(function NodeView(props) {
1588
1588
  highlights
1589
1589
  })
1590
1590
  }) : node.group ? /* @__PURE__ */ jsx(DefaultGroupNode, {
1591
+ geometryObserver: props.geometryObserver,
1591
1592
  flow: props.flow,
1592
1593
  node,
1593
1594
  bounds,
@@ -1615,51 +1616,92 @@ const NodeView = memo(function NodeView(props) {
1615
1616
  * `engine.getGroupGeometry`; custom renderers can do the same.
1616
1617
  */
1617
1618
  function DefaultGroupNode(props) {
1619
+ const childAreaRef = useRef(null);
1620
+ const contentRef = useRef(null);
1621
+ useLayoutEffect(() => {
1622
+ const observePart = (element, part) => {
1623
+ if (!element) return () => {};
1624
+ return props.geometryObserver.observeRect(element, {
1625
+ id: props.node.id,
1626
+ getRect: () => {
1627
+ const geometry = props.flow.internals.engine.getGroupGeometry(props.node.id);
1628
+ const bounds = props.flow.internals.engine.visualBounds(props.node.id);
1629
+ if (!geometry || !bounds) return null;
1630
+ return {
1631
+ ...geometry[part],
1632
+ x: geometry[part].x - bounds.x,
1633
+ y: geometry[part].y - bounds.y
1634
+ };
1635
+ }
1636
+ });
1637
+ };
1638
+ const stopChildArea = observePart(childAreaRef.current, "childArea");
1639
+ const stopContent = observePart(contentRef.current, "contentBounds");
1640
+ return () => {
1641
+ stopChildArea();
1642
+ stopContent();
1643
+ };
1644
+ }, [
1645
+ props.flow,
1646
+ props.geometryObserver,
1647
+ props.node.id
1648
+ ]);
1618
1649
  const geometry = props.flow.internals.engine.getGroupGeometry(props.node.id);
1619
1650
  const label = props.node.data?.label;
1620
1651
  const badges = decorationsToArray(props.attrs.badge);
1621
1652
  const title = decorationTitle(props.attrs.tooltip);
1622
1653
  if (!geometry) return /* @__PURE__ */ jsx(DefaultNode, { ...props });
1623
1654
  const relative = (rect) => ({
1624
- left: rect.x - props.bounds.x,
1625
- top: rect.y - props.bounds.y,
1655
+ left: 0,
1656
+ top: 0,
1657
+ transform: `translate(${rect.x - props.bounds.x}px, ${rect.y - props.bounds.y}px)`,
1626
1658
  width: rect.width,
1627
1659
  height: rect.height
1628
1660
  });
1629
1661
  const dropTarget = props.highlights.some((entry) => entry.kind === "drop-target");
1630
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", { style: {
1631
- position: "absolute",
1632
- ...relative(geometry.childArea),
1633
- boxSizing: "border-box",
1634
- border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : props.attrs.color ?? "#cbd5e1"}`,
1635
- ...dropTarget ? { background: "rgba(22, 163, 74, 0.08)" } : null,
1636
- borderRadius: 10,
1637
- background: "rgba(148, 163, 184, 0.07)",
1638
- pointerEvents: "none"
1639
- } }), /* @__PURE__ */ jsxs("div", {
1640
- "data-flow-drag-handle": true,
1641
- title,
1662
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
1663
+ ref: childAreaRef,
1642
1664
  style: {
1643
1665
  position: "absolute",
1644
- ...relative(geometry.contentBounds),
1666
+ ...relative(geometry.childArea),
1645
1667
  boxSizing: "border-box",
1646
- display: "flex",
1647
- alignItems: "center",
1648
- padding: "0 12px",
1649
- border: `2px solid ${props.selected ? "#2563eb" : props.attrs.color ?? "#94a3b8"}`,
1650
- borderRadius: 8,
1651
- background: props.selected ? "#eff6ff" : "#f8fafc",
1652
- color: "#0f172a",
1653
- font: "600 13px/1.2 system-ui, sans-serif",
1654
- cursor: props.dragging ? "grabbing" : "grab",
1655
- pointerEvents: "auto",
1656
- userSelect: "none",
1657
- ...props.attrs.style
1668
+ border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : props.attrs.color ?? "#cbd5e1"}`,
1669
+ ...dropTarget ? { background: "rgba(22, 163, 74, 0.08)" } : null,
1670
+ borderRadius: 10,
1671
+ background: "rgba(148, 163, 184, 0.07)",
1672
+ pointerEvents: "none"
1673
+ }
1674
+ }), /* @__PURE__ */ jsx("div", {
1675
+ ref: contentRef,
1676
+ style: {
1677
+ position: "absolute",
1678
+ ...relative(geometry.contentBounds)
1658
1679
  },
1659
- children: [/* @__PURE__ */ jsx("span", { children: String(label ?? props.node.id) }), badges.length ? /* @__PURE__ */ jsx("span", {
1660
- style: defaultBadgeListStyle,
1661
- children: badges
1662
- }) : null]
1680
+ children: /* @__PURE__ */ jsxs("div", {
1681
+ "data-flow-drag-handle": true,
1682
+ title,
1683
+ style: {
1684
+ width: "100%",
1685
+ height: "100%",
1686
+ boxSizing: "border-box",
1687
+ display: "flex",
1688
+ alignItems: "center",
1689
+ padding: "0 12px",
1690
+ border: `2px solid ${props.selected ? "#2563eb" : props.attrs.color ?? "#94a3b8"}`,
1691
+ borderRadius: 8,
1692
+ background: props.selected ? "#eff6ff" : "#f8fafc",
1693
+ color: "#0f172a",
1694
+ font: "600 13px/1.2 system-ui, sans-serif",
1695
+ cursor: props.dragging ? "grabbing" : "grab",
1696
+ pointerEvents: "auto",
1697
+ userSelect: "none",
1698
+ ...props.attrs.style
1699
+ },
1700
+ children: [/* @__PURE__ */ jsx("span", { children: String(label ?? props.node.id) }), badges.length ? /* @__PURE__ */ jsx("span", {
1701
+ style: defaultBadgeListStyle,
1702
+ children: badges
1703
+ }) : null]
1704
+ })
1663
1705
  })] });
1664
1706
  }
1665
1707
  function DefaultNode(props) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/flow-react",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
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.10.0",
38
- "@statelyai/flow-dom": "3.4.5"
37
+ "@statelyai/flow": "0.11.0",
38
+ "@statelyai/flow-dom": "3.4.7"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^18.2.0 || ^19.0.0",
@@ -45,6 +45,7 @@
45
45
  "@dagrejs/dagre": "^3.0.0",
46
46
  "@playwright/test": "^1.60.0",
47
47
  "@statelyai/graph": "^2.1.0",
48
+ "@statelyai/layout": "^0.0.5",
48
49
  "@storybook/addon-docs": "^10.1.2",
49
50
  "@storybook/react-vite": "^10.1.2",
50
51
  "@testing-library/react": "^16.1.0",