@kolosal-ai/rivet 0.4.0 → 0.6.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.
@@ -1,7 +1,7 @@
1
1
  import { CSSProperties, ReactNode } from 'react';
2
- import { A as AnchorRef } from '../registry-Z_oiPyjm.js';
3
- export { a as ANCHOR_SIDES, b as AnchorGeometry, c as AnchorOptions, d as AnchorPoint, e as AnchorRecord, f as AnchorRegistration, g as AnchorRegistrationOptions, h as AnchorStrayPolicy, D as DEFAULT_ANCHOR_OPTIONS, R as ResolvedAnchorOptions, i as anchorAutoHandleId, j as anchorDotHandleId, k as anchorHandleId, l as anchorRect, m as computeAnchorGeometry, n as findSelectableAnchor, o as normalizeAnchorConnection, p as parseAnchorAutoHandleId, q as parseAnchorDotHandleId, r as parseAnchorHandleId, s as strayPlacementPct } from '../registry-Z_oiPyjm.js';
4
- import '../types-D3aldmsO.js';
2
+ import { A as AnchorRef } from '../registry-DPfWNdkC.js';
3
+ export { a as ANCHOR_SIDES, b as AnchorGeometry, c as AnchorOptions, d as AnchorPoint, e as AnchorRecord, f as AnchorRegistration, g as AnchorRegistrationOptions, h as AnchorStrayPolicy, D as DEFAULT_ANCHOR_OPTIONS, R as ResolvedAnchorOptions, i as anchorAutoHandleId, j as anchorDotHandleId, k as anchorHandleId, l as anchorRect, m as computeAnchorGeometry, n as findSelectableAnchor, o as normalizeAnchorConnection, p as orderedSelectableAnchors, q as parseAnchorAutoHandleId, r as parseAnchorDotHandleId, s as parseAnchorHandleId, t as strayPlacementPct, u as strayPoint } from '../registry-DPfWNdkC.js';
4
+ import '../types-tkMbskgC.js';
5
5
 
6
6
  type AnchorProps = {
7
7
  /**
@@ -1,4 +1,4 @@
1
- export { ANCHOR_SIDES, DEFAULT_ANCHOR_OPTIONS, anchorAutoHandleId, anchorDotHandleId, anchorHandleId, anchorRect, computeAnchorGeometry, findSelectableAnchor, normalizeAnchorConnection, parseAnchorAutoHandleId, parseAnchorDotHandleId, parseAnchorHandleId, strayPlacementPct } from '../chunk-W5HBJ4VF.js';
1
+ export { ANCHOR_SIDES, DEFAULT_ANCHOR_OPTIONS, anchorAutoHandleId, anchorDotHandleId, anchorHandleId, anchorRect, computeAnchorGeometry, findSelectableAnchor, normalizeAnchorConnection, orderedSelectableAnchors, parseAnchorAutoHandleId, parseAnchorDotHandleId, parseAnchorHandleId, strayPlacementPct, strayPoint } from '../chunk-76GHEXDP.js';
2
2
  import { useRivetContext, useRivetNodeContext } from '../chunk-VQYN27OK.js';
3
3
  import { useRef, useLayoutEffect, useSyncExternalStore } from 'react';
4
4
  import { jsx } from 'react/jsx-runtime';
@@ -2,10 +2,14 @@
2
2
  var ANCHOR_SIDES = ["top", "right", "bottom", "left"];
3
3
  function findSelectableAnchor(records, target) {
4
4
  let found = null;
5
+ let foundElement = null;
5
6
  for (const record of records) {
6
- if (!record.selectable || !record.element?.contains(target)) continue;
7
- if (found?.element && record.element.contains(found.element)) continue;
7
+ if (!record.selectable) continue;
8
+ const hit = record.elements.find((element) => element.contains(target));
9
+ if (!hit) continue;
10
+ if (foundElement && hit.contains(foundElement)) continue;
8
11
  found = record;
12
+ foundElement = hit;
9
13
  }
10
14
  return found;
11
15
  }
@@ -83,6 +87,21 @@ function computeAnchorGeometry(root, first, last, box) {
83
87
  }
84
88
  };
85
89
  }
90
+ function orderedSelectableAnchors(records) {
91
+ return records.filter((record) => record.selectable).map((record, index) => ({ record, index })).sort((a, b) => {
92
+ const left = a.record.geometry;
93
+ const right = b.record.geometry;
94
+ if (!left || !right) {
95
+ if (left) return -1;
96
+ if (right) return 1;
97
+ return a.index - b.index;
98
+ }
99
+ const rowDelta = left.firstLineYPct - right.firstLineYPct;
100
+ if (Math.abs(rowDelta) > SAME_ROW_TOLERANCE_PCT) return rowDelta;
101
+ return left.centerXPct - right.centerXPct;
102
+ }).map(({ record }) => record);
103
+ }
104
+ var SAME_ROW_TOLERANCE_PCT = 2;
86
105
  function anchorRect(node, geometry) {
87
106
  const { dots } = geometry;
88
107
  const left = Math.min(dots.left.xPct, dots.right.xPct);
@@ -110,11 +129,24 @@ function placeAlongSide(rawPct, options) {
110
129
  if (Math.abs(clamped - 50) >= options.centerKeepoutPct) return clamped;
111
130
  return clamped < 50 ? 50 - options.centerKeepoutPct : 50 + options.centerKeepoutPct;
112
131
  }
132
+ function strayPoint(geometry, side, options) {
133
+ const along = strayPlacementPct(geometry, side, options);
134
+ switch (side) {
135
+ case "left":
136
+ return { xPct: 0, yPct: along };
137
+ case "right":
138
+ return { xPct: 100, yPct: along };
139
+ case "top":
140
+ return { xPct: along, yPct: 0 };
141
+ case "bottom":
142
+ return { xPct: along, yPct: 100 };
143
+ }
144
+ }
113
145
  function strayPlacementPct(geometry, side, options) {
114
146
  const raw = side === "left" ? geometry.firstLineYPct : side === "right" ? geometry.lastLineYPct : geometry.centerXPct;
115
147
  return placeAlongSide(raw, options);
116
148
  }
117
149
 
118
- export { ANCHOR_SIDES, DEFAULT_ANCHOR_OPTIONS, anchorAutoHandleId, anchorDotHandleId, anchorGeometryEqual, anchorHandleId, anchorRect, computeAnchorGeometry, findSelectableAnchor, normalizeAnchorConnection, parseAnchorAutoHandleId, parseAnchorDotHandleId, parseAnchorHandleId, strayPlacementPct };
119
- //# sourceMappingURL=chunk-W5HBJ4VF.js.map
120
- //# sourceMappingURL=chunk-W5HBJ4VF.js.map
150
+ export { ANCHOR_SIDES, DEFAULT_ANCHOR_OPTIONS, anchorAutoHandleId, anchorDotHandleId, anchorGeometryEqual, anchorHandleId, anchorRect, computeAnchorGeometry, findSelectableAnchor, normalizeAnchorConnection, orderedSelectableAnchors, parseAnchorAutoHandleId, parseAnchorDotHandleId, parseAnchorHandleId, strayPlacementPct, strayPoint };
151
+ //# sourceMappingURL=chunk-76GHEXDP.js.map
152
+ //# sourceMappingURL=chunk-76GHEXDP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/anchor/registry.ts"],"names":[],"mappings":"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","file":"chunk-76GHEXDP.js","sourcesContent":["/**\n * Anchor — a connectable point bound to a DOM element rendered inside a node.\n *\n * Rivet knows nothing about what the element means (an inline text span, an\n * icon, a table row — that's the consumer's business); it owns the geometry:\n * measuring the element's line boxes within the node, rendering grab dots and\n * stray perimeter handles from those measurements, and rewriting dot handle\n * ids to the persisted stray ids when a connection commits.\n *\n * Placement derives from the element's *first* and *last* client line rects —\n * never from CSS positioning inside the inline element, which Chrome anchors\n * to the first line box only — so a line-wrapped span places each dot where it\n * visually belongs. All values are percentages of the node box: client rects\n * scale together under the viewport transform, so the ratio cancels zoom.\n */\n\nimport type { Connection, HandlePosition, NodeId, Rect } from \"../types\"\n\n/** The four sides an anchor can expose handles on. */\nexport const ANCHOR_SIDES = [\"top\", \"right\", \"bottom\", \"left\"] as const\n\n/** A measured point within a node's box, as percentages (zoom-independent). */\nexport type AnchorPoint = { xPct: number; yPct: number }\n\n/** Where an anchor's element sits within its node box, from measured line rects. */\nexport type AnchorGeometry = {\n /** Vertical center of the element's first line box, as % of the node height. */\n firstLineYPct: number\n /** Vertical center of the element's last line box, as % of the node height. */\n lastLineYPct: number\n /** Horizontal center of the element's bounding box, as % of the node width. */\n centerXPct: number\n /**\n * Grab dot positions: left/top on the first line box, right/bottom on the\n * last, so a wrapped element gets each dot on the line it belongs to.\n */\n dots: Record<HandlePosition, AnchorPoint>\n}\n\n/**\n * Stray-handle chrome policy, chosen at registration:\n *\n * - `\"dynamic\"` — one perimeter handle per connected side, a single\n * default-side handle while unconnected. The inline-text default.\n * - `\"none\"` — no stray chrome at all; the anchor connects through its grab\n * dots only. For anchors whose element boundary *is* the visual connection\n * point (an SVG shape), where a floating perimeter handle reads as clutter.\n * Purely visual: edges bound to stray ids still resolve at the measured\n * placement — the renderer derives endpoints from geometry, not chrome.\n */\nexport type AnchorStrayPolicy = \"dynamic\" | \"none\"\n\n/**\n * A registered anchor. Empty `elements` means detached — the consumer unmounted\n * the display (or an editor replaced the DOM node) without unregistering, so\n * the last-known `geometry` is retained and edges stay put (stale retention).\n * Unregistering is the explicit removal signal.\n *\n * An anchor can be bound to **several** elements, because one thing on screen\n * isn't always one element: a rich-text mark interrupted by another mark\n * renders as two spans, and an imported graphic can be a group of paths. They\n * are treated as fragments of a single run — first line of the first, last line\n * of the last — which is what {@link AnchorGeometry} already describes.\n */\nexport type AnchorRecord = {\n nodeId: NodeId\n anchorId: string\n /** Accent color for the anchor's dots and stray handles. */\n color?: string\n /** Stray chrome policy. Absent = `\"dynamic\"`. */\n strays?: AnchorStrayPolicy\n /** Whether the anchor is a selection target at all — by press or key. Absent = `false`. */\n selectable?: boolean\n /**\n * Whatever the consumer attached at registration — a shape's label, the\n * bounds it was imported at, the layer it came from. Rivet stores and returns\n * it, and never reads it.\n */\n data?: unknown\n /**\n * Every element this anchor is bound to, in document order. Empty while\n * detached.\n */\n elements: readonly Element[]\n /**\n * The first of {@link elements}, or `null` when detached — the answer for the\n * overwhelmingly common one-element anchor, kept so call sites that only need\n * \"is this anchor live, and where\" don't have to index.\n */\n element: Element | null\n /** Null until the first successful measurement. */\n geometry: AnchorGeometry | null\n}\n\nexport type AnchorRegistrationOptions = {\n color?: string\n /** Stray-handle chrome policy for this anchor. Default `\"dynamic\"`. */\n strays?: AnchorStrayPolicy\n /**\n * Make the anchor a selection target: the element becomes pointer-interactive\n * so a click selects it, and it joins the node's keyboard stepping order\n * (Alt+Left/Right on the focused node).\n *\n * Default `false`: an anchor around inline text usually sits in editable\n * content, where swallowing a press would stop the caret from landing. Shapes\n * are the opposite case — the element *is* the thing you mean to click. One\n * flag for both routes, so an anchor is never selectable one way and not the\n * other.\n */\n selectable?: boolean\n /** Opaque consumer payload, returned on the record untouched. */\n data?: unknown\n}\n\n/** Identifies one anchor: which node holds it, and which anchor on that node. */\nexport type AnchorRef = {\n nodeId: NodeId\n anchorId: string\n}\n\n/**\n * The selectable anchor a press landed in, or `null` for a press anywhere else\n * on the node. Innermost wins: anchors can nest (a shape inside a group that is\n * itself an anchor), and the deepest one is the one under the pointer.\n */\nexport function findSelectableAnchor(\n records: readonly AnchorRecord[],\n target: Element,\n): AnchorRecord | null {\n let found: AnchorRecord | null = null\n let foundElement: Element | null = null\n for (const record of records) {\n if (!record.selectable) continue\n const hit = record.elements.find((element) => element.contains(target))\n if (!hit) continue\n // Innermost wins across fragments too: the containing element of the\n // candidate is compared, not the record's first.\n if (foundElement && hit.contains(foundElement)) continue\n found = record\n foundElement = hit\n }\n return found\n}\n\n/**\n * Imperative registration handle for headless consumers (rich-text editors and\n * other renderers that own their DOM). Returned by `RivetInstance.registerAnchor`.\n */\nexport type AnchorRegistration = {\n /**\n * Re-attach the anchor after the renderer replaced its DOM node — one element\n * or, for content that renders as several, all of its fragments. Passing\n * `null` detaches instead: geometry is retained and edges stay put until the\n * element (or a replacement) is rebound or the anchor is unregistered.\n */\n rebind: (element: Element | readonly Element[] | null) => void\n /** Explicit removal — drops the anchor and the chrome rendered for it. */\n unregister: () => void\n}\n\n/** Placement policy for anchor chrome, set via the `anchorOptions` prop. */\nexport type AnchorOptions = {\n /** Side an unconnected anchor shows its single stray handle on. Default `\"top\"`. */\n defaultSide?: HandlePosition\n /**\n * Minimum distance (in % of the side) a stray handle keeps from the side's\n * center, where a node's own side-center handles usually sit. Default `14`.\n */\n centerKeepoutPct?: number\n /** Minimum distance (in % of the side) a stray handle keeps from the corners. Default `8`. */\n cornerMarginPct?: number\n /** Grab dot diameter in px. Default `7`. */\n dotSize?: number\n /** Stray handle diameter in px. Default `8`. */\n strayHandleSize?: number\n}\n\nexport type ResolvedAnchorOptions = Required<AnchorOptions>\n\nexport const DEFAULT_ANCHOR_OPTIONS: ResolvedAnchorOptions = {\n defaultSide: \"top\",\n centerKeepoutPct: 14,\n cornerMarginPct: 8,\n dotSize: 7,\n strayHandleSize: 8,\n}\n\n// ============================================================================\n// Handle-id grammar\n// ============================================================================\n//\n// The string forms are the persisted wire format for anchor connectivity:\n// `inode-<anchorId>-<side>` for the stray (perimeter) handles that edges\n// reference, `inode-text-<anchorId>-<side>` for the in-content grab dots that\n// only ever *start* a drag. The `inode-` prefix is the historical grammar from\n// the pre-primitive consumer implementation, kept verbatim so existing graphs\n// load without migration; the structured edge-end model replaces these strings\n// later without changing what's stored.\n\nconst STRAY_PREFIX = \"inode-\"\nconst DOT_PREFIX = \"inode-text-\"\n\nfunction isSide(value: string): value is HandlePosition {\n return (ANCHOR_SIDES as readonly string[]).includes(value)\n}\n\n/** Build the persisted handle id for an anchor's stray handle on `side`. */\nexport function anchorHandleId(anchorId: string, side: HandlePosition): string {\n return `${STRAY_PREFIX}${anchorId}-${side}`\n}\n\n/** Parse a stray anchor handle id back into `{ anchorId, side }`, or null. */\nexport function parseAnchorHandleId(\n handleId: string | null | undefined,\n): { anchorId: string; side: HandlePosition } | null {\n // A dot handle id shares the stray prefix; it is a local-only affordance,\n // never a persisted edge handle, so it must not be read as a stray here.\n if (!handleId?.startsWith(STRAY_PREFIX) || handleId.startsWith(DOT_PREFIX)) return null\n const lastDash = handleId.lastIndexOf(\"-\")\n const side = handleId.slice(lastDash + 1)\n if (!isSide(side)) return null\n const anchorId = handleId.slice(STRAY_PREFIX.length, lastDash)\n if (!anchorId) return null\n return { anchorId, side }\n}\n\n/**\n * The auto-side marker in the anchor id grammar. `inode-<anchorId>-auto` is an\n * anchor end with *no pinned side*: the renderer resolves the side per frame to\n * face the peer node (a floating end). Not a side, so it never collides with\n * the stray forms — but an `anchorId` should not itself end in `-auto` or a\n * `-<side>` suffix, which the grammar cannot distinguish.\n */\nconst AUTO_SIDE = \"auto\"\n\n/** Build the handle id for an anchor end with an auto (render-resolved) side. */\nexport function anchorAutoHandleId(anchorId: string): string {\n return `${STRAY_PREFIX}${anchorId}-${AUTO_SIDE}`\n}\n\n/** Parse an auto anchor handle id back into its `anchorId`, or null. */\nexport function parseAnchorAutoHandleId(handleId: string | null | undefined): string | null {\n if (!handleId?.startsWith(STRAY_PREFIX) || handleId.startsWith(DOT_PREFIX)) return null\n const lastDash = handleId.lastIndexOf(\"-\")\n if (handleId.slice(lastDash + 1) !== AUTO_SIDE) return null\n const anchorId = handleId.slice(STRAY_PREFIX.length, lastDash)\n return anchorId || null\n}\n\n/** Build the handle id for an anchor's in-content grab dot on `side`. */\nexport function anchorDotHandleId(anchorId: string, side: HandlePosition): string {\n return `${DOT_PREFIX}${anchorId}-${side}`\n}\n\n/** Parse a grab dot handle id back into `{ anchorId, side }`, or null. */\nexport function parseAnchorDotHandleId(\n handleId: string | null | undefined,\n): { anchorId: string; side: HandlePosition } | null {\n if (!handleId?.startsWith(DOT_PREFIX)) return null\n const lastDash = handleId.lastIndexOf(\"-\")\n const side = handleId.slice(lastDash + 1)\n if (!isSide(side)) return null\n const anchorId = handleId.slice(DOT_PREFIX.length, lastDash)\n if (!anchorId) return null\n return { anchorId, side }\n}\n\n/**\n * Rewrite grab dot ends to the persisted stray ids — same anchor, same side —\n * before a connection commits. Intent-preserving: the side comes from the dot\n * the user actually grabbed or dropped on. Runs before any consumer\n * `mapConnection`; connections not touching a dot pass through unchanged.\n */\nexport function normalizeAnchorConnection(connection: Connection): Connection {\n const source = parseAnchorDotHandleId(connection.sourceHandle)\n const target = parseAnchorDotHandleId(connection.targetHandle)\n if (!source && !target) return connection\n return {\n ...connection,\n sourceHandle: source ? anchorHandleId(source.anchorId, source.side) : connection.sourceHandle,\n targetHandle: target ? anchorHandleId(target.anchorId, target.side) : connection.targetHandle,\n }\n}\n\n// ============================================================================\n// Measurement\n// ============================================================================\n\n/** The rect fields measurement reads — satisfied by DOMRect. */\nexport type AnchorRectLike = {\n left: number\n top: number\n width: number\n height: number\n}\n\n/**\n * Derive an anchor's geometry from its node box (`root`), its first and last\n * client line rects, and its bounding box. Pure — callers supply the rects.\n */\nexport function computeAnchorGeometry(\n root: AnchorRectLike,\n first: AnchorRectLike,\n last: AnchorRectLike,\n box: AnchorRectLike,\n): AnchorGeometry {\n const point = (x: number, y: number): AnchorPoint => ({\n xPct: ((x - root.left) / root.width) * 100,\n yPct: ((y - root.top) / root.height) * 100,\n })\n return {\n firstLineYPct: ((first.top + first.height / 2 - root.top) / root.height) * 100,\n lastLineYPct: ((last.top + last.height / 2 - root.top) / root.height) * 100,\n centerXPct: ((box.left + box.width / 2 - root.left) / root.width) * 100,\n dots: {\n top: point(first.left + first.width / 2, first.top),\n bottom: point(last.left + last.width / 2, last.top + last.height),\n left: point(first.left, first.top + first.height / 2),\n right: point(last.left + last.width, last.top + last.height / 2),\n },\n }\n}\n\n/**\n * The box an anchor's element occupies inside `node`, in the same space that\n * rect is given in — how chrome that annotates a whole anchor (a peer's claim\n * on it, say) gets a rect out of measurements taken as percentages.\n *\n * Derived from the grab dots, which sit on the element's edges: left and top on\n * its first line box, right and bottom on its last. For anything that fits on\n * one line that *is* the element's box. A line-wrapped element instead reports\n * the span from where it starts to where it ends — the union of its line boxes\n * would be the node's whole text column, which describes the paragraph rather\n * than the anchor.\n */\n/**\n * A node's selectable anchors in **reading order** — first line box down the\n * node, then left to right within a line.\n *\n * The order keyboard stepping moves in, and it has to be reading order rather\n * than registration order: a consumer's JSX order is usually the same thing,\n * but an editor that re-registers anchors as its document changes has no reason\n * to keep them in sync, and stepping through a paragraph in the order its spans\n * happened to mount would be unusable.\n *\n * Unmeasured anchors sort last, keeping their relative order: they have no\n * position to compare, and putting them first would make the first press of a\n * key land somewhere arbitrary.\n */\nexport function orderedSelectableAnchors(\n records: readonly AnchorRecord[],\n): readonly AnchorRecord[] {\n return records\n .filter((record) => record.selectable)\n .map((record, index) => ({ record, index }))\n .sort((a, b) => {\n const left = a.record.geometry\n const right = b.record.geometry\n if (!left || !right) {\n if (left) return -1\n if (right) return 1\n return a.index - b.index\n }\n // A line's own height is the tolerance: two anchors on the same line\n // differ in `firstLineYPct` by measurement noise, not by a line.\n const rowDelta = left.firstLineYPct - right.firstLineYPct\n if (Math.abs(rowDelta) > SAME_ROW_TOLERANCE_PCT) return rowDelta\n return left.centerXPct - right.centerXPct\n })\n .map(({ record }) => record)\n}\n\n/** Vertical distance (% of node height) within which two anchors count as one row. */\nconst SAME_ROW_TOLERANCE_PCT = 2\n\nexport function anchorRect(node: Rect, geometry: AnchorGeometry): Rect {\n const { dots } = geometry\n // Sorted rather than assumed: a wrapped element can start further right than\n // it ends, which would otherwise come out as a negative width.\n const left = Math.min(dots.left.xPct, dots.right.xPct)\n const right = Math.max(dots.left.xPct, dots.right.xPct)\n const top = Math.min(dots.top.yPct, dots.bottom.yPct)\n const bottom = Math.max(dots.top.yPct, dots.bottom.yPct)\n return {\n x: node.x + (left / 100) * node.width,\n y: node.y + (top / 100) * node.height,\n width: ((right - left) / 100) * node.width,\n height: ((bottom - top) / 100) * node.height,\n }\n}\n\nconst GEOMETRY_TOLERANCE_PCT = 0.1\n\nfunction pointsEqual(a: AnchorPoint, b: AnchorPoint): boolean {\n return (\n Math.abs(a.xPct - b.xPct) < GEOMETRY_TOLERANCE_PCT &&\n Math.abs(a.yPct - b.yPct) < GEOMETRY_TOLERANCE_PCT\n )\n}\n\n/** Sub-tolerance equality, so measurement echoes don't churn subscribers. */\nexport function anchorGeometryEqual(a: AnchorGeometry | null, b: AnchorGeometry | null): boolean {\n if (a === b) return true\n if (!a || !b) return false\n return (\n Math.abs(a.firstLineYPct - b.firstLineYPct) < GEOMETRY_TOLERANCE_PCT &&\n Math.abs(a.lastLineYPct - b.lastLineYPct) < GEOMETRY_TOLERANCE_PCT &&\n Math.abs(a.centerXPct - b.centerXPct) < GEOMETRY_TOLERANCE_PCT &&\n ANCHOR_SIDES.every((side) => pointsEqual(a.dots[side], b.dots[side]))\n )\n}\n\n// ============================================================================\n// Stray-handle placement along a node side\n// ============================================================================\n\n/**\n * Nudge a raw position (in % of the side) out of the side-center keep-out band\n * — where the node's own handles usually sit — and off the corners.\n */\nexport function placeAlongSide(rawPct: number, options: ResolvedAnchorOptions): number {\n const clamped = Math.min(Math.max(rawPct, options.cornerMarginPct), 100 - options.cornerMarginPct)\n if (Math.abs(clamped - 50) >= options.centerKeepoutPct) return clamped\n return clamped < 50 ? 50 - options.centerKeepoutPct : 50 + options.centerKeepoutPct\n}\n\n/**\n * Where an anchor's stray handle sits along `side`, as % of that side — the\n * measured row (left/right) or column (top/bottom) nearest the anchor, nudged\n * by {@link placeAlongSide}. Shared by the stray-handle chrome and by the edge\n * renderer when it places an auto anchor end on a render-resolved side.\n */\n/**\n * A stray handle's position on the node box, as percentages of it — the same\n * placement {@link strayPlacementPct} gives, resolved into both axes.\n *\n * Both axes, for every side, so one pair of numbers can put a stray anywhere on\n * the perimeter. That's what lets the chrome follow an end floating from one\n * side to another without re-rendering: the element's position is two style\n * writes rather than a different set of anchoring properties.\n */\nexport function strayPoint(\n geometry: AnchorGeometry,\n side: HandlePosition,\n options: ResolvedAnchorOptions,\n): AnchorPoint {\n const along = strayPlacementPct(geometry, side, options)\n switch (side) {\n case \"left\":\n return { xPct: 0, yPct: along }\n case \"right\":\n return { xPct: 100, yPct: along }\n case \"top\":\n return { xPct: along, yPct: 0 }\n case \"bottom\":\n return { xPct: along, yPct: 100 }\n }\n}\n\nexport function strayPlacementPct(\n geometry: AnchorGeometry,\n side: HandlePosition,\n options: ResolvedAnchorOptions,\n): number {\n const raw =\n side === \"left\"\n ? geometry.firstLineYPct\n : side === \"right\"\n ? geometry.lastLineYPct\n : geometry.centerXPct\n return placeAlongSide(raw, options)\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { R as Rect, V as Vec2, A as AlignmentGuide, c as EdgeChange, d as RivetEdge, a as NodeChange, e as RivetNode, N as NodeId, f as NodeProps, g as NodeTypes, b as HandleType, H as HandlePosition, S as Size, h as ChangeOrigin, E as EdgeId, i as HandleRecord, j as ChangeMeta, k as Viewport, l as NodeGestureEvent, m as NodeLockConflictEvent, n as RivetSelection, o as ReconnectDelegate, C as Connection, P as PendingConnection, p as EdgeTypes, D as DefaultEdgeOptions, q as EdgeRendererFactory, r as SwimlaneGroup, s as SwimlaneMargin, t as SwimlaneHeaderProps, u as SwimlaneLabelProps, v as SwimlaneSizeChange, L as LaneChange, w as RivetControls, x as RivetSnapshot, y as EdgeRenderer, z as EdgeRendererOptions, B as EdgeDrawExtras, F as EdgeEnd, G as EdgePathFn } from './types-D3aldmsO.js';
2
- export { I as CenterNodeOptions, J as EdgeMarker, K as EdgePath, M as EdgePathParams, O as EdgeStyle, Q as FitViewOptions, T as NodeComponent } from './types-D3aldmsO.js';
1
+ import { R as Rect, V as Vec2, A as AlignmentGuide, c as EdgeChange, d as RivetEdge, a as NodeChange, e as RivetNode, N as NodeId, f as NodeProps, g as NodeTypes, b as HandleType, H as HandlePosition, S as Size, h as ChangeOrigin, E as EdgeId, i as HandleRecord, j as ChangeMeta, k as Viewport, l as NodeGestureEvent, m as NodeLockConflictEvent, n as RivetSelection, o as ReconnectDelegate, C as Connection, P as PendingConnection, p as EdgeTypes, D as DefaultEdgeOptions, q as EdgeRendererFactory, r as SwimlaneGroup, s as SwimlaneMargin, t as SwimlaneHeaderProps, u as SwimlaneLabelProps, v as SwimlaneSizeChange, L as LaneChange, w as RivetControls, x as RivetSnapshot, y as EdgeRenderer, z as EdgeRendererOptions, B as EdgeDrawExtras, F as EdgeEnd, G as EdgePathFn } from './types-tkMbskgC.js';
2
+ export { I as CenterNodeOptions, J as EdgeMarker, K as EdgePath, M as EdgePathParams, O as EdgeStyle, Q as FitViewOptions, T as NodeComponent } from './types-tkMbskgC.js';
3
3
  import { ReactNode, CSSProperties, MouseEvent, RefObject } from 'react';
4
- import { e as AnchorRecord, g as AnchorRegistrationOptions, A as AnchorRef, c as AnchorOptions, R as ResolvedAnchorOptions, f as AnchorRegistration } from './registry-Z_oiPyjm.js';
5
- import { b as PresenceTargetKey, p as PresenceRegistry, h as LockRegistry, k as Peer, e as LocalPresence, o as PresenceOptions, C as CanInteractWithLocked, l as PeerConnection } from './use-node-lock-CbS59CRE.js';
6
- export { L as LOCK_DEFAULT_REFUSED, f as LockIntent, g as LockInteraction, i as LockTarget, N as NodeLock, y as useNodeLock, z as useNodeLockAllows } from './use-node-lock-CbS59CRE.js';
4
+ import { e as AnchorRecord, g as AnchorRegistrationOptions, A as AnchorRef, c as AnchorOptions, R as ResolvedAnchorOptions, f as AnchorRegistration } from './registry-DPfWNdkC.js';
5
+ import { b as PresenceTargetKey, p as PresenceRegistry, h as LockRegistry, k as Peer, e as LocalPresence, o as PresenceOptions, C as CanInteractWithLocked, l as PeerConnection } from './use-node-lock-CKtOwBrI.js';
6
+ export { L as LOCK_DEFAULT_REFUSED, f as LockIntent, g as LockInteraction, i as LockTarget, N as NodeLock, y as useNodeLock, z as useNodeLockAllows } from './use-node-lock-CKtOwBrI.js';
7
7
  import { ResolvedLane, ViewportElements, ResolvedSwimlaneGroup, ResolvedMargin } from './swimlane/index.js';
8
8
 
9
9
  /**
@@ -374,6 +374,15 @@ type RivetStore = {
374
374
  setEdgeAlignment: (mode: EdgeAlignment) => void;
375
375
  /** The current edge-alignment policy (the runtime reads it per frame). */
376
376
  getEdgeAlignment: () => EdgeAlignment;
377
+ /**
378
+ * Set how far the losing axis must dominate before a resolved side switches
379
+ * axes (the `edgeAlignmentHysteresis` prop). Clamped to `>= 1`: below that the
380
+ * band would make an axis change *easier* than plain dominance, which flickers
381
+ * rather than resists, and 1 is already "no band at all".
382
+ */
383
+ setAlignmentHysteresis: (ratio: number) => void;
384
+ /** The current hysteresis ratio — read per frame, and at each commit. */
385
+ getAlignmentHysteresis: () => number;
377
386
  /**
378
387
  * Hysteresis memory for per-frame side resolution, keyed by
379
388
  * `displayedSideKey(edgeId, end)`. The render loop hands this map to the
@@ -561,8 +570,12 @@ type RivetStore = {
561
570
  * Register (or re-bind) an anchor. Idempotent on the `(nodeId, anchorId)`
562
571
  * key: calling again with a new element re-attaches it, keeping any
563
572
  * last-known geometry until the next successful measurement.
573
+ *
574
+ * Several elements bind one anchor to content that renders as fragments — a
575
+ * rich-text mark split by another mark, a graphic that is a group of paths.
576
+ * They're measured as one run: first line of the first, last line of the last.
564
577
  */
565
- registerAnchor: (nodeId: NodeId, anchorId: string, element: Element, options?: AnchorRegistrationOptions) => void;
578
+ registerAnchor: (nodeId: NodeId, anchorId: string, element: Element | readonly Element[], options?: AnchorRegistrationOptions) => void;
566
579
  /**
567
580
  * Mark an anchor's element gone without removing the anchor — the stale
568
581
  * retention signal. Geometry (and the stray handles rendered from it) is
@@ -952,6 +965,25 @@ type RivetProps = {
952
965
  * renderer resolves its facing side per frame and never writes it back.
953
966
  */
954
967
  edgeAlignment?: EdgeAlignment;
968
+ /**
969
+ * How far the losing axis must dominate before a resolved side switches axes.
970
+ * Defaults to `DEFAULT_ALIGNMENT_HYSTERESIS` (`1.2`), i.e. the other axis has
971
+ * to win by 20% before an end changes from a horizontal side to a vertical
972
+ * one or back.
973
+ *
974
+ * The band is what stops a near-diagonal layout flickering between sides
975
+ * frame to frame; it's a ratio rather than a distance so it reads the same at
976
+ * every zoom and node size. Raise it for dense graphs of small nodes, where
977
+ * 20% of a short center-to-center distance is a few pixels of pointer travel;
978
+ * lower it toward `1` (no band) when ends feel reluctant to follow a node
979
+ * that has clearly moved. Values below `1` are clamped: they'd make an axis
980
+ * change *easier* than plain dominance, which flickers rather than resists.
981
+ *
982
+ * Applies to both halves of the same resolution — the per-frame `"live"`
983
+ * render and the side committed on drop — so what you see mid-drag stays what
984
+ * you get.
985
+ */
986
+ edgeAlignmentHysteresis?: number;
955
987
  /** Called when an endpoint drag re-points an edge (the edge is already updated). */
956
988
  onReconnect?: (edge: RivetEdge, connection: Connection) => void;
957
989
  /** Called when an endpoint reconnection drag starts. */
@@ -988,7 +1020,7 @@ type RivetProps = {
988
1020
  * The graph surface. Stacks a background grid canvas, an edge canvas, and a DOM
989
1021
  * node layer — all driven by one shared viewport (see {@link useRivetRuntime}).
990
1022
  */
991
- declare function Rivet({ defaultNodes, defaultEdges, nodes: controlledNodes, edges: controlledEdges, onNodesChange, onEdgesChange, defaultViewport, nodeTypes, edgeTypes, defaultEdgeOptions, ariaLabel, renderer, minZoom, maxZoom, gridGap, snapGrid, alignmentGuides, anchorOptions, peers, onLocalPresence, presenceOptions, lockedNodes, canInteractWithLocked, onNodeLockConflict, panOnDrag, selectionOnDrag, selectionKeyCode, multiSelectionKeyCode, scrollToPan, zoomSpeed, swimlane, clampToSwimlane, swimlaneMargin, swimlaneHeaderHeight, swimlaneResizable, swimlaneBottomReveal, renderSwimlaneHeader, renderSwimlaneLabel, onSwimlaneSizeChange, onPaneContextMenu, onSwimlaneHeaderContextMenu, onSwimlaneGroupHeaderContextMenu, onLaneChange, isValidConnection, mapConnection, onConnect, edgesReconnectable, edgeAlignment, onReconnect, onReconnectStart, onReconnectEnd, onNodeDragStart, onNodeDragEnd, onSelectionChange, onFocusChange, className, style, children, }: RivetProps): React.ReactElement;
1023
+ declare function Rivet({ defaultNodes, defaultEdges, nodes: controlledNodes, edges: controlledEdges, onNodesChange, onEdgesChange, defaultViewport, nodeTypes, edgeTypes, defaultEdgeOptions, ariaLabel, renderer, minZoom, maxZoom, gridGap, snapGrid, alignmentGuides, anchorOptions, peers, onLocalPresence, presenceOptions, lockedNodes, canInteractWithLocked, onNodeLockConflict, panOnDrag, selectionOnDrag, selectionKeyCode, multiSelectionKeyCode, scrollToPan, zoomSpeed, swimlane, clampToSwimlane, swimlaneMargin, swimlaneHeaderHeight, swimlaneResizable, swimlaneBottomReveal, renderSwimlaneHeader, renderSwimlaneLabel, onSwimlaneSizeChange, onPaneContextMenu, onSwimlaneHeaderContextMenu, onSwimlaneGroupHeaderContextMenu, onLaneChange, isValidConnection, mapConnection, onConnect, edgesReconnectable, edgeAlignment, edgeAlignmentHysteresis, onReconnect, onReconnectStart, onReconnectEnd, onNodeDragStart, onNodeDragEnd, onSelectionChange, onFocusChange, className, style, children, }: RivetProps): React.ReactElement;
992
1024
 
993
1025
  type RivetContextValue = {
994
1026
  store: RivetStore;
@@ -1213,7 +1245,19 @@ type RivetInstance<TNodeData = unknown, TEdgeData = unknown> = RivetControls & {
1213
1245
  * Idempotent on `(nodeId, anchorId)`; the returned registration re-binds a
1214
1246
  * replaced element or unregisters explicitly.
1215
1247
  */
1216
- registerAnchor: (nodeId: NodeId, anchorId: string, element: Element, options?: AnchorRegistrationOptions) => AnchorRegistration;
1248
+ registerAnchor: (nodeId: NodeId, anchorId: string, element: Element | readonly Element[], options?: AnchorRegistrationOptions) => AnchorRegistration;
1249
+ /**
1250
+ * Mark an anchor's element gone without removing the anchor: geometry is
1251
+ * retained and its edges stay put, so a display that unmounts — an editor
1252
+ * swapping its DOM, a panel closing — doesn't move anything.
1253
+ *
1254
+ * The same signal `<Anchor>` sends when it unmounts, and what
1255
+ * {@link AnchorRegistration.rebind}`(null)` does, as a call that needs no
1256
+ * registration handle in hand. A consumer syncing anchors from a document
1257
+ * (see the Tiptap recipe) re-registers on every change and would otherwise
1258
+ * have to keep a handle per anchor purely to be able to detach.
1259
+ */
1260
+ detachAnchor: (nodeId: NodeId, anchorId: string) => void;
1217
1261
  /**
1218
1262
  * Explicitly remove an anchor and its chrome. Unmounting `<Anchor>` (or
1219
1263
  * rebinding `null`) only *detaches* — geometry and edges are retained — so