f1ow 1.0.0 → 1.1.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.
@@ -0,0 +1,8 @@
1
+ import { default as Konva } from 'konva';
2
+ /**
3
+ * Animate dashOffset on a Konva node to create a "flowing" dash effect.
4
+ * @param nodeRef - React ref to a Konva Line or Shape node
5
+ * @param enabled - whether animation is active
6
+ * @param speed - pixels per second (default 40)
7
+ */
8
+ export declare function useFlowAnimation(nodeRef: React.RefObject<Konva.Line | Konva.Shape | null>, enabled: boolean, speed?: number): void;
@@ -1,8 +1,9 @@
1
- import { CanvasElement, ElementStyle, ToolType } from '../types';
1
+ import { CanvasElement, ElementStyle, ToolType, ConnectionConfig } from '../types';
2
2
  import { ContextMenuItem } from '../components/ContextMenu/ContextMenu';
3
3
  import { CollaborationConfig } from '../collaboration/types';
4
4
  import { CustomElementConfig } from '../utils/elementRegistry';
5
5
  import { RenderAnnotationFn } from '../components/Canvas/AnnotationsOverlay';
6
+ import { CanvasStore } from '../store/useCanvasStore';
6
7
  export type { ContextMenuItem };
7
8
  /** Context passed to custom context menu renderers */
8
9
  export interface ContextMenuContext {
@@ -122,6 +123,19 @@ export interface FlowCanvasProps {
122
123
  * Pass a `CollaborationConfig` to connect, or `undefined`/`null` to disable.
123
124
  */
124
125
  collaboration?: CollaborationConfig | null;
126
+ /**
127
+ * Optional canvas store instance produced by `createCanvasStore()`.
128
+ * When supplied, this `<FlowCanvas>` and its descendant React
129
+ * subscribers (Toolbar, StylePanel, overlays) read state from this
130
+ * isolated store instead of the module-level singleton, allowing
131
+ * multiple canvases to coexist on the same page without cross-talk.
132
+ *
133
+ * Note: tools, keyboard shortcuts, and the collaboration sync bridge
134
+ * still read from the singleton via `getState()`. Until that wiring
135
+ * is migrated, those subsystems target the singleton even when this
136
+ * prop is supplied.
137
+ */
138
+ store?: CanvasStore;
125
139
  /**
126
140
  * Register custom element types for this canvas instance.
127
141
  *
@@ -143,6 +157,22 @@ export interface FlowCanvasProps {
143
157
  * ```
144
158
  */
145
159
  customElementTypes?: CustomElementConfig[];
160
+ /**
161
+ * Configure the connection/binding system behavior.
162
+ * Controls snap thresholds, port visibility, default line styles, and more.
163
+ *
164
+ * @example
165
+ * ```tsx
166
+ * <FlowCanvas
167
+ * connectionConfig={{
168
+ * enablePorts: true,
169
+ * snapThreshold: 20,
170
+ * defaultLineType: 'elbow',
171
+ * }}
172
+ * />
173
+ * ```
174
+ */
175
+ connectionConfig?: ConnectionConfig;
146
176
  /**
147
177
  * Configure Web Workers for background processing (elbow routing, SVG export).
148
178
  *
@@ -0,0 +1,10 @@
1
+ export type { CollaborationUser, AwarenessState, CollaborationConfig, ConnectionStatus, CollaborationEvent, } from '../collaboration/types';
2
+ export { createCollaborationProvider, destroyCollaborationProvider, getYDoc, getYProvider, getYElements, isCollaborationActive, onStatusChange, updateAwareness, getRemoteAwareness, } from '../collaboration/yjsProvider';
3
+ export { startSync, stopSync } from '../collaboration/syncBridge';
4
+ export { elementToYMap, yMapToElement, SYNC_FIELDS, STYLE_FIELDS } from '../collaboration/syncBridgeCodec';
5
+ export { CollaborationManager } from '../collaboration/CollaborationManager';
6
+ export { SyncWorkerAdapter } from '../collaboration/syncWorker';
7
+ export type { WorkerInMessage, WorkerOutMessage, SyncWorkerCallbacks } from '../collaboration/syncWorker';
8
+ export { useCollaboration } from '../collaboration/useCollaboration';
9
+ export type { UseCollaborationReturn } from '../collaboration/useCollaboration';
10
+ export { default as CursorOverlay } from '../collaboration/CursorOverlay';
@@ -2,20 +2,23 @@ export { default as FlowCanvas } from './FlowCanvas';
2
2
  export type { FlowCanvasProps, FlowCanvasRef, FlowCanvasTheme, ContextMenuItem, ContextMenuContext, } from './FlowCanvasProps';
3
3
  export { DEFAULT_THEME } from './FlowCanvasProps';
4
4
  export type { AnnotationContext, AnnotationScreenBounds, RenderAnnotationFn, } from '../components/Canvas/AnnotationsOverlay';
5
- export type { CanvasElement, RectangleElement, EllipseElement, DiamondElement, LineElement, ArrowElement, FreeDrawElement, TextElement, ImageElement, BaseElement, ElementStyle, ElementType, ToolType, Point, ViewportState, ConnectionAnchor, BoundElement, Binding, SnapTarget, Arrowhead, LineType, TextAlign, VerticalAlign, ImageScaleMode, ImageCrop, ElementMeta, CanvasOperation, } from '../types';
6
- export { useCanvasStore } from '../store/useCanvasStore';
5
+ export type { CanvasElement, RectangleElement, EllipseElement, DiamondElement, LineElement, ArrowElement, FreeDrawElement, TextElement, ImageElement, BaseElement, ElementStyle, ElementType, ToolType, Point, ViewportState, ConnectionAnchor, BoundElement, Binding, SnapTarget, Arrowhead, LineType, TextAlign, VerticalAlign, ImageScaleMode, ImageCrop, ElementMeta, CanvasOperation, AnchorId, Port, SnapMode, LineGradient, LineTaper, LineStyleExtension, ConnectionConfig, } from '../types';
6
+ export { useCanvasStore, createCanvasStore } from '../store/useCanvasStore';
7
+ export type { CanvasStore } from '../store/useCanvasStore';
8
+ export { CanvasStoreProvider, useCanvasStoreInstance } from '../store/CanvasStoreContext';
9
+ export type { CanvasStoreProviderProps } from '../store/CanvasStoreContext';
7
10
  export { DEFAULT_STYLE, STROKE_COLORS, FILL_COLORS, STROKE_WIDTHS, TOOLS, ARROWHEAD_TYPES, LINE_TYPES, ROUGHNESS_CONFIGS } from '../constants';
8
11
  export { generateId } from '../utils/id';
9
12
  export { distance, normalizeRect, rotatePoint, isPointInRect, getDiamondPoints, getStrokeDash } from '../utils/geometry';
10
13
  export { exportToDataURL, downloadPNG, exportToJSON, downloadJSON, exportToSVG, downloadSVG } from '../utils/export';
11
14
  export { drawArrowhead, arrowheadSize, flatToPoints } from '../utils/arrowheads';
12
15
  export { computeCurveControlPoint, quadBezierAt, quadBezierTangent, curveArrowPrev, CURVE_RATIO } from '../utils/curve';
16
+ export { anchorToFixedPoint, fixedPointToAnchor, resolvePort, resolveBindingPoint, createBindingFromSnap, isBindingStale, findNearestSnapTarget, recomputeBoundPoints, computeFixedPoint, getEdgePointFromFixedPoint, isConnectable, getConnectionPoints, getEdgePoint, getAnchorPosition, findConnectorsForElement, addBoundElement, removeBoundElement, syncBoundElements, } from '../utils/connection';
13
17
  export { LABEL_PADDING_H, LABEL_PADDING_V, LABEL_CORNER, LABEL_LINE_HEIGHT, LABEL_MIN_WIDTH, measureLabelText, computePillSize } from '../utils/labelMetrics';
14
18
  export { elementRegistry, registerCustomElement } from '../utils/elementRegistry';
15
19
  export type { CustomElementConfig, ValidationResult } from '../utils/elementRegistry';
16
20
  export { computeElbowPoints, computeElbowRoute, simplifyElbowPath, clearElbowRouteCache, directionFromFixedPoint, directionFromPoints, directionFromShapeToPoint, directionFromEdgePoint, getElbowPreferredDirection, } from '../utils/elbow';
17
21
  export type { Direction } from '../utils/elbow';
18
- export { getConnectionPoints, getEdgePoint, getEdgePointFromFixedPoint, computeFixedPoint, getAnchorPosition, findNearestSnapTarget, isConnectable, recomputeBoundPoints, findConnectorsForElement, addBoundElement, removeBoundElement, syncBoundElements, } from '../utils/connection';
19
22
  export { fileToDataURL, loadImage, computeImageElementDimensions, createImageElement, getImageFilesFromDataTransfer, extractImageDataFromClipboard, clipboardHasImage, resolveImageSource, openImageFilePicker, } from '../utils/image';
20
23
  export { getVisibleBounds, getElementAABB, aabbOverlaps, cullToViewport, buildElementMap, cloneElementsForHistory, rafThrottle, batchElementUpdates, } from '../utils/performance';
21
24
  export type { AABB } from '../utils/performance';
@@ -41,19 +44,13 @@ export { generateKeyBetween, generateNKeysBetween, isValidFractionalIndex, compa
41
44
  export { OperationLog, opAdd, opDelete, opMove, opResize, opStyle, opRotate, opReorder, opUpdatePoints, opSetText, opBatch, applyOperation, detectOperations, } from '../utils/crdtPrep';
42
45
  export type { OperationEntry } from '../utils/crdtPrep';
43
46
  export type { CollaborationUser, AwarenessState, CollaborationConfig, ConnectionStatus, CollaborationEvent, } from '../collaboration/types';
44
- export { createCollaborationProvider, destroyCollaborationProvider, getYDoc, getYProvider, getYElements, isCollaborationActive, onStatusChange, updateAwareness, getRemoteAwareness, } from '../collaboration/yjsProvider';
45
- export { startSync, stopSync } from '../collaboration/syncBridge';
46
- export { elementToYMap, yMapToElement, SYNC_FIELDS, STYLE_FIELDS } from '../collaboration/syncBridgeCodec';
47
- export { CollaborationManager } from '../collaboration/CollaborationManager';
48
- export { SyncWorkerAdapter } from '../collaboration/syncWorker';
49
- export type { WorkerInMessage, WorkerOutMessage, SyncWorkerCallbacks } from '../collaboration/syncWorker';
50
47
  export { useCollaboration } from '../collaboration/useCollaboration';
51
48
  export type { UseCollaborationReturn } from '../collaboration/useCollaboration';
52
49
  export { default as CursorOverlay } from '../collaboration/CursorOverlay';
53
50
  export { TileCache, tileKey } from '../rendering/tileCache';
54
51
  export type { TileCoord } from '../rendering/tileCache';
55
52
  export { TileRenderer, TILE_SIZE, discreteZoom, worldTileSize, getVisibleTiles, tileBounds, getElementTiles, } from '../rendering/tileRenderer';
56
- export type { TileDrawFn, TileRendererOptions } from '../rendering/tileRenderer';
53
+ export type { TileDrawFn, TileRendererOptions, TileSpatialQuery } from '../rendering/tileRenderer';
57
54
  export { useTileRenderer } from '../rendering/useTileRenderer';
58
55
  export type { UseTileRendererOptions, UseTileRendererReturn } from '../rendering/useTileRenderer';
59
56
  export { TextureAtlas } from '../webgl/textureAtlas';
@@ -0,0 +1,14 @@
1
+ import { ReactNode } from 'react';
2
+ import { CanvasStore } from './useCanvasStore';
3
+ export interface CanvasStoreProviderProps {
4
+ /** Store instance produced by `createCanvasStore()`. */
5
+ store: CanvasStore;
6
+ children: ReactNode;
7
+ }
8
+ export declare function CanvasStoreProvider({ store, children }: CanvasStoreProviderProps): import("react/jsx-runtime").JSX.Element;
9
+ /**
10
+ * Resolve the canvas store for the current React subtree. Falls back to
11
+ * the singleton `useCanvasStore` when no provider is mounted, so existing
12
+ * single-instance apps work without changes.
13
+ */
14
+ export declare function useCanvasStoreInstance(): CanvasStore;
@@ -15,11 +15,17 @@ interface ElementDiff {
15
15
  */
16
16
  interface HistoryEntry {
17
17
  diffs: ElementDiff[];
18
+ /** Element order before this entry, used to restore z-order changes. */
19
+ beforeOrder?: string[];
20
+ /** Element order after this entry, used to redo z-order changes. */
21
+ afterOrder?: string[];
18
22
  /** Optional named mark/checkpoint for grouping */
19
23
  mark?: string;
20
24
  /** Timestamp for squash heuristics */
21
25
  timestamp: number;
22
26
  }
27
+ type AlignMode = 'left' | 'centerH' | 'right' | 'top' | 'centerV' | 'bottom';
28
+ type FlipAxis = 'horizontal' | 'vertical';
23
29
  interface CanvasState {
24
30
  elements: CanvasElement[];
25
31
  selectedIds: string[];
@@ -35,6 +41,8 @@ interface CanvasState {
35
41
  historyIndex: number;
36
42
  /** Baseline snapshot for computing diffs against current state */
37
43
  _historyBaseline: Map<string, CanvasElement>;
44
+ /** Baseline element order for z-order history. */
45
+ _historyOrderBaseline: string[];
38
46
  /** Whether history recording is temporarily paused */
39
47
  _historyPaused: boolean;
40
48
  showGrid: boolean;
@@ -55,6 +63,9 @@ interface CanvasState {
55
63
  sendToBack: (ids: string[]) => void;
56
64
  bringForward: (ids: string[]) => void;
57
65
  sendBackward: (ids: string[]) => void;
66
+ alignElements: (ids: string[], mode: AlignMode) => void;
67
+ rotateElements: (ids: string[], deltaDegrees: number) => void;
68
+ flipElements: (ids: string[], axis: FlipAxis) => void;
58
69
  toggleLockElements: (ids: string[]) => void;
59
70
  groupElements: (ids: string[]) => void;
60
71
  ungroupElements: (ids: string[]) => void;
@@ -123,5 +134,26 @@ interface CanvasState {
123
134
  canRedo: () => boolean;
124
135
  toggleGrid: () => void;
125
136
  }
137
+ /**
138
+ * Factory: create a fresh canvas store instance.
139
+ *
140
+ * Each call returns an independent Zustand store with its own elements,
141
+ * selection, viewport, and history. Use this with `CanvasStoreProvider`
142
+ * to render multiple `<FlowCanvas>` instances side-by-side without state
143
+ * cross-talk on the React subscriber side.
144
+ *
145
+ * Note: the module-level `useCanvasStore` singleton remains exported for
146
+ * backward compatibility and is still used by tools, hooks, and the
147
+ * collaboration sync bridge that read state via `getState()`. Full
148
+ * multi-instance isolation across those subsystems is a follow-up phase.
149
+ */
150
+ export type CanvasStore = ReturnType<typeof createCanvasStore>;
151
+ export declare function createCanvasStore(): import('zustand').UseBoundStore<import('zustand').StoreApi<CanvasState>>;
152
+ /**
153
+ * Default singleton store. Used by tools, hooks, and the collaboration sync
154
+ * bridge that read state via `getState()`. Most apps render a single
155
+ * `<FlowCanvas>` per page, in which case this singleton is what the
156
+ * `CanvasStoreProvider` exposes.
157
+ */
126
158
  export declare const useCanvasStore: import('zustand').UseBoundStore<import('zustand').StoreApi<CanvasState>>;
127
159
  export {};
@@ -0,0 +1,254 @@
1
+ import * as A from "yjs";
2
+ import { useCanvasStore as w } from "./f1ow.js";
3
+ import { getYElements as _, getYDoc as W } from "./yjsProvider-CXmqUJXr.js";
4
+ const R = [
5
+ "id",
6
+ "type",
7
+ "x",
8
+ "y",
9
+ "width",
10
+ "height",
11
+ "rotation",
12
+ "isLocked",
13
+ "isVisible",
14
+ "sortOrder",
15
+ "version"
16
+ ], T = [
17
+ "strokeColor",
18
+ "fillColor",
19
+ "strokeWidth",
20
+ "opacity",
21
+ "strokeStyle",
22
+ "roughness",
23
+ "fontSize",
24
+ "fontFamily"
25
+ ];
26
+ function B(t, e) {
27
+ const s = t;
28
+ for (const i of R) {
29
+ const r = s[i];
30
+ r !== void 0 && e.set(i, r);
31
+ }
32
+ if (t.style)
33
+ for (const i of T)
34
+ e.set(`style.${i}`, t.style[i]);
35
+ switch (t.boundElements ? e.set("boundElements", JSON.stringify(t.boundElements)) : e.set("boundElements", null), t.ports && e.set("ports", JSON.stringify(t.ports)), "lineStyle" in t && t.lineStyle && e.set("lineStyle", JSON.stringify(t.lineStyle)), t.ports && e.set("ports", JSON.stringify(t.ports)), "lineStyle" in t && t.lineStyle && e.set("lineStyle", JSON.stringify(t.lineStyle)), t.groupIds && e.set("groupIds", JSON.stringify(t.groupIds)), t.type) {
36
+ case "rectangle":
37
+ e.set("cornerRadius", t.cornerRadius);
38
+ break;
39
+ case "line":
40
+ case "arrow":
41
+ e.set("points", JSON.stringify(t.points)), e.set("lineType", t.lineType), t.curvature !== void 0 && e.set("curvature", t.curvature), e.set("startBinding", t.startBinding ? JSON.stringify(t.startBinding) : null), e.set("endBinding", t.endBinding ? JSON.stringify(t.endBinding) : null), t.type === "arrow" && (e.set("startArrowhead", t.startArrowhead), e.set("endArrowhead", t.endArrowhead));
42
+ break;
43
+ case "freedraw":
44
+ e.set("points", JSON.stringify(t.points));
45
+ break;
46
+ case "text":
47
+ e.set("text", t.text), e.set("containerId", t.containerId), e.set("textAlign", t.textAlign), e.set("verticalAlign", t.verticalAlign);
48
+ break;
49
+ case "image":
50
+ e.set("src", t.src), e.set("naturalWidth", t.naturalWidth), e.set("naturalHeight", t.naturalHeight), e.set("scaleMode", t.scaleMode), e.set("crop", t.crop ? JSON.stringify(t.crop) : null), e.set("cornerRadius", t.cornerRadius), e.set("alt", t.alt);
51
+ break;
52
+ }
53
+ }
54
+ function E(t) {
55
+ const e = t.get("type"), s = t.get("id");
56
+ if (!e || !s) return null;
57
+ const i = {};
58
+ for (const n of T) {
59
+ const h = t.get(`style.${n}`);
60
+ h !== void 0 && (i[n] = h);
61
+ }
62
+ const r = {
63
+ id: s,
64
+ type: e,
65
+ x: t.get("x") ?? 0,
66
+ y: t.get("y") ?? 0,
67
+ width: t.get("width") ?? 100,
68
+ height: t.get("height") ?? 100,
69
+ rotation: t.get("rotation") ?? 0,
70
+ isLocked: t.get("isLocked") ?? !1,
71
+ isVisible: t.get("isVisible") ?? !0,
72
+ version: t.get("version") ?? 0,
73
+ style: i,
74
+ boundElements: u(t.get("boundElements")) ?? null,
75
+ groupIds: u(t.get("groupIds")) ?? void 0,
76
+ sortOrder: t.get("sortOrder") ?? void 0,
77
+ ports: u(t.get("ports")) ?? void 0
78
+ };
79
+ switch (e) {
80
+ case "rectangle":
81
+ r.cornerRadius = t.get("cornerRadius") ?? 0;
82
+ break;
83
+ case "line":
84
+ case "arrow":
85
+ r.points = u(t.get("points")) ?? [0, 0, 100, 0], r.lineType = t.get("lineType") ?? "sharp", r.curvature = t.get("curvature") ?? void 0, r.startBinding = u(t.get("startBinding")), r.endBinding = u(t.get("endBinding")), e === "arrow" && (r.startArrowhead = t.get("startArrowhead") ?? null, r.endArrowhead = t.get("endArrowhead") ?? "arrow");
86
+ {
87
+ const n = u(t.get("lineStyle"));
88
+ n && (r.lineStyle = n);
89
+ }
90
+ break;
91
+ case "freedraw":
92
+ r.points = u(t.get("points")) ?? [];
93
+ break;
94
+ case "text":
95
+ r.text = t.get("text") ?? "", r.containerId = t.get("containerId") ?? null, r.textAlign = t.get("textAlign") ?? "center", r.verticalAlign = t.get("verticalAlign") ?? "middle";
96
+ break;
97
+ case "image":
98
+ r.src = t.get("src") ?? "", r.naturalWidth = t.get("naturalWidth") ?? 0, r.naturalHeight = t.get("naturalHeight") ?? 0, r.scaleMode = t.get("scaleMode") ?? "fit", r.crop = u(t.get("crop")) ?? null, r.cornerRadius = t.get("cornerRadius") ?? 0, r.alt = t.get("alt") ?? "";
99
+ break;
100
+ }
101
+ return r;
102
+ }
103
+ function u(t) {
104
+ if (t == null) return null;
105
+ try {
106
+ return JSON.parse(t);
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+ let m = !1, S = !1, x = null, J = null, O = null, a = [];
112
+ function $(t = 50) {
113
+ const e = W(), s = _();
114
+ if (!e || !s)
115
+ return;
116
+ if (H(), s.size > 0) {
117
+ m = !0;
118
+ const o = C(s);
119
+ w.getState().setElements(o), a = o, m = !1;
120
+ } else {
121
+ const o = w.getState().elements;
122
+ o.length > 0 && (S = !0, e.transact(() => {
123
+ for (const l of o) {
124
+ const c = new A.Map();
125
+ B(l, c), s.set(l.id, c);
126
+ }
127
+ }, "local-init"), S = !1), a = o;
128
+ }
129
+ const i = (o, l) => {
130
+ if (l.origin === "local-sync" || l.origin === "local-init" || S) return;
131
+ m = !0;
132
+ const c = w.getState();
133
+ let d = [...a], v = !1;
134
+ for (const [f, g] of o.keys)
135
+ if (g.action === "add" || g.action === "update") {
136
+ const b = s.get(f);
137
+ if (b) {
138
+ const k = E(b);
139
+ if (k) {
140
+ const N = d.findIndex((I) => I.id === f);
141
+ N >= 0 ? d[N] = k : d.push(k), v = !0;
142
+ }
143
+ }
144
+ } else g.action === "delete" && (d = d.filter((b) => b.id !== f), v = !0);
145
+ v && (d.sort((f, g) => f.sortOrder && g.sortOrder ? f.sortOrder < g.sortOrder ? -1 : f.sortOrder > g.sortOrder ? 1 : 0 : 0), c.setElements(d), a = d), m = !1;
146
+ };
147
+ let r = null;
148
+ const n = /* @__PURE__ */ new Set(), h = (o) => {
149
+ if (!S) {
150
+ for (const l of o) {
151
+ let c = l.target;
152
+ for (; c && !(c instanceof A.Map && c.parent === s); )
153
+ c = c.parent;
154
+ if (c instanceof A.Map) {
155
+ const d = c.get("id");
156
+ d && n.add(d);
157
+ }
158
+ }
159
+ r && clearTimeout(r), r = setTimeout(() => {
160
+ if (n.size === 0 || S) return;
161
+ m = !0;
162
+ let l = [...a], c = !1;
163
+ for (const d of n) {
164
+ const v = s.get(d);
165
+ if (!v) continue;
166
+ const f = E(v);
167
+ if (!f) continue;
168
+ const g = l.findIndex((b) => b.id === d);
169
+ g >= 0 && (l[g] = f, c = !0);
170
+ }
171
+ n.clear(), c && (w.getState().setElements(l), a = l), m = !1;
172
+ }, 16);
173
+ }
174
+ };
175
+ s.observe(i), s.observeDeep(h), J = () => {
176
+ s.unobserve(i), s.unobserveDeep(h), r && clearTimeout(r), n.clear();
177
+ }, x = w.subscribe(
178
+ (o) => {
179
+ m || o.elements !== a && (O && clearTimeout(O), O = setTimeout(() => {
180
+ L(o.elements, s, e);
181
+ }, t));
182
+ }
183
+ );
184
+ }
185
+ function H() {
186
+ x && (x(), x = null), J && (J(), J = null), O && (clearTimeout(O), O = null), a = [];
187
+ }
188
+ function L(t, e, s) {
189
+ S = !0, a = t;
190
+ const i = /* @__PURE__ */ new Map();
191
+ for (const r of t)
192
+ i.set(r.id, r);
193
+ s.transact(() => {
194
+ for (const [r] of e.entries())
195
+ i.has(r) || e.delete(r);
196
+ for (const r of t) {
197
+ let n = e.get(r.id);
198
+ n ? Y(r, n) : (n = new A.Map(), B(r, n), e.set(r.id, n));
199
+ }
200
+ }, "local-sync"), S = !1;
201
+ }
202
+ function Y(t, e) {
203
+ const s = t;
204
+ for (const r of R) {
205
+ const n = s[r];
206
+ n !== e.get(r) && e.set(r, n);
207
+ }
208
+ if (t.style)
209
+ for (const r of T) {
210
+ const n = t.style[r];
211
+ n !== e.get(`style.${r}`) && e.set(`style.${r}`, n);
212
+ }
213
+ const i = t.boundElements ? JSON.stringify(t.boundElements) : null;
214
+ switch (i !== e.get("boundElements") && e.set("boundElements", i), t.type) {
215
+ case "rectangle":
216
+ t.cornerRadius !== e.get("cornerRadius") && e.set("cornerRadius", t.cornerRadius);
217
+ break;
218
+ case "line":
219
+ case "arrow": {
220
+ const n = JSON.stringify(t.points);
221
+ n !== e.get("points") && e.set("points", n), t.lineType !== e.get("lineType") && e.set("lineType", t.lineType), t.curvature !== e.get("curvature") && e.set("curvature", t.curvature);
222
+ const h = t.startBinding ? JSON.stringify(t.startBinding) : null;
223
+ h !== e.get("startBinding") && e.set("startBinding", h);
224
+ const o = t.endBinding ? JSON.stringify(t.endBinding) : null;
225
+ o !== e.get("endBinding") && e.set("endBinding", o), t.type === "arrow" && (t.startArrowhead !== e.get("startArrowhead") && e.set("startArrowhead", t.startArrowhead), t.endArrowhead !== e.get("endArrowhead") && e.set("endArrowhead", t.endArrowhead));
226
+ break;
227
+ }
228
+ case "freedraw": {
229
+ const n = JSON.stringify(t.points);
230
+ n !== e.get("points") && e.set("points", n);
231
+ break;
232
+ }
233
+ case "text":
234
+ t.text !== e.get("text") && e.set("text", t.text), t.containerId !== e.get("containerId") && e.set("containerId", t.containerId), t.textAlign !== e.get("textAlign") && e.set("textAlign", t.textAlign), t.verticalAlign !== e.get("verticalAlign") && e.set("verticalAlign", t.verticalAlign);
235
+ break;
236
+ case "image":
237
+ t.src !== e.get("src") && e.set("src", t.src), t.naturalWidth !== e.get("naturalWidth") && e.set("naturalWidth", t.naturalWidth), t.naturalHeight !== e.get("naturalHeight") && e.set("naturalHeight", t.naturalHeight), t.scaleMode !== e.get("scaleMode") && e.set("scaleMode", t.scaleMode);
238
+ const r = t.crop ? JSON.stringify(t.crop) : null;
239
+ r !== e.get("crop") && e.set("crop", r), t.cornerRadius !== e.get("cornerRadius") && e.set("cornerRadius", t.cornerRadius), t.alt !== e.get("alt") && e.set("alt", t.alt);
240
+ break;
241
+ }
242
+ }
243
+ function C(t) {
244
+ const e = [];
245
+ for (const [, s] of t.entries()) {
246
+ const i = E(s);
247
+ i && e.push(i);
248
+ }
249
+ return e.sort((s, i) => s.sortOrder && i.sortOrder ? s.sortOrder < i.sortOrder ? -1 : s.sortOrder > i.sortOrder ? 1 : 0 : 0), e;
250
+ }
251
+ export {
252
+ $ as startSync,
253
+ H as stopSync
254
+ };
@@ -2,6 +2,27 @@ export type ToolType = 'select' | 'hand' | 'rectangle' | 'ellipse' | 'diamond' |
2
2
  export type ElementType = 'rectangle' | 'ellipse' | 'diamond' | 'line' | 'arrow' | 'freedraw' | 'text' | 'image';
3
3
  export type Arrowhead = 'arrow' | 'triangle' | 'triangle_outline' | 'circle' | 'circle_outline' | 'diamond' | 'diamond_outline' | 'bar' | 'crowfoot_one' | 'crowfoot_many' | 'crowfoot_one_or_many';
4
4
  export type LineType = 'sharp' | 'curved' | 'elbow';
5
+ /** Gradient definition for connector strokes */
6
+ export interface LineGradient {
7
+ /** Color stops: [offset (0-1), cssColor] */
8
+ stops: [number, string][];
9
+ /** Gradient follows the path direction */
10
+ type: 'along-path';
11
+ }
12
+ /** Tapered stroke: width varies from start to end */
13
+ export interface LineTaper {
14
+ startWidth: number;
15
+ endWidth: number;
16
+ }
17
+ /** Extended line style options for connectors */
18
+ export interface LineStyleExtension {
19
+ /** Gradient stroke along the connector path */
20
+ gradient?: LineGradient;
21
+ /** Tapered stroke width */
22
+ taper?: LineTaper;
23
+ /** Animated flow direction indicator (dash animation) */
24
+ flowAnimation?: boolean;
25
+ }
5
26
  export type FreehandStyle = 'standard' | 'pen' | 'brush' | 'pencil';
6
27
  export interface ElementStyle {
7
28
  strokeColor: string;
@@ -27,6 +48,17 @@ export interface BaseElement {
27
48
  isVisible: boolean;
28
49
  /** Bidirectional refs: arrows/text bound to this element */
29
50
  boundElements: BoundElement[] | null;
51
+ /**
52
+ * Custom ports defined on this element instance.
53
+ * Used for architecture diagrams where shapes have named I/O points.
54
+ */
55
+ ports?: Port[];
56
+ /**
57
+ * Monotonic version counter — bumped on every geometry/port mutation.
58
+ * Used by binding system to detect stale worker results and cache invalidation.
59
+ * Auto-managed by the store; consumers should not set this directly.
60
+ */
61
+ version: number;
30
62
  /** Group hierarchy: element belongs to these groups (innermost first, outermost last) */
31
63
  groupIds?: string[];
32
64
  /**
@@ -123,6 +155,8 @@ export interface LineElement extends BaseElement {
123
155
  curvature?: number;
124
156
  startBinding: Binding | null;
125
157
  endBinding: Binding | null;
158
+ /** Extended line style (gradient, taper, animation) */
159
+ lineStyle?: LineStyleExtension;
126
160
  }
127
161
  export interface ArrowElement extends BaseElement {
128
162
  type: 'arrow';
@@ -141,6 +175,8 @@ export interface ArrowElement extends BaseElement {
141
175
  curvature?: number;
142
176
  startBinding: Binding | null;
143
177
  endBinding: Binding | null;
178
+ /** Extended line style (gradient, taper, animation) */
179
+ lineStyle?: LineStyleExtension;
144
180
  }
145
181
  export interface FreeDrawElement extends BaseElement {
146
182
  type: 'freedraw';
@@ -208,8 +244,23 @@ export interface SelectionBox {
208
244
  width: number;
209
245
  height: number;
210
246
  }
247
+ /** Named anchor positions on a shape's bounding box (compass notation) */
248
+ export type AnchorId = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw' | 'center' | 'auto';
249
+ /** Custom port on an element — user-defined attachment point */
250
+ export interface Port {
251
+ /** Unique port identifier within the element */
252
+ id: string;
253
+ /** Normalized [0-1, 0-1] position on the element's bounding box */
254
+ ratio: [number, number];
255
+ /** Optional display label for the port */
256
+ label?: string;
257
+ /** Optional preferred edge (used for elbow routing direction) */
258
+ edge?: AnchorId;
259
+ }
260
+ /** How the binding target was determined */
261
+ export type SnapMode = 'anchor' | 'port' | 'edge' | 'center';
211
262
  /**
212
- * @deprecated Use fixedPoint binding instead. Kept for backward-compat exports.
263
+ * @deprecated Use AnchorId instead. Kept for backward-compat exports.
213
264
  */
214
265
  export type ConnectionAnchor = 'top' | 'bottom' | 'left' | 'right' | 'center';
215
266
  /** Bidirectional reference stored on shapes to track bound connectors/text */
@@ -221,13 +272,30 @@ export type BoundElement = {
221
272
  export interface Binding {
222
273
  /** The element this end is connected to */
223
274
  elementId: string;
275
+ /**
276
+ * Named anchor position. When set, takes precedence over fixedPoint.
277
+ * Resolves to a fixedPoint at bind time, but re-resolves after resize
278
+ * to maintain semantic meaning (e.g. "always top-center").
279
+ * 'auto' = let the system choose the nearest anchor.
280
+ */
281
+ anchor?: AnchorId;
282
+ /** Custom port ID. When set, takes precedence over anchor and fixedPoint. */
283
+ portId?: string;
224
284
  /**
225
285
  * Continuous attachment ratio [0-1, 0-1] on target's bounding box.
226
286
  * e.g. [0.5, 0] = top center, [1, 0.5] = right center, [0.3, 0.7] = arbitrary.
287
+ * Always populated — serves as resolved cache when anchor/port is set.
227
288
  */
228
289
  fixedPoint: [number, number];
229
290
  /** Gap between the edge of the shape and the arrow tip (px) */
230
291
  gap: number;
292
+ /** How the binding target was determined */
293
+ snapMode: SnapMode;
294
+ /**
295
+ * Version of the target element at bind time.
296
+ * Used to detect stale bindings (e.g. worker results arriving after element changed).
297
+ */
298
+ elementVersion: number;
231
299
  /**
232
300
  * Whether to bind to the exact fixedPoint position, or to the shape center.
233
301
  * When false (default), the arrow connects from/to the shape's center,
@@ -250,6 +318,12 @@ export interface SnapTarget {
250
318
  * - `false`: cursor is in the center zone → attaches to center for auto-routing
251
319
  */
252
320
  isPrecise: boolean;
321
+ /** Named anchor if snapped to a cardinal position */
322
+ anchor?: AnchorId;
323
+ /** Custom port ID if snapped to a port */
324
+ portId?: string;
325
+ /** How the snap target was determined */
326
+ snapMode: SnapMode;
253
327
  }
254
328
  /** State for editing individual points of a line/arrow element */
255
329
  export interface LinearEditState {
@@ -266,3 +340,25 @@ export interface LinearEditState {
266
340
  /** Whether a point is currently being dragged */
267
341
  isDraggingPoint: boolean;
268
342
  }
343
+ /**
344
+ * Consumer-facing configuration for the connection/binding system.
345
+ * Pass via `FlowCanvasProps.connectionConfig`.
346
+ */
347
+ export interface ConnectionConfig {
348
+ /** Distance outside shape perimeter to activate edge snap (px). Default: 24 */
349
+ snapThreshold?: number;
350
+ /** Hysteresis margin to prevent edge/center mode flickering (px). Default: 6 */
351
+ hysteresisMargin?: number;
352
+ /** Clearance around shapes for elbow routing (px). Default: adaptive */
353
+ elbowMargin?: number;
354
+ /** Hit radius for port dots (px, before zoom compensation). Default: 8 */
355
+ portHitRadius?: number;
356
+ /** Minimum stub length for elbow route endpoints (px). Default: 36 */
357
+ stubLength?: number;
358
+ /** Whether to render custom ports on shapes. Default: true */
359
+ enablePorts?: boolean;
360
+ /** Default line style extension applied to new connectors */
361
+ defaultLineStyle?: LineStyleExtension;
362
+ /** Default line type for new arrows/lines. Default: 'sharp' */
363
+ defaultLineType?: LineType;
364
+ }
@@ -1,4 +1,4 @@
1
- import { CanvasElement, Point, ConnectionAnchor, Binding, SnapTarget, ArrowElement, LineElement, TextElement, BoundElement } from '../types';
1
+ import { CanvasElement, Point, ConnectionAnchor, Binding, SnapTarget, ArrowElement, LineElement, TextElement, BoundElement, AnchorId, SnapMode } from '../types';
2
2
  /** Whether an element can be a connection target */
3
3
  export declare function isConnectable(el: CanvasElement): boolean;
4
4
  /** Get the 5 named anchor positions for a bounding-box shape */
@@ -7,6 +7,29 @@ export declare function getConnectionPoints(el: CanvasElement): Record<Connectio
7
7
  * @deprecated Use fixedPoint-based getEdgePointFromFixedPoint() instead.
8
8
  */
9
9
  export declare function getAnchorPosition(el: CanvasElement, anchor: ConnectionAnchor): Point;
10
+ /** Map a named AnchorId to its fixedPoint ratio on the bounding box */
11
+ export declare function anchorToFixedPoint(anchor: AnchorId): [number, number];
12
+ /** Find the nearest cardinal AnchorId for a given fixedPoint */
13
+ export declare function fixedPointToAnchor(fp: [number, number]): AnchorId;
14
+ /** Resolve a port's ratio to a fixedPoint on an element */
15
+ export declare function resolvePort(el: CanvasElement, portId: string): [number, number] | null;
16
+ /**
17
+ * Resolve the effective fixedPoint for a binding, respecting priority:
18
+ * portId > anchor > fixedPoint.
19
+ *
20
+ * Returns the resolved fixedPoint and the effective snapMode.
21
+ */
22
+ export declare function resolveBindingPoint(binding: Binding, element: CanvasElement): {
23
+ fixedPoint: [number, number];
24
+ snapMode: SnapMode;
25
+ };
26
+ /**
27
+ * Create a Binding from a SnapTarget result.
28
+ * Convenience helper for LinearTool and LinearElementHandles.
29
+ */
30
+ export declare function createBindingFromSnap(snap: SnapTarget, gap: number, elementVersion: number): Binding;
31
+ /** Check whether a binding's elementVersion matches the current element */
32
+ export declare function isBindingStale(binding: Binding, element: CanvasElement): boolean;
10
33
  /**
11
34
  * Compute the appropriate gap between a connector and a shape edge,
12
35
  * based on both the connector's stroke width and a base offset.
@@ -21,8 +44,13 @@ export declare function computeFixedPoint(el: CanvasElement, worldPt: Point): [n
21
44
  /**
22
45
  * Convert a fixedPoint ratio back to a world-space target point,
23
46
  * then compute the edge intersection from center to that target.
47
+ *
48
+ * @param toward Optional direction hint for center fixedPoint [0.5, 0.5].
49
+ * When the fixedPoint is exactly center, there's no meaningful
50
+ * direction — `toward` provides the other endpoint so we can
51
+ * compute a stable edge exit. Without it, returns the element center.
24
52
  */
25
- export declare function getEdgePointFromFixedPoint(el: CanvasElement, fixedPoint: [number, number], gap?: number): Point;
53
+ export declare function getEdgePointFromFixedPoint(el: CanvasElement, fixedPoint: [number, number], gap?: number, toward?: Point): Point;
26
54
  /**
27
55
  * Given a shape and an external world-space point, compute the point on
28
56
  * the shape's perimeter closest to `toward`.