f1ow 1.0.0 → 1.1.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.
@@ -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`.
@@ -0,0 +1,2 @@
1
+ export declare function isTextEditingTarget(target: EventTarget | null): boolean;
2
+ export declare function blurTextEditingTarget(target: EventTarget | null): boolean;
@@ -7,6 +7,7 @@ interface BBox {
7
7
  width: number;
8
8
  height: number;
9
9
  }
10
+ type BoxLike = Pick<BBox, 'x' | 'y' | 'width' | 'height'>;
10
11
  /**
11
12
  * Determine the preferred exit direction for elbow routing from a
12
13
  * center (imprecise) binding.
@@ -21,8 +22,11 @@ interface BBox {
21
22
  * shapes at their vertical center — visually too close to objects and
22
23
  * often requiring more bends.
23
24
  *
24
- * Only uses horizontal exits when the target is strongly horizontally
25
- * aligned (>3× horizontal dominance after normalizing by shape size).
25
+ * When both endpoint shapes are known, prefer the axis with the smaller
26
+ * inter-shape gap (or the orthogonal axis with less overlap). This makes
27
+ * diagonal routes look more intentional than a pure center-to-center
28
+ * heuristic. If only a target point is known, fall back to the normalized
29
+ * center-delta heuristic with a slight vertical bias.
26
30
  *
27
31
  * This function should be used by BOTH:
28
32
  * - The binding system (connection.ts) to compute edge points for
@@ -31,12 +35,7 @@ interface BBox {
31
35
  *
32
36
  * For PRECISE bindings, always use directionFromFixedPoint instead.
33
37
  */
34
- export declare function getElbowPreferredDirection(shape: {
35
- x: number;
36
- y: number;
37
- width: number;
38
- height: number;
39
- }, targetPoint: Point): Direction;
38
+ export declare function getElbowPreferredDirection(shape: BoxLike, targetPoint: Point, targetShape?: BoxLike | null, targetBinding?: Binding | null): Direction;
40
39
  /**
41
40
  * Determine the exit/entry direction from a binding's fixedPoint.
42
41
  * fixedPoint is [fx, fy] in [0-1, 0-1] on the target shape's bbox.
@@ -67,6 +66,34 @@ export declare function directionFromShapeToPoint(shape: BBox, targetPoint: Poin
67
66
  * computed edge point from the binding system.
68
67
  */
69
68
  export declare function directionFromEdgePoint(shape: BBox, edgePoint: Point): Direction;
69
+ export declare function selectElbowDirectionPair(args: {
70
+ startWorld: Point;
71
+ endWorld: Point;
72
+ startBinding: Binding | null;
73
+ endBinding: Binding | null;
74
+ startShape?: {
75
+ x: number;
76
+ y: number;
77
+ width: number;
78
+ height: number;
79
+ } | null;
80
+ endShape?: {
81
+ x: number;
82
+ y: number;
83
+ width: number;
84
+ height: number;
85
+ } | null;
86
+ intermediateObstacles?: Array<{
87
+ x: number;
88
+ y: number;
89
+ width: number;
90
+ height: number;
91
+ }>;
92
+ minStubLength?: number;
93
+ }): {
94
+ startDir: Direction;
95
+ endDir: Direction;
96
+ };
70
97
  /**
71
98
  * Compute an orthogonal route between two points using a multi-candidate
72
99
  * grid-based shortest-path algorithm.
@@ -99,6 +126,12 @@ export declare function directionFromEdgePoint(shape: BBox, edgePoint: Point): D
99
126
  export declare function computeElbowRoute(start: Point, end: Point, startDir: Direction, endDir: Direction, startBBox?: BBox | null, endBBox?: BBox | null, minStubLength?: number,
100
127
  /** Additional obstacles (intermediate shapes) to avoid — already as BBox */
101
128
  intermediateObstacles?: BBox[]): Point[];
129
+ export declare function collectElbowIntermediateObstacles(startWorld: Point, endWorld: Point, allElements: CanvasElement[], excludeIds: Set<string>): Array<{
130
+ x: number;
131
+ y: number;
132
+ width: number;
133
+ height: number;
134
+ }>;
102
135
  /** Clear the route cache (call when elements change structurally) */
103
136
  export declare function clearElbowRouteCache(): void;
104
137
  export declare function computeElbowPoints(startWorld: Point, endWorld: Point, startBinding: Binding | null, endBinding: Binding | null, allElements: CanvasElement[], minStubLength?: number): number[];
@@ -36,6 +36,27 @@ export declare function measureLabelText(text: string, fontSize: number, fontFam
36
36
  width: number;
37
37
  height: number;
38
38
  };
39
+ /**
40
+ * Compute the CSS half-leading offset for a given font.
41
+ *
42
+ * Konva renders text with `textBaseline='top'` — the glyph top aligns
43
+ * with the node's y position (no leading above).
44
+ * CSS distributes leading equally above and below the content area
45
+ * within each line box. The half-leading is the space ABOVE the glyphs
46
+ * that CSS adds but Konva does not.
47
+ *
48
+ * To align a DOM textarea's visible text with Konva's rendered text,
49
+ * shift the textarea UP by this amount (in canvas-space pixels).
50
+ *
51
+ * Uses `fontBoundingBoxAscent/Descent` for accurate per-font measurement
52
+ * with a safe fallback for older browsers.
53
+ *
54
+ * @param fontSize - Font size in canvas-space pixels
55
+ * @param fontFamily - CSS font-family string
56
+ * @param lineHeight - Line-height multiplier (default: LABEL_LINE_HEIGHT)
57
+ * @returns Half-leading in canvas-space pixels (≥ 0)
58
+ */
59
+ export declare function computeHalfLeading(fontSize: number, fontFamily: string, lineHeight?: number): number;
39
60
  /**
40
61
  * Compute the full pill (background rect) dimensions for a connector label.
41
62
  *
@@ -0,0 +1,14 @@
1
+ export declare function htmlToPlainText(html: string): string;
2
+ /**
3
+ * Render markdown text to inline-styled HTML.
4
+ * Safe for use with `innerHTML` — raw HTML in the source is escaped.
5
+ */
6
+ export declare function renderMarkdown(text: string): string;
7
+ export declare function markdownToPlainText(text: string): string;
8
+ /** CSS class name for the markdown overlay container */
9
+ export declare const MD_CLASS = "fc-md";
10
+ /**
11
+ * CSS rules for markdown-rendered content.
12
+ * Injected once via a <style> tag by the overlay component.
13
+ */
14
+ export declare const MD_STYLES = "\n.fc-md { line-height: inherit; }\n.fc-md p { margin: 0; }\n.fc-md h1 { margin: 0; font-weight: bold; font-size: 1.5em; line-height: 1.3; }\n.fc-md h2 { margin: 0; font-weight: bold; font-size: 1.3em; line-height: 1.3; }\n.fc-md h3 { margin: 0; font-weight: bold; font-size: 1.15em; line-height: 1.3; }\n.fc-md h4, .fc-md h5, .fc-md h6 { margin: 0; font-weight: bold; font-size: 1em; }\n.fc-md strong { font-weight: bold; }\n.fc-md em { font-style: italic; }\n.fc-md del { text-decoration: line-through; }\n.fc-md code {\n background: rgba(0,0,0,0.06);\n padding: 0.1em 0.3em;\n border-radius: 3px;\n font-size: 0.9em;\n font-family: 'SF Mono', Monaco, Menlo, Consolas, monospace;\n}\n.fc-md pre {\n background: rgba(0,0,0,0.06);\n padding: 0.4em 0.6em;\n border-radius: 4px;\n font-size: 0.85em;\n overflow-x: auto;\n margin: 0.2em 0;\n}\n.fc-md pre code { background: none; padding: 0; font-size: inherit; }\n.fc-md ul, .fc-md ol { padding-left: 1.5em; margin: 0.15em 0; }\n.fc-md li { margin: 0; }\n.fc-md blockquote {\n border-left: 3px solid rgba(0,0,0,0.15);\n padding-left: 0.8em;\n margin: 0.2em 0;\n color: rgba(0,0,0,0.55);\n}\n.fc-md a { color: #4f8df7; text-decoration: underline; }\n.fc-md hr { border: none; border-top: 1px solid rgba(0,0,0,0.12); margin: 0.3em 0; }\n.fc-md img { max-width: 100%; }\n";
@@ -0,0 +1 @@
1
+ export declare function serializeEditableHtmlToMarkdown(html: string): string;
@@ -0,0 +1,18 @@
1
+ import { CanvasElement, ElementStyle, Point, TextElement } from '../types';
2
+ export type TextContainerElement = Extract<CanvasElement, {
3
+ type: 'rectangle' | 'ellipse' | 'diamond' | 'image';
4
+ }>;
5
+ export declare function isTextContainerElement(element: CanvasElement): element is TextContainerElement;
6
+ export declare function isPointInsideTextContainer(element: TextContainerElement, point: Point): boolean;
7
+ export declare function findTopmostTextContainerAtPoint(elements: CanvasElement[], point: Point): TextContainerElement | null;
8
+ /**
9
+ * Reorder elements so every bound text element is placed immediately
10
+ * after its container. This keeps a shape and its label at the same
11
+ * logical z-index — shapes stacked above the container occlude the
12
+ * label, and the label never leaks above unrelated foreground elements.
13
+ *
14
+ * Pure function — returns the input reference unchanged when no
15
+ * reordering is required so downstream memoization stays stable.
16
+ */
17
+ export declare function orderBoundTextWithContainers(elements: CanvasElement[]): CanvasElement[];
18
+ export declare function createBoundTextElement(id: string, container: TextContainerElement, style: ElementStyle): TextElement;
@@ -0,0 +1,6 @@
1
+ import { CanvasElement } from '../types';
2
+ /**
3
+ * Resolve text element ids that should receive font style updates.
4
+ * Includes directly selected text elements and bound text from selected containers.
5
+ */
6
+ export declare function collectTextStyleTargetIds(selectedIds: string[], elements: CanvasElement[]): string[];
@@ -0,0 +1,100 @@
1
+ import * as Y from "yjs";
2
+ import { WebsocketProvider } from "y-websocket";
3
+ let _doc = null;
4
+ let _provider = null;
5
+ let _config = null;
6
+ let _statusListeners = /* @__PURE__ */ new Set();
7
+ function createCollaborationProvider(config) {
8
+ if (_provider) {
9
+ destroyCollaborationProvider();
10
+ }
11
+ _config = config;
12
+ _doc = new Y.Doc();
13
+ _provider = new WebsocketProvider(
14
+ config.serverUrl,
15
+ config.roomName,
16
+ _doc,
17
+ {
18
+ connect: true,
19
+ params: config.authToken ? { token: config.authToken } : void 0
20
+ }
21
+ );
22
+ _provider.awareness.setLocalState({
23
+ user: config.user,
24
+ cursor: null,
25
+ selectedIds: []
26
+ });
27
+ _provider.on("status", (event) => {
28
+ const status = event.status;
29
+ for (const listener of _statusListeners) {
30
+ listener(status);
31
+ }
32
+ });
33
+ return { doc: _doc, provider: _provider };
34
+ }
35
+ function destroyCollaborationProvider() {
36
+ if (_provider) {
37
+ _provider.awareness.setLocalState(null);
38
+ _provider.disconnect();
39
+ _provider.destroy();
40
+ _provider = null;
41
+ }
42
+ if (_doc) {
43
+ _doc.destroy();
44
+ _doc = null;
45
+ }
46
+ _config = null;
47
+ }
48
+ function getYDoc() {
49
+ return _doc;
50
+ }
51
+ function getYProvider() {
52
+ return _provider;
53
+ }
54
+ function getYElements() {
55
+ return _doc == null ? void 0 : _doc.getMap("elements");
56
+ }
57
+ function getCollaborationConfig() {
58
+ return _config;
59
+ }
60
+ function isCollaborationActive() {
61
+ return _provider !== null && _provider.wsconnected;
62
+ }
63
+ function onStatusChange(listener) {
64
+ _statusListeners.add(listener);
65
+ return () => {
66
+ _statusListeners.delete(listener);
67
+ };
68
+ }
69
+ function updateAwareness(update) {
70
+ if (!_provider) return;
71
+ const current = _provider.awareness.getLocalState();
72
+ _provider.awareness.setLocalState({
73
+ ...current,
74
+ ...update
75
+ });
76
+ }
77
+ function getRemoteAwareness() {
78
+ if (!_provider) return /* @__PURE__ */ new Map();
79
+ const all = _provider.awareness.getStates();
80
+ const localId = _provider.awareness.clientID;
81
+ const remote = /* @__PURE__ */ new Map();
82
+ for (const [clientId, state] of all) {
83
+ if (clientId !== localId && state && state.user) {
84
+ remote.set(clientId, state);
85
+ }
86
+ }
87
+ return remote;
88
+ }
89
+ export {
90
+ createCollaborationProvider,
91
+ destroyCollaborationProvider,
92
+ getCollaborationConfig,
93
+ getRemoteAwareness,
94
+ getYDoc,
95
+ getYElements,
96
+ getYProvider,
97
+ isCollaborationActive,
98
+ onStatusChange,
99
+ updateAwareness
100
+ };