@ankhorage/zora 25.10.9 → 25.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.d.ts +10 -0
  4. package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.d.ts.map +1 -1
  5. package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js +18 -8
  6. package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js.map +1 -1
  7. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.d.ts +2 -0
  8. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.d.ts.map +1 -1
  9. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js +68 -4
  10. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js.map +1 -1
  11. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphResizeObserver.d.ts +3 -1
  12. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphResizeObserver.d.ts.map +1 -1
  13. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphResizeObserver.js +41 -8
  14. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphResizeObserver.js.map +1 -1
  15. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.d.ts.map +1 -1
  16. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js +6 -2
  17. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js.map +1 -1
  18. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphViewDomCallbacks.d.ts +9 -0
  19. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphViewDomCallbacks.d.ts.map +1 -0
  20. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphViewDomCallbacks.js +36 -0
  21. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphViewDomCallbacks.js.map +1 -0
  22. package/dist/features/graph-view/adapters/inbound/web-artifact/shouldFitGraphAfterLayout.d.ts +2 -2
  23. package/dist/features/graph-view/adapters/inbound/web-artifact/shouldFitGraphAfterLayout.d.ts.map +1 -1
  24. package/dist/features/graph-view/adapters/inbound/web-artifact/shouldFitGraphAfterLayout.js +3 -3
  25. package/dist/features/graph-view/adapters/inbound/web-artifact/shouldFitGraphAfterLayout.js.map +1 -1
  26. package/dist/features/graph-view/constants/events.d.ts +11 -0
  27. package/dist/features/graph-view/constants/events.d.ts.map +1 -0
  28. package/dist/features/graph-view/constants/events.js +10 -0
  29. package/dist/features/graph-view/constants/events.js.map +1 -0
  30. package/dist/features/graph-view/public.d.ts +2 -1
  31. package/dist/features/graph-view/public.d.ts.map +1 -1
  32. package/dist/features/graph-view/public.js +1 -0
  33. package/dist/features/graph-view/public.js.map +1 -1
  34. package/dist/types/graph-view.d.ts +4 -0
  35. package/dist/types/graph-view.d.ts.map +1 -1
  36. package/dist/types/graph-view.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/features/graph-view/adapters/inbound/web-artifact/GraphView.tsx +36 -13
  39. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphController.test.ts +145 -1
  40. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphController.ts +86 -5
  41. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphResizeObserver.test.ts +163 -6
  42. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphResizeObserver.ts +57 -8
  43. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.test.ts +42 -0
  44. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.ts +7 -1
  45. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphViewDomCallbacks.test.ts +77 -0
  46. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphViewDomCallbacks.ts +55 -0
  47. package/src/features/graph-view/adapters/inbound/web-artifact/shouldFitGraphAfterLayout.test.ts +7 -3
  48. package/src/features/graph-view/adapters/inbound/web-artifact/shouldFitGraphAfterLayout.ts +7 -3
  49. package/src/features/graph-view/constants/events.ts +12 -0
  50. package/src/features/graph-view/public.ts +2 -0
  51. package/src/types/graph-view.ts +5 -0
  52. package/web-dist/components/graph-view/GraphView.d.ts +12 -1
  53. package/web-dist/components/graph-view/GraphView.js +152 -25
  54. package/web-dist/components/graph-view/GraphView.types-0.d.ts +4 -0
@@ -0,0 +1,77 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ import { GRAPH_VIEW_EVENTS } from '../../../constants/events';
4
+ import { createGraphViewDomCallbacks } from './createGraphViewDomCallbacks';
5
+
6
+ test('exposes public controller on bubbling ready and layout events without changing React callbacks', () => {
7
+ const host = new EventTarget();
8
+ const controller = createController();
9
+ const observed: string[] = [];
10
+ const browserEvents: string[] = [];
11
+ for (const name of [GRAPH_VIEW_EVENTS.ready, GRAPH_VIEW_EVENTS.layoutComplete]) {
12
+ host.addEventListener(name, (event) => {
13
+ if (!(event instanceof CustomEvent)) throw new Error('Expected a CustomEvent.');
14
+ const detail: unknown = event.detail;
15
+ if (typeof detail !== 'object' || detail === null || !('controller' in detail))
16
+ throw new Error('Expected a public controller in the browser event.');
17
+ expect(detail.controller).toBe(controller);
18
+ expect(event.bubbles).toBe(true);
19
+ browserEvents.push(name);
20
+ });
21
+ }
22
+
23
+ const callbacks = createGraphViewDomCallbacks(
24
+ {
25
+ onReady: (received) => {
26
+ expect(received).toBe(controller);
27
+ observed.push('ready');
28
+ },
29
+ onLayoutComplete: (received) => {
30
+ expect(received).toBe(controller);
31
+ observed.push('layout');
32
+ },
33
+ },
34
+ { current: host },
35
+ );
36
+ callbacks.onReady?.(controller);
37
+ callbacks.onLayoutComplete?.(controller);
38
+
39
+ expect(observed).toEqual(['ready', 'layout']);
40
+ expect(browserEvents).toEqual([GRAPH_VIEW_EVENTS.ready, GRAPH_VIEW_EVENTS.layoutComplete]);
41
+ expect(controller.measureNodes(['src']).nodes[0]?.id).toBe('src');
42
+ });
43
+
44
+ test('preserves React callbacks when the browser host is unmounted', () => {
45
+ const controller = createController();
46
+ const observed: string[] = [];
47
+ const callbacks = createGraphViewDomCallbacks(
48
+ {
49
+ onReady: () => observed.push('ready'),
50
+ onLayoutComplete: () => observed.push('layout'),
51
+ },
52
+ { current: null },
53
+ );
54
+ callbacks.onReady?.(controller);
55
+ callbacks.onLayoutComplete?.(controller);
56
+ expect(observed).toEqual(['ready', 'layout']);
57
+ });
58
+
59
+ /*** Supplies public viewport and rendered geometry without a Cytoscape instance. */
60
+ function createController() {
61
+ return {
62
+ fit: () => undefined,
63
+ measureNodes: (ids: readonly string[]) => ({
64
+ nodes: ids.map((id) => ({
65
+ id,
66
+ position: { x: 120, y: 80 },
67
+ size: { width: 100, height: 48 },
68
+ })),
69
+ viewport: { width: 800, height: 600 },
70
+ }),
71
+ getViewport: () => ({ pan: { x: 0, y: 0 }, zoom: 1 }),
72
+ getZoomRange: () => ({ min: 0.5, max: 2 }),
73
+ setPan: () => undefined,
74
+ setZoom: () => undefined,
75
+ zoomBy: () => undefined,
76
+ };
77
+ }
@@ -0,0 +1,55 @@
1
+ import type {
2
+ GraphViewCallbacks,
3
+ GraphViewController,
4
+ GraphViewDomControllerEventDetail,
5
+ } from '../../../../../types/graph-view';
6
+ import { GRAPH_VIEW_EVENTS, type GraphViewDomControllerEventName } from '../../../constants/events';
7
+
8
+ /***
9
+ * Mirror public GraphView controller callbacks as bubbling browser lifecycle events.
10
+ * Browser hosts can measure actual rendered nodes without reaching into the graph engine.
11
+ */
12
+ export function createGraphViewDomCallbacks(
13
+ callbacks: GraphViewCallbacks,
14
+ hostRef: { readonly current: unknown },
15
+ ): GraphViewCallbacks {
16
+ return {
17
+ ...callbacks,
18
+ onReady(controller) {
19
+ callbacks.onReady?.(controller);
20
+ dispatchGraphViewControllerEvent(hostRef, GRAPH_VIEW_EVENTS.ready, controller);
21
+ },
22
+ onLayoutComplete(controller) {
23
+ callbacks.onLayoutComplete?.(controller);
24
+ dispatchGraphViewControllerEvent(hostRef, GRAPH_VIEW_EVENTS.layoutComplete, controller);
25
+ },
26
+ };
27
+ }
28
+
29
+ /*** Deliver the existing public controller reference on the GraphView root event channel. */
30
+ function dispatchGraphViewControllerEvent(
31
+ hostRef: { readonly current: unknown },
32
+ name: GraphViewDomControllerEventName,
33
+ controller: GraphViewController,
34
+ ): void {
35
+ const host = hostRef.current;
36
+ if (!canDispatchBrowserEvent(host)) return;
37
+ host.dispatchEvent(
38
+ new CustomEvent<GraphViewDomControllerEventDetail>(name, {
39
+ bubbles: true,
40
+ detail: { controller },
41
+ }),
42
+ );
43
+ }
44
+
45
+ /*** Narrow an untrusted browser host to the standard event-dispatch method. */
46
+ function canDispatchBrowserEvent(
47
+ value: unknown,
48
+ ): value is { dispatchEvent(event: Event): boolean } {
49
+ return (
50
+ typeof value === 'object' &&
51
+ value !== null &&
52
+ 'dispatchEvent' in value &&
53
+ typeof value.dispatchEvent === 'function'
54
+ );
55
+ }
@@ -4,14 +4,18 @@ import { shouldFitGraphAfterLayout } from './shouldFitGraphAfterLayout';
4
4
 
5
5
  describe('shouldFitGraphAfterLayout', () => {
6
6
  it('fits the initial layout once', () => {
7
- expect(shouldFitGraphAfterLayout(false, false)).toBe(true);
7
+ expect(shouldFitGraphAfterLayout(false, false, false)).toBe(true);
8
8
  });
9
9
 
10
10
  it('preserves viewport after the graph is ready', () => {
11
- expect(shouldFitGraphAfterLayout(true, false)).toBe(false);
11
+ expect(shouldFitGraphAfterLayout(true, false, false)).toBe(false);
12
12
  });
13
13
 
14
14
  it('fits a changed package depth or filtered topology after readiness', () => {
15
- expect(shouldFitGraphAfterLayout(true, true)).toBe(true);
15
+ expect(shouldFitGraphAfterLayout(true, true, false)).toBe(true);
16
+ });
17
+
18
+ it('fits a newly selected layout algorithm with unchanged topology', () => {
19
+ expect(shouldFitGraphAfterLayout(true, false, true)).toBe(true);
16
20
  });
17
21
  });
@@ -1,4 +1,8 @@
1
- /*** Fit initial or structurally changed graphs while preserving the viewport across spacing updates. */
2
- export function shouldFitGraphAfterLayout(ready: boolean, topologyChanged: boolean): boolean {
3
- return !ready || topologyChanged;
1
+ /*** Fit initial, topology, or layout-algorithm changes while preserving viewport intent otherwise. */
2
+ export function shouldFitGraphAfterLayout(
3
+ ready: boolean,
4
+ topologyChanged: boolean,
5
+ layoutChanged: boolean,
6
+ ): boolean {
7
+ return !ready || topologyChanged || layoutChanged;
4
8
  }
@@ -0,0 +1,12 @@
1
+ /***
2
+ * Canonical public GraphView browser lifecycle event names.
3
+ * Candidate for a shared Ankhorage event strategy: define common naming, typed
4
+ * payloads and dispatch conventions before extracting a cross-component contract.
5
+ */
6
+ export const GRAPH_VIEW_EVENTS = {
7
+ ready: 'zora-graph-view-ready',
8
+ layoutComplete: 'zora-graph-view-layout-complete',
9
+ } as const;
10
+
11
+ export type GraphViewDomControllerEventName =
12
+ (typeof GRAPH_VIEW_EVENTS)[keyof typeof GRAPH_VIEW_EVENTS];
@@ -1,6 +1,7 @@
1
1
  export type {
2
2
  GraphViewCallbacks,
3
3
  GraphViewController,
4
+ GraphViewDomControllerEventDetail,
4
5
  GraphViewEdge,
5
6
  GraphViewElementEvent,
6
7
  GraphViewElementEventType,
@@ -18,3 +19,4 @@ export type {
18
19
  GraphViewViewport,
19
20
  } from '../../types/graph-view';
20
21
  export { GraphView } from './adapters/inbound/web-artifact/GraphView';
22
+ export { GRAPH_VIEW_EVENTS } from './constants/events';
@@ -94,6 +94,11 @@ export interface GraphViewController {
94
94
  zoomBy(factor: number): void;
95
95
  }
96
96
 
97
+ /*** The browser lifecycle events carry only the existing public graph controller. */
98
+ export interface GraphViewDomControllerEventDetail {
99
+ readonly controller: GraphViewController;
100
+ }
101
+
97
102
  export interface GraphViewProps {
98
103
  readonly nodes: readonly GraphViewNode[];
99
104
  readonly edges: readonly GraphViewEdge[];
@@ -10,10 +10,21 @@ import type { GraphViewProps } from './GraphView.types-0';
10
10
  * text. Keep padding in styleRules. Compound and rich-node dimensions retain their existing owners.
11
11
  * minReadableLabelSize Expand the manual zoom ceiling until plain labels reach this CSS-pixel size.
12
12
  * maxFitLabelSize Cap automatic fitting at this plain-label CSS-pixel size, leaving whitespace for tiny graphs.
13
+ * Browser lifecycle events named by public GRAPH_VIEW_EVENTS bubble from the root
14
+ * with `detail.controller`: the same public GraphViewController delivered to React.
15
+ * Browser integrations can call `measureNodes` without accessing the Cytoscape runtime.
16
+ * The standalone browser artifact exposes the same names as `GraphView.events`.
13
17
  * onSpacingFactorChange Receive the spacing accepted by explicit fit({ optimizeSpacing: true }).
14
18
  * Ordinary zoom never adjusts spacing or relayouts. Explicit optimization preserves layout ordering,
15
19
  * stops once effective fit reaches maxFitLabelSize, keeps labels and unrelated compound boundaries
16
20
  * collision-safe, and permits only a small rendered background-overlap fallback for dense graphs.
17
21
  */
18
22
  export declare function GraphView(props: GraphViewProps): React.JSX.Element;
19
- export type { GraphViewCallbacks, GraphViewController, GraphViewEdge, GraphViewElementEvent, GraphViewElementEventType, GraphViewFitOptions, GraphViewLayoutName, GraphViewMeasurement, GraphViewNode, GraphViewNodeMeasurement, GraphViewNodeRenderContext, GraphViewPoint, GraphViewProps, GraphViewRenderedNode, GraphViewSize, GraphViewStyleRule, GraphViewViewport } from './GraphView.types-0';
23
+
24
+ export declare namespace GraphView {
25
+ var events: {
26
+ readonly ready: "zora-graph-view-ready";
27
+ readonly layoutComplete: "zora-graph-view-layout-complete";
28
+ };
29
+ }
30
+ export type { GraphViewCallbacks, GraphViewController, GraphViewDomControllerEventDetail, GraphViewEdge, GraphViewElementEvent, GraphViewElementEventType, GraphViewFitOptions, GraphViewLayoutName, GraphViewMeasurement, GraphViewNode, GraphViewNodeMeasurement, GraphViewNodeRenderContext, GraphViewPoint, GraphViewProps, GraphViewRenderedNode, GraphViewSize, GraphViewStyleRule, GraphViewViewport } from './GraphView.types-0';
@@ -91964,6 +91964,12 @@ Consider installing the package or pass your own 'workerFactory' to ELK's constr
91964
91964
  // src/features/graph-view/adapters/inbound/web-artifact/GraphView.tsx
91965
91965
  import React2 from "react";
91966
91966
 
91967
+ // src/features/graph-view/constants/events.ts
91968
+ var GRAPH_VIEW_EVENTS = {
91969
+ ready: "zora-graph-view-ready",
91970
+ layoutComplete: "zora-graph-view-layout-complete"
91971
+ };
91972
+
91967
91973
  // node_modules/cytoscape/dist/cytoscape.esm.mjs
91968
91974
  function _arrayLikeToArray(r, a) {
91969
91975
  (a == null || a > r.length) && (a = r.length);
@@ -122730,7 +122736,8 @@ function createGraphController(cy, fitPaddingRef, optimizeFit) {
122730
122736
  readableSize: 0,
122731
122737
  fitSize: Infinity,
122732
122738
  readableZoom: 0,
122733
- fitZoom: Infinity
122739
+ fitZoom: Infinity,
122740
+ resizeSnapshot: null
122734
122741
  };
122735
122742
  const controller = {
122736
122743
  fit(options) {
@@ -122780,6 +122787,9 @@ function createGraphController(cy, fitPaddingRef, optimizeFit) {
122780
122787
  getReadableZoomTarget() {
122781
122788
  return state.readableZoom;
122782
122789
  },
122790
+ beginResize() {
122791
+ state.resizeSnapshot = captureResizeSnapshot(cy);
122792
+ },
122783
122793
  settle(fit) {
122784
122794
  const logicalZoom = cy.zoom() / state.scale;
122785
122795
  const fonts = cy.nodes().filter((node) => !node.isParent() && Boolean(node.style("label"))).map((node) => Number.parseFloat(String(node.style("font-size")))).filter((size) => Number.isFinite(size) && size > 0);
@@ -122787,15 +122797,66 @@ function createGraphController(cy, fitPaddingRef, optimizeFit) {
122787
122797
  state.fitZoom = fonts.length ? state.fitSize / fonts.reduce((maximum, size) => Math.max(maximum, size), 0) : Infinity;
122788
122798
  state.scale = state.relative ? Math.min(getNodeFitZoom(cy, fitPaddingRef.current), state.fitZoom) : 1;
122789
122799
  applyZoomLimits(cy, state);
122790
- if (fit)
122800
+ if (fit) {
122801
+ state.resizeSnapshot = null;
122802
+ controller.fit();
122803
+ } else if (settlePreservedViewport(cy, state, logicalZoom))
122791
122804
  controller.fit();
122792
- else if (state.relative) {
122793
- setCenteredZoom(cy, logicalZoom * state.scale);
122794
- cy.center(cy.nodes());
122795
- }
122796
122805
  }
122797
122806
  };
122798
122807
  }
122808
+ function captureResizeSnapshot(cy) {
122809
+ const viewport = { width: cy.width(), height: cy.height() };
122810
+ const zoom = cy.zoom();
122811
+ if (!isUsableViewport(viewport) || !Number.isFinite(zoom) || zoom <= 0)
122812
+ return null;
122813
+ const pan = cy.pan();
122814
+ const renderedCenter = { x: viewport.width / 2, y: viewport.height / 2 };
122815
+ return {
122816
+ center: {
122817
+ x: (renderedCenter.x - pan.x) / zoom,
122818
+ y: (renderedCenter.y - pan.y) / zoom
122819
+ },
122820
+ nodeBounds: getNodeBoundsStatus(cy, viewport)
122821
+ };
122822
+ }
122823
+ function settlePreservedViewport(cy, state, logicalZoom) {
122824
+ const snapshot = state.resizeSnapshot;
122825
+ state.resizeSnapshot = null;
122826
+ const zoom = state.relative ? logicalZoom * state.scale : cy.zoom();
122827
+ if (snapshot === null) {
122828
+ if (state.relative)
122829
+ setCenteredZoom(cy, zoom);
122830
+ return false;
122831
+ }
122832
+ const viewport = { width: cy.width(), height: cy.height() };
122833
+ if (!isUsableViewport(viewport))
122834
+ return false;
122835
+ cy.viewport({
122836
+ zoom,
122837
+ pan: {
122838
+ x: viewport.width / 2 - snapshot.center.x * zoom,
122839
+ y: viewport.height / 2 - snapshot.center.y * zoom
122840
+ }
122841
+ });
122842
+ const nodeBounds = getNodeBoundsStatus(cy, viewport);
122843
+ return nodeBounds === "unusable" || snapshot.nodeBounds === "fit" && nodeBounds === "clipped";
122844
+ }
122845
+ function getNodeBoundsStatus(cy, viewport) {
122846
+ if (cy.nodes().empty())
122847
+ return "empty";
122848
+ if (!isUsableViewport(viewport))
122849
+ return "unusable";
122850
+ const bounds = cy.nodes().renderedBoundingBox();
122851
+ if (![bounds.x1, bounds.x2, bounds.y1, bounds.y2].every(Number.isFinite))
122852
+ return "unusable";
122853
+ const fitsHorizontally = bounds.x1 >= 0 && bounds.x2 <= viewport.width;
122854
+ const fitsVertically = bounds.y1 >= 0 && bounds.y2 <= viewport.height;
122855
+ return fitsHorizontally && fitsVertically ? "fit" : "clipped";
122856
+ }
122857
+ function isUsableViewport(viewport) {
122858
+ return Number.isFinite(viewport.width) && Number.isFinite(viewport.height) && viewport.width > 0 && viewport.height > 0;
122859
+ }
122799
122860
  function measureRenderedNodes(cy, nodeIds) {
122800
122861
  const viewport = { width: cy.width(), height: cy.height() };
122801
122862
  if (viewport.width <= 0 || viewport.height <= 0 || nodeIds.length === 0)
@@ -122894,31 +122955,62 @@ function createGraphResizeObserver(input) {
122894
122955
  if (Observer === null)
122895
122956
  return null;
122896
122957
  const sizeRef = { height: -1, width: -1 };
122958
+ const pendingLayoutResizeRef = { current: false };
122959
+ const pendingResizeSnapshotRef = { current: false };
122960
+ const resizeScheduledRef = { current: false };
122897
122961
  const observer = new Observer((entries) => {
122898
122962
  const entry = entries.at(0);
122899
122963
  if (entry === undefined || !hasSizeChanged(sizeRef, entry.contentRect))
122900
122964
  return;
122901
122965
  sizeRef.height = entry.contentRect.height;
122902
122966
  sizeRef.width = entry.contentRect.width;
122903
- if (input.layoutRunningRef.current)
122904
- return;
122905
122967
  if (!input.readyRef.current && !input.pendingInitialFitRef.current)
122906
122968
  return;
122907
- scheduleResizeFit(input);
122969
+ captureResizeSnapshot2(input, pendingResizeSnapshotRef);
122970
+ if (input.layoutRunningRef.current) {
122971
+ pendingLayoutResizeRef.current = true;
122972
+ return;
122973
+ }
122974
+ scheduleResizeFit(input, pendingLayoutResizeRef, pendingResizeSnapshotRef, resizeScheduledRef);
122908
122975
  });
122909
122976
  observer.observe(input.container);
122910
- return observer;
122977
+ return {
122978
+ disconnect: () => observer.disconnect(),
122979
+ flushAfterLayout: () => flushAfterLayout(input, pendingLayoutResizeRef, pendingResizeSnapshotRef)
122980
+ };
122981
+ }
122982
+ function captureResizeSnapshot2(input, pendingResizeSnapshotRef) {
122983
+ if (pendingResizeSnapshotRef.current)
122984
+ return;
122985
+ input.beginViewportResize();
122986
+ pendingResizeSnapshotRef.current = true;
122987
+ }
122988
+ function flushAfterLayout(input, pendingLayoutResizeRef, pendingResizeSnapshotRef) {
122989
+ if (!pendingLayoutResizeRef.current || input.cy.destroyed() || input.layoutRunningRef.current)
122990
+ return;
122991
+ pendingLayoutResizeRef.current = false;
122992
+ pendingResizeSnapshotRef.current = false;
122993
+ input.cy.resize();
122911
122994
  }
122912
- function scheduleResizeFit(input) {
122995
+ function scheduleResizeFit(input, pendingLayoutResizeRef, pendingResizeSnapshotRef, resizeScheduledRef) {
122996
+ if (resizeScheduledRef.current)
122997
+ return;
122998
+ resizeScheduledRef.current = true;
122913
122999
  scheduleGraphFrame(() => {
122914
- if (input.cy.destroyed() || input.layoutRunningRef.current)
123000
+ resizeScheduledRef.current = false;
123001
+ if (input.cy.destroyed())
123002
+ return;
123003
+ if (input.layoutRunningRef.current) {
123004
+ pendingLayoutResizeRef.current = true;
122915
123005
  return;
123006
+ }
122916
123007
  input.cy.resize();
122917
123008
  if (input.cy.width() <= 0 || input.cy.height() <= 0)
122918
123009
  return;
123010
+ pendingResizeSnapshotRef.current = false;
122919
123011
  const completedPendingInitialFit = input.pendingInitialFitRef.current;
122920
123012
  input.pendingInitialFitRef.current = false;
122921
- input.settleViewport();
123013
+ input.settleViewport(completedPendingInitialFit);
122922
123014
  input.onViewportSettled(completedPendingInitialFit);
122923
123015
  });
122924
123016
  }
@@ -123227,8 +123319,8 @@ function stopGraphLayout(layout, handleStop) {
123227
123319
  }
123228
123320
 
123229
123321
  // src/features/graph-view/adapters/inbound/web-artifact/shouldFitGraphAfterLayout.ts
123230
- function shouldFitGraphAfterLayout(ready, topologyChanged) {
123231
- return !ready || topologyChanged;
123322
+ function shouldFitGraphAfterLayout(ready, topologyChanged, layoutChanged) {
123323
+ return !ready || topologyChanged || layoutChanged;
123232
123324
  }
123233
123325
 
123234
123326
  // src/features/graph-view/adapters/inbound/web-artifact/sizeGraphNodesToLabels.ts
@@ -123378,6 +123470,7 @@ function createRuntimeState(cy, container, callbacksRef) {
123378
123470
  loopSizer: createGraphLoopSizer(cy),
123379
123471
  pendingInitialFitRef: { current: false },
123380
123472
  readyRef,
123473
+ settledLayoutRef: { current: null },
123381
123474
  settledTopologyRef: { current: null },
123382
123475
  relayoutScheduledRef: { current: false },
123383
123476
  renderedNodeListeners
@@ -123386,7 +123479,8 @@ function createRuntimeState(cy, container, callbacksRef) {
123386
123479
  const resizeObserver = createGraphResizeObserver({
123387
123480
  container,
123388
123481
  cy,
123389
- settleViewport: () => viewport.settle(true),
123482
+ beginViewportResize: () => viewport.beginResize(),
123483
+ settleViewport: (fit) => viewport.settle(fit),
123390
123484
  layoutRunningRef,
123391
123485
  pendingInitialFitRef: stateBase.pendingInitialFitRef,
123392
123486
  onViewportSettled: (completedPendingInitialFit) => {
@@ -123462,6 +123556,7 @@ function completeCurrentLayout(state, generation) {
123462
123556
  state.layoutRef.current = null;
123463
123557
  state.cy.resize();
123464
123558
  state.layoutRunningRef.current = false;
123559
+ state.resizeObserver?.flushAfterLayout();
123465
123560
  if (!hasUsableViewport(state.cy)) {
123466
123561
  if (!state.readyRef.current)
123467
123562
  state.pendingInitialFitRef.current = true;
@@ -123472,8 +123567,9 @@ function completeCurrentLayout(state, generation) {
123472
123567
  if (input === null)
123473
123568
  return;
123474
123569
  const topology = getGraphTopologyKey(input.nodes, input.edges);
123475
- const shouldFit = shouldFitGraphAfterLayout(state.readyRef.current, topology !== state.settledTopologyRef.current);
123570
+ const shouldFit = shouldFitGraphAfterLayout(state.readyRef.current, topology !== state.settledTopologyRef.current, input.layout !== state.settledLayoutRef.current);
123476
123571
  state.viewport.settle(shouldFit);
123572
+ state.settledLayoutRef.current = input.layout;
123477
123573
  state.settledTopologyRef.current = topology;
123478
123574
  const fitRequest = state.fitRequestRef.current;
123479
123575
  state.fitRequestRef.current = null;
@@ -123615,6 +123711,33 @@ function destroyRuntime(state) {
123615
123711
  }
123616
123712
  registerGraphLayouts();
123617
123713
 
123714
+ // src/features/graph-view/adapters/inbound/web-artifact/createGraphViewDomCallbacks.ts
123715
+ function createGraphViewDomCallbacks(callbacks, hostRef) {
123716
+ return {
123717
+ ...callbacks,
123718
+ onReady(controller) {
123719
+ callbacks.onReady?.(controller);
123720
+ dispatchGraphViewControllerEvent(hostRef, GRAPH_VIEW_EVENTS.ready, controller);
123721
+ },
123722
+ onLayoutComplete(controller) {
123723
+ callbacks.onLayoutComplete?.(controller);
123724
+ dispatchGraphViewControllerEvent(hostRef, GRAPH_VIEW_EVENTS.layoutComplete, controller);
123725
+ }
123726
+ };
123727
+ }
123728
+ function dispatchGraphViewControllerEvent(hostRef, name, controller) {
123729
+ const host = hostRef.current;
123730
+ if (!canDispatchBrowserEvent(host))
123731
+ return;
123732
+ host.dispatchEvent(new CustomEvent(name, {
123733
+ bubbles: true,
123734
+ detail: { controller }
123735
+ }));
123736
+ }
123737
+ function canDispatchBrowserEvent(value) {
123738
+ return typeof value === "object" && value !== null && "dispatchEvent" in value && typeof value.dispatchEvent === "function";
123739
+ }
123740
+
123618
123741
  // src/features/graph-view/adapters/inbound/web-artifact/GraphNodeOverlay.tsx
123619
123742
  import React from "react";
123620
123743
  import { jsx } from "react/jsx-runtime";
@@ -123700,8 +123823,10 @@ function readResizeObserverConstructor2() {
123700
123823
  // src/features/graph-view/adapters/inbound/web-artifact/GraphView.tsx
123701
123824
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
123702
123825
  function GraphView(props) {
123703
- const { containerRef, renderedNodes, runtimeRef } = useGraphViewRuntime(props);
123826
+ const hostRef = React2.useRef(null);
123827
+ const { containerRef, renderedNodes, runtimeRef } = useGraphViewRuntime(props, hostRef);
123704
123828
  return /* @__PURE__ */ jsxs("div", {
123829
+ ref: hostRef,
123705
123830
  "aria-label": props.ariaLabel ?? "Graph",
123706
123831
  className: props.className,
123707
123832
  role: "application",
@@ -123728,10 +123853,11 @@ function GraphView(props) {
123728
123853
  ]
123729
123854
  });
123730
123855
  }
123731
- function useGraphViewRuntime(props) {
123856
+ GraphView.events = GRAPH_VIEW_EVENTS;
123857
+ function useGraphViewRuntime(props, hostRef) {
123732
123858
  const containerRef = React2.useRef(null);
123733
123859
  const runtimeRef = React2.useRef(null);
123734
- const callbacksRef = useGraphCallbacksRef(props);
123860
+ const callbacksRef = useGraphCallbacksRef(props, hostRef);
123735
123861
  const [renderedNodes, setRenderedNodes] = React2.useState([]);
123736
123862
  useRuntimeLifecycle(containerRef, runtimeRef, callbacksRef);
123737
123863
  useRuntimeGraphUpdate(runtimeRef, props);
@@ -123740,8 +123866,8 @@ function useGraphViewRuntime(props) {
123740
123866
  useKnownNodeSizes(runtimeRef, props.nodes, props.getNodeSize);
123741
123867
  return { containerRef, renderedNodes, runtimeRef };
123742
123868
  }
123743
- function useGraphCallbacksRef(props) {
123744
- const callbacksRef = React2.useRef(readCallbacks(props));
123869
+ function useGraphCallbacksRef(props, hostRef) {
123870
+ const callbacksRef = React2.useRef(createGraphViewDomCallbacks(readCallbacks(props), hostRef));
123745
123871
  const {
123746
123872
  onEdgeEvent,
123747
123873
  onLayoutComplete,
@@ -123751,21 +123877,22 @@ function useGraphCallbacksRef(props) {
123751
123877
  onSpacingFactorChange
123752
123878
  } = props;
123753
123879
  React2.useEffect(() => {
123754
- callbacksRef.current = {
123880
+ callbacksRef.current = createGraphViewDomCallbacks({
123755
123881
  onEdgeEvent,
123756
123882
  onLayoutComplete,
123757
123883
  onNodeEvent,
123758
123884
  onReady,
123759
123885
  onViewportChange,
123760
123886
  onSpacingFactorChange
123761
- };
123887
+ }, hostRef);
123762
123888
  }, [
123763
123889
  onEdgeEvent,
123764
123890
  onLayoutComplete,
123765
123891
  onNodeEvent,
123766
123892
  onReady,
123767
123893
  onViewportChange,
123768
- onSpacingFactorChange
123894
+ onSpacingFactorChange,
123895
+ hostRef
123769
123896
  ]);
123770
123897
  return callbacksRef;
123771
123898
  }
@@ -81,6 +81,10 @@ export interface GraphViewController {
81
81
  setZoom(zoom: number): void;
82
82
  zoomBy(factor: number): void;
83
83
  }
84
+ /*** The browser lifecycle events carry only the existing public graph controller. */
85
+ export interface GraphViewDomControllerEventDetail {
86
+ readonly controller: GraphViewController;
87
+ }
84
88
  export interface GraphViewProps {
85
89
  readonly nodes: readonly GraphViewNode[];
86
90
  readonly edges: readonly GraphViewEdge[];