@kolosal-ai/rivet 0.5.0 → 0.7.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.
Files changed (38) hide show
  1. package/dist/anchor/index.d.ts +3 -3
  2. package/dist/anchor/index.js +1 -36
  3. package/dist/anchor/index.js.map +1 -1
  4. package/dist/chunk-B3YKB2QN.js +2 -0
  5. package/dist/chunk-B3YKB2QN.js.map +1 -0
  6. package/dist/chunk-CVSEMPYO.js +2 -0
  7. package/dist/chunk-CVSEMPYO.js.map +1 -0
  8. package/dist/chunk-NLE6BOPW.js +2 -0
  9. package/dist/chunk-NLE6BOPW.js.map +1 -0
  10. package/dist/chunk-RDSWIH3F.js +2 -0
  11. package/dist/chunk-RDSWIH3F.js.map +1 -0
  12. package/dist/chunk-X4VW3W4W.js +2 -0
  13. package/dist/chunk-X4VW3W4W.js.map +1 -0
  14. package/dist/chunk-XTQGGEUS.js +2 -0
  15. package/dist/chunk-XTQGGEUS.js.map +1 -0
  16. package/dist/index.d.ts +127 -40
  17. package/dist/index.js +1 -6176
  18. package/dist/index.js.map +1 -1
  19. package/dist/presence/index.d.ts +3 -3
  20. package/dist/presence/index.js +1 -37
  21. package/dist/presence/index.js.map +1 -1
  22. package/dist/{registry-ClCPnBac.d.ts → registry-Vc6r10MJ.d.ts} +24 -7
  23. package/dist/svg/index.d.ts +2 -2
  24. package/dist/svg/index.js +2 -128
  25. package/dist/svg/index.js.map +1 -1
  26. package/dist/swimlane/index.d.ts +4 -4
  27. package/dist/swimlane/index.js +1 -2
  28. package/dist/{types-D3aldmsO.d.ts → types-BPtqcAVr.d.ts} +52 -1
  29. package/dist/{use-node-lock-CbS59CRE.d.ts → use-node-lock-BFQQAr8M.d.ts} +1 -1
  30. package/package.json +1 -1
  31. package/dist/chunk-6XRQSAQT.js +0 -148
  32. package/dist/chunk-6XRQSAQT.js.map +0 -1
  33. package/dist/chunk-QHMB4WYT.js +0 -148
  34. package/dist/chunk-QHMB4WYT.js.map +0 -1
  35. package/dist/chunk-TT2N7KWU.js +0 -676
  36. package/dist/chunk-TT2N7KWU.js.map +0 -1
  37. package/dist/chunk-VQYN27OK.js +0 -36
  38. package/dist/chunk-VQYN27OK.js.map +0 -1
@@ -1,6 +1,6 @@
1
- import { P as PeerIdentity, a as PeerRecord, b as PresenceTargetKey, c as PresenceTarget, d as PeerClaim } from '../use-node-lock-CbS59CRE.js';
2
- export { C as CanInteractWithLocked, D as DEFAULT_PRESENCE_OPTIONS, L as LOCK_DEFAULT_REFUSED, e as LocalPresence, f as LockIntent, g as LockInteraction, h as LockRegistry, i as LockTarget, N as NodeLock, j as PEER_COLORS, k as Peer, l as PeerConnection, m as PeerCursorProps, n as PeerOutlineProps, o as PresenceOptions, p as PresenceRegistry, R as ResolvedPresenceOptions, q as anchorTargetKey, r as edgeTargetKey, s as isNodeTarget, t as parsePresenceTarget, u as peerColor, v as presenceTargetEdgeId, w as presenceTargetKey, x as presenceTargetNodeId, y as useNodeLock, z as useNodeLockAllows } from '../use-node-lock-CbS59CRE.js';
3
- import { a as NodeChange } from '../types-D3aldmsO.js';
1
+ import { P as PeerIdentity, a as PeerRecord, b as PresenceTargetKey, c as PresenceTarget, d as PeerClaim } from '../use-node-lock-BFQQAr8M.js';
2
+ export { C as CanInteractWithLocked, D as DEFAULT_PRESENCE_OPTIONS, L as LOCK_DEFAULT_REFUSED, e as LocalPresence, f as LockIntent, g as LockInteraction, h as LockRegistry, i as LockTarget, N as NodeLock, j as PEER_COLORS, k as Peer, l as PeerConnection, m as PeerCursorProps, n as PeerOutlineProps, o as PresenceOptions, p as PresenceRegistry, R as ResolvedPresenceOptions, q as anchorTargetKey, r as edgeTargetKey, s as isNodeTarget, t as parsePresenceTarget, u as peerColor, v as presenceTargetEdgeId, w as presenceTargetKey, x as presenceTargetNodeId, y as useNodeLock, z as useNodeLockAllows } from '../use-node-lock-BFQQAr8M.js';
3
+ import { a as NodeChange } from '../types-BPtqcAVr.js';
4
4
  import 'react';
5
5
 
6
6
  /**
@@ -1,38 +1,2 @@
1
- export { DEFAULT_PRESENCE_OPTIONS, LOCK_DEFAULT_REFUSED, PEER_COLORS, anchorTargetKey, collectPeerOutlines, edgeTargetKey, isNodeTarget, parsePresenceTarget, peerColor, peerOutlinesEqual, presenceTargetEdgeId, presenceTargetKey, presenceTargetNodeId, useNodeLock, useNodeLockAllows, usePeerIdentities } from '../chunk-TT2N7KWU.js';
2
- import { useRivetContext } from '../chunk-VQYN27OK.js';
3
- import { useSyncExternalStore } from 'react';
4
-
5
- function usePeers() {
6
- const { store } = useRivetContext();
7
- return useSyncExternalStore(
8
- store.presence.subscribe,
9
- store.presence.getPeers,
10
- store.presence.getPeers
11
- );
12
- }
13
-
14
- // src/presence/changes.ts
15
- function splitLocalNodeChanges(changes) {
16
- const durable = [];
17
- const ephemeral = [];
18
- for (const change of changes) {
19
- if (change.type === "select") continue;
20
- if (change.type === "position") {
21
- if (change.dragging) ephemeral.push(change);
22
- else durable.push(change);
23
- continue;
24
- }
25
- if (change.type === "dimensions") {
26
- if (change.resizing === void 0) continue;
27
- if (change.resizing) ephemeral.push(change);
28
- else durable.push(change);
29
- continue;
30
- }
31
- durable.push(change);
32
- }
33
- return { durable, ephemeral };
34
- }
35
-
36
- export { splitLocalNodeChanges, usePeers };
37
- //# sourceMappingURL=index.js.map
1
+ export{h as DEFAULT_PRESENCE_OPTIONS,x as LOCK_DEFAULT_REFUSED,i as PEER_COLORS,c as anchorTargetKey,A as collectPeerOutlines,b as edgeTargetKey,d as isNodeTarget,e as parsePresenceTarget,j as peerColor,B as peerOutlinesEqual,g as presenceTargetEdgeId,a as presenceTargetKey,f as presenceTargetNodeId,C as useNodeLock,D as useNodeLockAllows,z as usePeerIdentities}from'../chunk-CVSEMPYO.js';import'../chunk-B3YKB2QN.js';import {c}from'../chunk-NLE6BOPW.js';import'../chunk-RDSWIH3F.js';import {useSyncExternalStore}from'react';function E(){let{store:t}=c();return useSyncExternalStore(t.presence.subscribe,t.presence.getPeers,t.presence.getPeers)}function L(t){let r=[],o=[];for(let e of t)if(e.type!=="select"){if(e.type==="position"){e.dragging?o.push(e):r.push(e);continue}if(e.type==="dimensions"){if(e.resizing===void 0)continue;e.resizing?o.push(e):r.push(e);continue}r.push(e);}return {durable:r,ephemeral:o}}export{L as splitLocalNodeChanges,E as usePeers};//# sourceMappingURL=index.js.map
38
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/use-peers.ts","../../src/presence/changes.ts"],"names":[],"mappings":";;;;AAeO,SAAS,QAAA,GAAkC;AAChD,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,eAAA,EAAgB;AAClC,EAAA,OAAO,oBAAA;AAAA,IACL,MAAM,QAAA,CAAS,SAAA;AAAA,IACf,MAAM,QAAA,CAAS,QAAA;AAAA,IACf,MAAM,QAAA,CAAS;AAAA,GACjB;AACF;;;ACsEO,SAAS,sBACd,OAAA,EAC6B;AAC7B,EAAA,MAAM,UAA+B,EAAC;AACtC,EAAA,MAAM,YAA0C,EAAC;AAEjD,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,IAAI,MAAA,CAAO,SAAS,QAAA,EAAU;AAE9B,IAAA,IAAI,MAAA,CAAO,SAAS,UAAA,EAAY;AAC9B,MAAA,IAAI,MAAA,CAAO,QAAA,EAAU,SAAA,CAAU,IAAA,CAAK,MAAM,CAAA;AAAA,WACrC,OAAA,CAAQ,KAAK,MAAM,CAAA;AACxB,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,MAAA,CAAO,SAAS,YAAA,EAAc;AAEhC,MAAA,IAAI,MAAA,CAAO,aAAa,MAAA,EAAW;AACnC,MAAA,IAAI,MAAA,CAAO,QAAA,EAAU,SAAA,CAAU,IAAA,CAAK,MAAM,CAAA;AAAA,WACrC,OAAA,CAAQ,KAAK,MAAM,CAAA;AACxB,MAAA;AAAA,IACF;AAEA,IAAA,OAAA,CAAQ,KAAK,MAAM,CAAA;AAAA,EACrB;AAEA,EAAA,OAAO,EAAE,SAAS,SAAA,EAAU;AAC9B","file":"index.js","sourcesContent":["import { useSyncExternalStore } from \"react\"\nimport { useRivetContext } from \"../context\"\nimport type { PeerRecord } from \"../presence/registry\"\n\n/**\n * The peers currently in the document — for participant chrome you render\n * yourself: an avatar stack, a follow button, a \"3 others editing\" line.\n *\n * Re-renders when the **roster** changes: someone joins or leaves, is renamed,\n * or changes what they have selected or held. It deliberately does *not*\n * re-render on cursor frames — those arrive at pointer rate and are already\n * painted on the canvas. If you're rendering cursors in the DOM yourself (with\n * `presenceOptions={{ renderCursors: false }}`), read them off the frame\n * channel instead, where the rest of the imperative chrome lives.\n */\nexport function usePeers(): readonly PeerRecord[] {\n const { store } = useRivetContext()\n return useSyncExternalStore(\n store.presence.subscribe,\n store.presence.getPeers,\n store.presence.getPeers,\n )\n}\n","/**\n * Which local changes belong on the wire, and at which speed.\n *\n * Publishing a change batch to peers means answering three questions per\n * change: is this the document (send it, keep it), is it a live gesture frame\n * (send it, throw it away on the next one), or is it this client talking to\n * itself (never send it). The answers are rivet semantics — they depend on\n * which fields are gesture-bounded and which flags mark a frame as uncommitted\n * — and every one of them fails *silently* when a consumer guesses wrong:\n * nothing throws, the shared graph just misbehaves in ways that read like\n * transport bugs.\n *\n * So the classification lives here. Transport policy does not: throttling the\n * ephemeral half, batching or coalescing the durable half, and deciding what a\n * frame even looks like on your wire are all yours.\n */\n\nimport type { NodeChange } from \"../types\"\n\n/**\n * A change that can carry an uncommitted gesture frame — the only two kinds a\n * gesture produces mid-flight. Narrower than {@link NodeChange} so that the\n * ephemeral half always has an `id` to key a transform by.\n */\nexport type EphemeralNodeChange<TData = unknown> = Extract<\n NodeChange<TData>,\n { type: \"position\" | \"dimensions\" }\n>\n\n/** The two halves of a local batch, as returned by {@link splitLocalNodeChanges}. */\nexport type LocalNodeChangeSplit<TData = unknown> = {\n /**\n * Document mutations. Send these, and expect peers to keep them: they're\n * what a client that reloads has to see.\n */\n durable: NodeChange<TData>[]\n /**\n * Uncommitted gesture frames — where this client currently has a node, not\n * where it has agreed to put it. Send them as ephemeral state (rivet's own\n * inbound door for this is `setPeerNodeTransform`, which paints without\n * touching the graph); drop them freely under load, because the commit that\n * closes the gesture arrives in {@link LocalNodeChangeSplit.durable}.\n *\n * One gesture frame can appear twice in a batch — a resize dragged from a\n * top-left handle emits both a `dimensions` and a `position` change for the\n * same node — so key by `id` rather than counting.\n */\n ephemeral: EphemeralNodeChange<TData>[]\n}\n\n/**\n * Split a local `onNodesChange` batch into the half peers must keep and the\n * half they only need until the next frame. Changes that must not travel at\n * all are in neither array.\n *\n * | change | bucket | why |\n * | --- | --- | --- |\n * | `add` / `remove` / `replace` | durable | the document |\n * | `position`, `dragging` unset or `false` | durable | the commit that closes a move |\n * | `position` with `dragging: true` | ephemeral | a live drag frame |\n * | `dimensions` with `resizing: false` | durable | the commit that closes a resize |\n * | `dimensions` with `resizing: true` | ephemeral | a live resize frame |\n * | `dimensions` with no `resizing` flag | **dropped** | a measurement |\n * | `select` | **dropped** | selection is presence |\n *\n * The two drops are the ones worth having in the library. A `select` on the\n * wire makes every client select in unison — selection is a fact about a\n * person, and it travels through `onLocalPresence` instead. And an unflagged\n * `dimensions` is this client reporting its own measured DOM box, which every\n * client produces for itself; publishing yours makes peers fight each other's\n * layout. That distinction — a measurement echo versus a real resize — is\n * invisible from outside without reading the store.\n *\n * Only call this on batches whose `ChangeMeta.origin` is `\"local\"`; a batch\n * re-emitted after `applyRemote` is a peer's edit coming back, and sending it\n * on is the echo loop.\n *\n * ```ts\n * onNodesChange={(changes, meta) => {\n * setNodes((current) => applyNodeChanges(changes, current))\n * if (meta.origin !== \"local\") return\n * const { durable, ephemeral } = splitLocalNodeChanges(changes)\n * if (durable.length > 0) channel.send({ kind: \"nodes\", changes: durable })\n * for (const change of ephemeral) {\n * channel.send({ kind: \"transform\", id: change.id, rect: rivet.getNodeRect(change.id) })\n * }\n * }}\n * ```\n *\n * Edge changes need no equivalent: they carry no gesture-bounded fields, so\n * dropping `select` is the whole rule.\n */\nexport function splitLocalNodeChanges<TData = unknown>(\n changes: readonly NodeChange<TData>[],\n): LocalNodeChangeSplit<TData> {\n const durable: NodeChange<TData>[] = []\n const ephemeral: EphemeralNodeChange<TData>[] = []\n\n for (const change of changes) {\n if (change.type === \"select\") continue\n\n if (change.type === \"position\") {\n if (change.dragging) ephemeral.push(change)\n else durable.push(change)\n continue\n }\n\n if (change.type === \"dimensions\") {\n // No flag at all means nobody resized: the store measured the DOM.\n if (change.resizing === undefined) continue\n if (change.resizing) ephemeral.push(change)\n else durable.push(change)\n continue\n }\n\n durable.push(change)\n }\n\n return { durable, ephemeral }\n}\n"]}
1
+ {"version":3,"sources":["../../src/hooks/use-peers.ts","../../src/presence/changes.ts"],"names":["usePeers","store","useRivetContext","useSyncExternalStore","splitLocalNodeChanges","changes","durable","ephemeral","change"],"mappings":"+gBAeO,SAASA,GAAkC,CAChD,GAAM,CAAE,KAAA,CAAAC,CAAM,EAAIC,CAAAA,EAAgB,CAClC,OAAOC,oBAAAA,CACLF,CAAAA,CAAM,SAAS,SAAA,CACfA,CAAAA,CAAM,SAAS,QAAA,CACfA,CAAAA,CAAM,SAAS,QACjB,CACF,CCsEO,SAASG,CAAAA,CACdC,EAC6B,CAC7B,IAAMC,EAA+B,EAAC,CAChCC,EAA0C,EAAC,CAEjD,QAAWC,CAAAA,IAAUH,CAAAA,CACnB,GAAIG,CAAAA,CAAO,IAAA,GAAS,SAEpB,CAAA,GAAIA,CAAAA,CAAO,OAAS,UAAA,CAAY,CAC1BA,EAAO,QAAA,CAAUD,CAAAA,CAAU,KAAKC,CAAM,CAAA,CACrCF,EAAQ,IAAA,CAAKE,CAAM,EACxB,QACF,CAEA,GAAIA,CAAAA,CAAO,IAAA,GAAS,aAAc,CAEhC,GAAIA,EAAO,QAAA,GAAa,MAAA,CAAW,SAC/BA,CAAAA,CAAO,QAAA,CAAUD,EAAU,IAAA,CAAKC,CAAM,EACrCF,CAAAA,CAAQ,IAAA,CAAKE,CAAM,CAAA,CACxB,QACF,CAEAF,CAAAA,CAAQ,IAAA,CAAKE,CAAM,EAAA,CAGrB,OAAO,CAAE,OAAA,CAAAF,CAAAA,CAAS,SAAA,CAAAC,CAAU,CAC9B","file":"index.js","sourcesContent":["import { useSyncExternalStore } from \"react\"\nimport { useRivetContext } from \"../context\"\nimport type { PeerRecord } from \"../presence/registry\"\n\n/**\n * The peers currently in the document — for participant chrome you render\n * yourself: an avatar stack, a follow button, a \"3 others editing\" line.\n *\n * Re-renders when the **roster** changes: someone joins or leaves, is renamed,\n * or changes what they have selected or held. It deliberately does *not*\n * re-render on cursor frames — those arrive at pointer rate and are already\n * painted on the canvas. If you're rendering cursors in the DOM yourself (with\n * `presenceOptions={{ renderCursors: false }}`), read them off the frame\n * channel instead, where the rest of the imperative chrome lives.\n */\nexport function usePeers(): readonly PeerRecord[] {\n const { store } = useRivetContext()\n return useSyncExternalStore(\n store.presence.subscribe,\n store.presence.getPeers,\n store.presence.getPeers,\n )\n}\n","/**\n * Which local changes belong on the wire, and at which speed.\n *\n * Publishing a change batch to peers means answering three questions per\n * change: is this the document (send it, keep it), is it a live gesture frame\n * (send it, throw it away on the next one), or is it this client talking to\n * itself (never send it). The answers are rivet semantics — they depend on\n * which fields are gesture-bounded and which flags mark a frame as uncommitted\n * — and every one of them fails *silently* when a consumer guesses wrong:\n * nothing throws, the shared graph just misbehaves in ways that read like\n * transport bugs.\n *\n * So the classification lives here. Transport policy does not: throttling the\n * ephemeral half, batching or coalescing the durable half, and deciding what a\n * frame even looks like on your wire are all yours.\n */\n\nimport type { NodeChange } from \"../types\"\n\n/**\n * A change that can carry an uncommitted gesture frame — the only two kinds a\n * gesture produces mid-flight. Narrower than {@link NodeChange} so that the\n * ephemeral half always has an `id` to key a transform by.\n */\nexport type EphemeralNodeChange<TData = unknown> = Extract<\n NodeChange<TData>,\n { type: \"position\" | \"dimensions\" }\n>\n\n/** The two halves of a local batch, as returned by {@link splitLocalNodeChanges}. */\nexport type LocalNodeChangeSplit<TData = unknown> = {\n /**\n * Document mutations. Send these, and expect peers to keep them: they're\n * what a client that reloads has to see.\n */\n durable: NodeChange<TData>[]\n /**\n * Uncommitted gesture frames — where this client currently has a node, not\n * where it has agreed to put it. Send them as ephemeral state (rivet's own\n * inbound door for this is `setPeerNodeTransform`, which paints without\n * touching the graph); drop them freely under load, because the commit that\n * closes the gesture arrives in {@link LocalNodeChangeSplit.durable}.\n *\n * One gesture frame can appear twice in a batch — a resize dragged from a\n * top-left handle emits both a `dimensions` and a `position` change for the\n * same node — so key by `id` rather than counting.\n */\n ephemeral: EphemeralNodeChange<TData>[]\n}\n\n/**\n * Split a local `onNodesChange` batch into the half peers must keep and the\n * half they only need until the next frame. Changes that must not travel at\n * all are in neither array.\n *\n * | change | bucket | why |\n * | --- | --- | --- |\n * | `add` / `remove` / `replace` | durable | the document |\n * | `position`, `dragging` unset or `false` | durable | the commit that closes a move |\n * | `position` with `dragging: true` | ephemeral | a live drag frame |\n * | `dimensions` with `resizing: false` | durable | the commit that closes a resize |\n * | `dimensions` with `resizing: true` | ephemeral | a live resize frame |\n * | `dimensions` with no `resizing` flag | **dropped** | a measurement |\n * | `select` | **dropped** | selection is presence |\n *\n * The two drops are the ones worth having in the library. A `select` on the\n * wire makes every client select in unison — selection is a fact about a\n * person, and it travels through `onLocalPresence` instead. And an unflagged\n * `dimensions` is this client reporting its own measured DOM box, which every\n * client produces for itself; publishing yours makes peers fight each other's\n * layout. That distinction — a measurement echo versus a real resize — is\n * invisible from outside without reading the store.\n *\n * Only call this on batches whose `ChangeMeta.origin` is `\"local\"`; a batch\n * re-emitted after `applyRemote` is a peer's edit coming back, and sending it\n * on is the echo loop.\n *\n * ```ts\n * onNodesChange={(changes, meta) => {\n * setNodes((current) => applyNodeChanges(changes, current))\n * if (meta.origin !== \"local\") return\n * const { durable, ephemeral } = splitLocalNodeChanges(changes)\n * if (durable.length > 0) channel.send({ kind: \"nodes\", changes: durable })\n * for (const change of ephemeral) {\n * channel.send({ kind: \"transform\", id: change.id, rect: rivet.getNodeRect(change.id) })\n * }\n * }}\n * ```\n *\n * Edge changes need no equivalent: they carry no gesture-bounded fields, so\n * dropping `select` is the whole rule.\n */\nexport function splitLocalNodeChanges<TData = unknown>(\n changes: readonly NodeChange<TData>[],\n): LocalNodeChangeSplit<TData> {\n const durable: NodeChange<TData>[] = []\n const ephemeral: EphemeralNodeChange<TData>[] = []\n\n for (const change of changes) {\n if (change.type === \"select\") continue\n\n if (change.type === \"position\") {\n if (change.dragging) ephemeral.push(change)\n else durable.push(change)\n continue\n }\n\n if (change.type === \"dimensions\") {\n // No flag at all means nobody resized: the store measured the DOM.\n if (change.resizing === undefined) continue\n if (change.resizing) ephemeral.push(change)\n else durable.push(change)\n continue\n }\n\n durable.push(change)\n }\n\n return { durable, ephemeral }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { H as HandlePosition, N as NodeId, R as Rect, C as Connection } from './types-D3aldmsO.js';
1
+ import { H as HandlePosition, N as NodeId, R as Rect, C as Connection } from './types-BPtqcAVr.js';
2
2
 
3
3
  /**
4
4
  * Anchor — a connectable point bound to a DOM element rendered inside a node.
@@ -50,10 +50,16 @@ type AnchorGeometry = {
50
50
  */
51
51
  type AnchorStrayPolicy = "dynamic" | "none";
52
52
  /**
53
- * A registered anchor. `element` is `null` while detached — the consumer
54
- * unmounted the display (or an editor replaced the DOM node) without
55
- * unregistering, so the last-known `geometry` is retained and edges stay put
56
- * (stale retention). Unregistering is the explicit removal signal.
53
+ * A registered anchor. Empty `elements` means detached — the consumer unmounted
54
+ * the display (or an editor replaced the DOM node) without unregistering, so
55
+ * the last-known `geometry` is retained and edges stay put (stale retention).
56
+ * Unregistering is the explicit removal signal.
57
+ *
58
+ * An anchor can be bound to **several** elements, because one thing on screen
59
+ * isn't always one element: a rich-text mark interrupted by another mark
60
+ * renders as two spans, and an imported graphic can be a group of paths. They
61
+ * are treated as fragments of a single run — first line of the first, last line
62
+ * of the last — which is what {@link AnchorGeometry} already describes.
57
63
  */
58
64
  type AnchorRecord = {
59
65
  nodeId: NodeId;
@@ -70,6 +76,16 @@ type AnchorRecord = {
70
76
  * it, and never reads it.
71
77
  */
72
78
  data?: unknown;
79
+ /**
80
+ * Every element this anchor is bound to, in document order. Empty while
81
+ * detached.
82
+ */
83
+ elements: readonly Element[];
84
+ /**
85
+ * The first of {@link elements}, or `null` when detached — the answer for the
86
+ * overwhelmingly common one-element anchor, kept so call sites that only need
87
+ * "is this anchor live, and where" don't have to index.
88
+ */
73
89
  element: Element | null;
74
90
  /** Null until the first successful measurement. */
75
91
  geometry: AnchorGeometry | null;
@@ -110,11 +126,12 @@ declare function findSelectableAnchor(records: readonly AnchorRecord[], target:
110
126
  */
111
127
  type AnchorRegistration = {
112
128
  /**
113
- * Re-attach the anchor after the renderer replaced its DOM node. Passing
129
+ * Re-attach the anchor after the renderer replaced its DOM node — one element
130
+ * or, for content that renders as several, all of its fragments. Passing
114
131
  * `null` detaches instead: geometry is retained and edges stay put until the
115
132
  * element (or a replacement) is rebound or the anchor is unregistered.
116
133
  */
117
- rebind: (element: Element | null) => void;
134
+ rebind: (element: Element | readonly Element[] | null) => void;
118
135
  /** Explicit removal — drops the anchor and the chrome rendered for it. */
119
136
  unregister: () => void;
120
137
  };
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties } from 'react';
3
- import { h as AnchorStrayPolicy } from '../registry-ClCPnBac.js';
4
- import '../types-D3aldmsO.js';
3
+ import { h as AnchorStrayPolicy } from '../registry-Vc6r10MJ.js';
4
+ import '../types-BPtqcAVr.js';
5
5
 
6
6
  /**
7
7
  * The extract seam behind `<SvgArtwork>` — which elements inside a parsed
package/dist/svg/index.js CHANGED
@@ -1,129 +1,3 @@
1
- import { RivetContext, RivetNodeContext } from '../chunk-VQYN27OK.js';
2
- import { useRef, useContext, useLayoutEffect } from 'react';
3
- import { jsx } from 'react/jsx-runtime';
4
-
5
- // src/svg/extract.ts
6
- var extractAnchors = (root) => {
7
- const targets = [];
8
- const taken = /* @__PURE__ */ new Set();
9
- for (const element of root.querySelectorAll("[data-anchor]")) {
10
- if (element.closest("defs")) continue;
11
- const anchorId = element.getAttribute("data-anchor")?.trim() || element.id;
12
- if (!anchorId || taken.has(anchorId)) continue;
13
- taken.add(anchorId);
14
- targets.push({ anchorId, element });
15
- }
16
- return targets;
17
- };
18
-
19
- // src/svg/parse.ts
20
- var SVG_NS = "http://www.w3.org/2000/svg";
21
- var DENIED_ELEMENTS = "script, foreignObject";
22
- var parseSvg = (markup) => {
23
- if (typeof DOMParser === "undefined") return null;
24
- const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
25
- if (doc.querySelector("parsererror")) return null;
26
- const root = doc.documentElement;
27
- if (root.namespaceURI !== SVG_NS || root.localName !== "svg") return null;
28
- const svg = root;
29
- for (const el of svg.querySelectorAll(DENIED_ELEMENTS)) el.remove();
30
- const strip = (el) => {
31
- for (const attr of Array.from(el.attributes)) {
32
- const name = attr.name.toLowerCase();
33
- const isRef = name === "href" || name === "xlink:href";
34
- const denied = name.startsWith("on") || isRef && !attr.value.trim().startsWith("#");
35
- if (denied) el.removeAttribute(attr.name);
36
- }
37
- };
38
- strip(svg);
39
- for (const el of svg.querySelectorAll("*")) strip(el);
40
- return svg;
41
- };
42
- function readIntrinsicSize(svg) {
43
- const box = viewBoxNumbers(svg.getAttribute("viewBox"));
44
- const width = parseLength(svg.getAttribute("width")) ?? box?.[2] ?? null;
45
- const height = parseLength(svg.getAttribute("height")) ?? box?.[3] ?? null;
46
- if (!width || !height || width <= 0 || height <= 0) return null;
47
- return { width, height };
48
- }
49
- function viewBoxNumbers(viewBox) {
50
- if (!viewBox) return null;
51
- const parts = viewBox.split(/[\s,]+/).map(Number);
52
- if (parts.length !== 4 || !parts.every(Number.isFinite)) return null;
53
- return parts;
54
- }
55
- function parseLength(value) {
56
- if (!value || value.includes("%")) return null;
57
- const n = Number.parseFloat(value);
58
- return Number.isFinite(n) ? n : null;
59
- }
60
- function SvgArtwork({
61
- markup,
62
- parse,
63
- extract,
64
- anchorStrays,
65
- selectableAnchors,
66
- className,
67
- style
68
- }) {
69
- const ref = useRef(null);
70
- const rivet = useContext(RivetContext);
71
- const node = useContext(RivetNodeContext);
72
- const store = rivet?.store ?? null;
73
- const nodeId = node?.nodeId;
74
- const wired = useRef(/* @__PURE__ */ new Set());
75
- useLayoutEffect(() => {
76
- const container = ref.current;
77
- if (!container) return;
78
- const svg = (parse ?? parseSvg)(markup);
79
- if (svg) {
80
- fitToBox(svg);
81
- container.replaceChildren(svg);
82
- } else {
83
- container.replaceChildren();
84
- }
85
- if (!store || nodeId === void 0) return;
86
- const targets = svg ? (extract ?? extractAnchors)(svg) : [];
87
- const next = /* @__PURE__ */ new Set();
88
- for (const target of targets) {
89
- next.add(target.anchorId);
90
- store.registerAnchor(nodeId, target.anchorId, target.element, {
91
- color: target.color,
92
- strays: anchorStrays ?? "none",
93
- selectable: target.selectable ?? selectableAnchors,
94
- data: target.data
95
- });
96
- }
97
- for (const id of wired.current) {
98
- if (!next.has(id)) store.unregisterAnchor(nodeId, id);
99
- }
100
- wired.current = next;
101
- return () => {
102
- for (const id of wired.current) store.detachAnchor(nodeId, id);
103
- };
104
- }, [markup, parse, extract, anchorStrays, selectableAnchors, store, nodeId]);
105
- return /* @__PURE__ */ jsx(
106
- "div",
107
- {
108
- ref,
109
- "data-rivet-svg-artwork": "",
110
- className,
111
- style: { width: "100%", height: "100%", pointerEvents: "none", ...style }
112
- }
113
- );
114
- }
115
- function fitToBox(svg) {
116
- if (!svg.getAttribute("viewBox")) {
117
- const size = readIntrinsicSize(svg);
118
- if (size) svg.setAttribute("viewBox", `0 0 ${size.width} ${size.height}`);
119
- }
120
- svg.setAttribute("width", "100%");
121
- svg.setAttribute("height", "100%");
122
- if (!svg.getAttribute("preserveAspectRatio"))
123
- svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
124
- svg.style.display = "block";
125
- }
126
-
127
- export { SvgArtwork, extractAnchors, parseSvg, readIntrinsicSize };
128
- //# sourceMappingURL=index.js.map
1
+ import {a,j}from'../chunk-NLE6BOPW.js';import {useRef,useContext,useLayoutEffect}from'react';import {jsx}from'react/jsx-runtime';var h=t=>{let e=[],n=new Set;for(let r of t.querySelectorAll("[data-anchor]")){if(r.closest("defs"))continue;let i=r.getAttribute("data-anchor")?.trim()||r.id;!i||n.has(i)||(n.add(i),e.push({anchorId:i,element:r}));}return e};var P="http://www.w3.org/2000/svg",k="script, foreignObject",d=t=>{if(typeof DOMParser>"u")return null;let e=new DOMParser().parseFromString(t,"image/svg+xml");if(e.querySelector("parsererror"))return null;let n=e.documentElement;if(n.namespaceURI!==P||n.localName!=="svg")return null;let r=n;for(let s of r.querySelectorAll(k))s.remove();let i=s=>{for(let a of Array.from(s.attributes)){let c=a.name.toLowerCase(),g=c==="href"||c==="xlink:href";(c.startsWith("on")||g&&!a.value.trim().startsWith("#"))&&s.removeAttribute(a.name);}};i(r);for(let s of r.querySelectorAll("*"))i(s);return r};function A(t){let e=N(t.getAttribute("viewBox")),n=x(t.getAttribute("width"))??e?.[2]??null,r=x(t.getAttribute("height"))??e?.[3]??null;return !n||!r||n<=0||r<=0?null:{width:n,height:r}}function N(t){if(!t)return null;let e=t.split(/[\s,]+/).map(Number);return e.length!==4||!e.every(Number.isFinite)?null:e}function x(t){if(!t||t.includes("%"))return null;let e=Number.parseFloat(t);return Number.isFinite(e)?e:null}function V({markup:t,parse:e,extract:n,anchorStrays:r,selectableAnchors:i,className:s,style:a$1}){let c=useRef(null),g=useContext(a),v=useContext(j),l=g?.store??null,u=v?.nodeId,f=useRef(new Set);return useLayoutEffect(()=>{let m=c.current;if(!m)return;let p=(e??d)(t);if(p?(C(p),m.replaceChildren(p)):m.replaceChildren(),!l||u===void 0)return;let I=p?(n??h)(p):[],S=new Set;for(let o of I)S.add(o.anchorId),l.registerAnchor(u,o.anchorId,o.element,{color:o.color,strays:r??"none",selectable:o.selectable??i,data:o.data});for(let o of f.current)S.has(o)||l.unregisterAnchor(u,o);return f.current=S,()=>{for(let o of f.current)l.detachAnchor(u,o);}},[t,e,n,r,i,l,u]),jsx("div",{ref:c,"data-rivet-svg-artwork":"",className:s,style:{width:"100%",height:"100%",pointerEvents:"none",...a$1}})}function C(t){if(!t.getAttribute("viewBox")){let e=A(t);e&&t.setAttribute("viewBox",`0 0 ${e.width} ${e.height}`);}t.setAttribute("width","100%"),t.setAttribute("height","100%"),t.getAttribute("preserveAspectRatio")||t.setAttribute("preserveAspectRatio","xMidYMid meet"),t.style.display="block";}
2
+ export{V as SvgArtwork,h as extractAnchors,d as parseSvg,A as readIntrinsicSize};//# sourceMappingURL=index.js.map
129
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/svg/extract.ts","../../src/svg/parse.ts","../../src/svg/svg-artwork.tsx"],"names":[],"mappings":";;;;;AA2CO,IAAM,cAAA,GAAqC,CAAC,IAAA,KAAS;AAC1D,EAAA,MAAM,UAA6B,EAAC;AACpC,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAY;AAC9B,EAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,gBAAA,CAAiB,eAAe,CAAA,EAAG;AAC5D,IAAA,IAAI,OAAA,CAAQ,OAAA,CAAQ,MAAM,CAAA,EAAG;AAC7B,IAAA,MAAM,WAAW,OAAA,CAAQ,YAAA,CAAa,aAAa,CAAA,EAAG,IAAA,MAAU,OAAA,CAAQ,EAAA;AACxE,IAAA,IAAI,CAAC,QAAA,IAAY,KAAA,CAAM,GAAA,CAAI,QAAQ,CAAA,EAAG;AACtC,IAAA,KAAA,CAAM,IAAI,QAAQ,CAAA;AAClB,IAAA,OAAA,CAAQ,IAAA,CAAK,EAAE,QAAA,EAAU,OAAA,EAAS,CAAA;AAAA,EACpC;AACA,EAAA,OAAO,OAAA;AACT;;;AC3CA,IAAM,MAAA,GAAS,4BAAA;AAUf,IAAM,eAAA,GAAkB,uBAAA;AASjB,IAAM,QAAA,GAAsB,CAAC,MAAA,KAAW;AAC7C,EAAA,IAAI,OAAO,SAAA,KAAc,WAAA,EAAa,OAAO,IAAA;AAC7C,EAAA,MAAM,MAAM,IAAI,SAAA,EAAU,CAAE,eAAA,CAAgB,QAAQ,eAAe,CAAA;AACnE,EAAA,IAAI,GAAA,CAAI,aAAA,CAAc,aAAa,CAAA,EAAG,OAAO,IAAA;AAC7C,EAAA,MAAM,OAAO,GAAA,CAAI,eAAA;AACjB,EAAA,IAAI,KAAK,YAAA,KAAiB,MAAA,IAAU,IAAA,CAAK,SAAA,KAAc,OAAO,OAAO,IAAA;AACrE,EAAA,MAAM,GAAA,GAAM,IAAA;AAEZ,EAAA,KAAA,MAAW,MAAM,GAAA,CAAI,gBAAA,CAAiB,eAAe,CAAA,KAAM,MAAA,EAAO;AAElE,EAAA,MAAM,KAAA,GAAQ,CAAC,EAAA,KAAgB;AAC7B,IAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,CAAM,IAAA,CAAK,EAAA,CAAG,UAAU,CAAA,EAAG;AAC5C,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,WAAA,EAAY;AACnC,MAAA,MAAM,KAAA,GAAQ,IAAA,KAAS,MAAA,IAAU,IAAA,KAAS,YAAA;AAC1C,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,UAAA,CAAW,IAAI,CAAA,IAAM,KAAA,IAAS,CAAC,IAAA,CAAK,KAAA,CAAM,IAAA,EAAK,CAAE,UAAA,CAAW,GAAG,CAAA;AACnF,MAAA,IAAI,MAAA,EAAQ,EAAA,CAAG,eAAA,CAAgB,IAAA,CAAK,IAAI,CAAA;AAAA,IAC1C;AAAA,EACF,CAAA;AACA,EAAA,KAAA,CAAM,GAAG,CAAA;AACT,EAAA,KAAA,MAAW,MAAM,GAAA,CAAI,gBAAA,CAAiB,GAAG,CAAA,QAAS,EAAE,CAAA;AAEpD,EAAA,OAAO,GAAA;AACT;AAYO,SAAS,kBAAkB,GAAA,EAA6C;AAC7E,EAAA,MAAM,GAAA,GAAM,cAAA,CAAe,GAAA,CAAI,YAAA,CAAa,SAAS,CAAC,CAAA;AACtD,EAAA,MAAM,KAAA,GAAQ,YAAY,GAAA,CAAI,YAAA,CAAa,OAAO,CAAC,CAAA,IAAK,GAAA,GAAM,CAAC,CAAA,IAAK,IAAA;AACpE,EAAA,MAAM,MAAA,GAAS,YAAY,GAAA,CAAI,YAAA,CAAa,QAAQ,CAAC,CAAA,IAAK,GAAA,GAAM,CAAC,CAAA,IAAK,IAAA;AACtE,EAAA,IAAI,CAAC,SAAS,CAAC,MAAA,IAAU,SAAS,CAAA,IAAK,MAAA,IAAU,GAAG,OAAO,IAAA;AAC3D,EAAA,OAAO,EAAE,OAAO,MAAA,EAAO;AACzB;AAGA,SAAS,eAAe,OAAA,EAAiE;AACvF,EAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AACrB,EAAA,MAAM,QAAQ,OAAA,CAAQ,KAAA,CAAM,QAAQ,CAAA,CAAE,IAAI,MAAM,CAAA;AAChD,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,IAAK,CAAC,MAAM,KAAA,CAAM,MAAA,CAAO,QAAQ,CAAA,EAAG,OAAO,IAAA;AAChE,EAAA,OAAO,KAAA;AACT;AAGA,SAAS,YAAY,KAAA,EAAqC;AACxD,EAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,QAAA,CAAS,GAAG,GAAG,OAAO,IAAA;AAC1C,EAAA,MAAM,CAAA,GAAI,MAAA,CAAO,UAAA,CAAW,KAAK,CAAA;AACjC,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,GAAI,IAAA;AAClC;ACxBO,SAAS,UAAA,CAAW;AAAA,EACzB,MAAA;AAAA,EACA,KAAA;AAAA,EACA,OAAA;AAAA,EACA,YAAA;AAAA,EACA,iBAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,EAAoB;AAClB,EAAA,MAAM,GAAA,GAAM,OAAuB,IAAI,CAAA;AAGvC,EAAA,MAAM,KAAA,GAAQ,WAAW,YAAY,CAAA;AACrC,EAAA,MAAM,IAAA,GAAO,WAAW,gBAAgB,CAAA;AACxC,EAAA,MAAM,KAAA,GAAQ,OAAO,KAAA,IAAS,IAAA;AAC9B,EAAA,MAAM,SAAS,IAAA,EAAM,MAAA;AAErB,EAAA,MAAM,KAAA,GAAQ,MAAA,iBAAO,IAAI,GAAA,EAAa,CAAA;AAEtC,EAAA,eAAA,CAAgB,MAAM;AACpB,IAAA,MAAM,YAAY,GAAA,CAAI,OAAA;AACtB,IAAA,IAAI,CAAC,SAAA,EAAW;AAChB,IAAA,MAAM,GAAA,GAAA,CAAO,KAAA,IAAS,QAAA,EAAU,MAAM,CAAA;AACtC,IAAA,IAAI,GAAA,EAAK;AACP,MAAA,QAAA,CAAS,GAAG,CAAA;AACZ,MAAA,SAAA,CAAU,gBAAgB,GAAG,CAAA;AAAA,IAC/B,CAAA,MAAO;AACL,MAAA,SAAA,CAAU,eAAA,EAAgB;AAAA,IAC5B;AAEA,IAAA,IAAI,CAAC,KAAA,IAAS,MAAA,KAAW,MAAA,EAAW;AAGpC,IAAA,MAAM,UAAU,GAAA,GAAA,CAAO,OAAA,IAAW,cAAA,EAAgB,GAAG,IAAI,EAAC;AAC1D,IAAA,MAAM,IAAA,uBAAW,GAAA,EAAY;AAC7B,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,QAAQ,CAAA;AACxB,MAAA,KAAA,CAAM,cAAA,CAAe,MAAA,EAAQ,MAAA,CAAO,QAAA,EAAU,OAAO,OAAA,EAAS;AAAA,QAC5D,OAAO,MAAA,CAAO,KAAA;AAAA,QACd,QAAQ,YAAA,IAAgB,MAAA;AAAA,QACxB,UAAA,EAAY,OAAO,UAAA,IAAc,iBAAA;AAAA,QACjC,MAAM,MAAA,CAAO;AAAA,OACd,CAAA;AAAA,IACH;AACA,IAAA,KAAA,MAAW,EAAA,IAAM,MAAM,OAAA,EAAS;AAC9B,MAAA,IAAI,CAAC,KAAK,GAAA,CAAI,EAAE,GAAG,KAAA,CAAM,gBAAA,CAAiB,QAAQ,EAAE,CAAA;AAAA,IACtD;AACA,IAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,IAAA,OAAO,MAAM;AACX,MAAA,KAAA,MAAW,MAAM,KAAA,CAAM,OAAA,EAAS,KAAA,CAAM,YAAA,CAAa,QAAQ,EAAE,CAAA;AAAA,IAC/D,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,MAAA,EAAQ,KAAA,EAAO,SAAS,YAAA,EAAc,iBAAA,EAAmB,KAAA,EAAO,MAAM,CAAC,CAAA;AAE3E,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAA;AAAA,MACA,wBAAA,EAAuB,EAAA;AAAA,MACvB,SAAA;AAAA,MACA,KAAA,EAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,QAAQ,MAAA,EAAQ,aAAA,EAAe,MAAA,EAAQ,GAAG,KAAA;AAAM;AAAA,GAC1E;AAEJ;AAGA,SAAS,SAAS,GAAA,EAA0B;AAC1C,EAAA,IAAI,CAAC,GAAA,CAAI,YAAA,CAAa,SAAS,CAAA,EAAG;AAChC,IAAA,MAAM,IAAA,GAAO,kBAAkB,GAAG,CAAA;AAClC,IAAA,IAAI,IAAA,EAAM,GAAA,CAAI,YAAA,CAAa,SAAA,EAAW,CAAA,IAAA,EAAO,KAAK,KAAK,CAAA,CAAA,EAAI,IAAA,CAAK,MAAM,CAAA,CAAE,CAAA;AAAA,EAC1E;AACA,EAAA,GAAA,CAAI,YAAA,CAAa,SAAS,MAAM,CAAA;AAChC,EAAA,GAAA,CAAI,YAAA,CAAa,UAAU,MAAM,CAAA;AACjC,EAAA,IAAI,CAAC,GAAA,CAAI,YAAA,CAAa,qBAAqB,CAAA;AACzC,IAAA,GAAA,CAAI,YAAA,CAAa,uBAAuB,eAAe,CAAA;AACzD,EAAA,GAAA,CAAI,MAAM,OAAA,GAAU,OAAA;AACtB","file":"index.js","sourcesContent":["/**\n * The extract seam behind `<SvgArtwork>` — which elements inside a parsed\n * artwork become connectable anchors. The default is attribute-driven and\n * convention-free: markup opts elements in with `data-anchor`. Consumers whose\n * artwork follows other conventions (Figma layer names serialized as `id`s, a\n * tool-specific class scheme) swap the extractor via the `extract` prop —\n * granularity lives here, never in core heuristics.\n */\n\n/** An element inside the artwork to expose as a connectable anchor. */\nexport type SvgAnchorTarget = {\n /**\n * Identifies the anchor within its node. Persisted edge handle ids derive\n * from it, so it must be stable across parses of the same markup.\n */\n anchorId: string\n /** The live element inside the mounted artwork the anchor binds to. */\n element: Element\n /** Accent color for the anchor's grab dots. */\n color?: string\n /**\n * Let a click on this element select the anchor. Falls back to the\n * `selectableAnchors` prop on `<SvgArtwork>` when the extractor leaves it out.\n */\n selectable?: boolean\n /** Opaque payload kept on the anchor record — a label, the bounds it came in at. */\n data?: unknown\n}\n\n/**\n * The extract seam's contract: the mounted, sanitized `<svg>` root in, the\n * anchor targets out. Runs after `parse`, re-runs when the markup changes.\n */\nexport type SvgAnchorExtractor = (root: SVGSVGElement) => SvgAnchorTarget[]\n\n/**\n * The default extractor: elements carrying a `data-anchor` attribute become\n * anchors, in document order. The attribute's value is the anchor id; an empty\n * value falls back to the element's `id`. Elements with neither are skipped,\n * as is anything inside `<defs>` (not rendered, so there is nothing to measure)\n * and any id already taken (first occurrence wins). Bare `id`s alone do not\n * opt in — ids are routinely gradient/clip plumbing, not connection points.\n */\nexport const extractAnchors: SvgAnchorExtractor = (root) => {\n const targets: SvgAnchorTarget[] = []\n const taken = new Set<string>()\n for (const element of root.querySelectorAll(\"[data-anchor]\")) {\n if (element.closest(\"defs\")) continue\n const anchorId = element.getAttribute(\"data-anchor\")?.trim() || element.id\n if (!anchorId || taken.has(anchorId)) continue\n taken.add(anchorId)\n targets.push({ anchorId, element })\n }\n return targets\n}\n","/**\n * The default SVG parser behind `<SvgArtwork>` — markup string in, safe live\n * root out, `null` when the input can't be rendered.\n *\n * The policy is strict-and-lossy: anything with scripting or\n * external-reference potential is dropped, never allowlisted-by-default.\n * Fidelity for exotic markup (filters, embedded rasters, cross-document\n * `<use>`) is regained by swapping the parser via the `parse` prop — the trust\n * decision is explicit and lives in the app, not in flags on this default.\n */\n\nconst SVG_NS = \"http://www.w3.org/2000/svg\"\n\n/**\n * The parse seam's contract: markup in, a safe, detached `<svg>` root out.\n * Returning `null` refuses to render. Consumers swap in their own (DOMPurify,\n * or a pass-through for self-authored markup) via `<SvgArtwork parse>`.\n */\nexport type SvgParser = (markup: string) => SVGSVGElement | null\n\n/** Elements with scripting or arbitrary-content potential, dropped entirely. */\nconst DENIED_ELEMENTS = \"script, foreignObject\"\n\n/**\n * Parse and sanitize SVG markup. Rejects (`null`) inputs that aren't a\n * well-formed SVG document; strips `<script>` and `<foreignObject>` subtrees,\n * every `on*` event attribute, and any `href`/`xlink:href` that isn't a\n * same-document fragment (`#…`) — which covers `javascript:` and external\n * URLs alike.\n */\nexport const parseSvg: SvgParser = (markup) => {\n if (typeof DOMParser === \"undefined\") return null\n const doc = new DOMParser().parseFromString(markup, \"image/svg+xml\")\n if (doc.querySelector(\"parsererror\")) return null\n const root = doc.documentElement\n if (root.namespaceURI !== SVG_NS || root.localName !== \"svg\") return null\n const svg = root as unknown as SVGSVGElement\n\n for (const el of svg.querySelectorAll(DENIED_ELEMENTS)) el.remove()\n\n const strip = (el: Element) => {\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase()\n const isRef = name === \"href\" || name === \"xlink:href\"\n const denied = name.startsWith(\"on\") || (isRef && !attr.value.trim().startsWith(\"#\"))\n if (denied) el.removeAttribute(attr.name)\n }\n }\n strip(svg)\n for (const el of svg.querySelectorAll(\"*\")) strip(el)\n\n return svg\n}\n\n/** An SVG document's natural size, from its attributes or `viewBox`. */\nexport type SvgIntrinsicSize = { width: number; height: number }\n\n/**\n * Read an SVG root's intrinsic size — the `width`/`height` attributes when\n * they carry positive lengths, the `viewBox` extent otherwise, `null` when\n * neither does. Consumers use it for spawn sizing (how large a pasted artwork\n * should drop in); `<SvgArtwork>` uses it to synthesize a missing `viewBox` so\n * the artwork scales with the node box.\n */\nexport function readIntrinsicSize(svg: SVGSVGElement): SvgIntrinsicSize | null {\n const box = viewBoxNumbers(svg.getAttribute(\"viewBox\"))\n const width = parseLength(svg.getAttribute(\"width\")) ?? box?.[2] ?? null\n const height = parseLength(svg.getAttribute(\"height\")) ?? box?.[3] ?? null\n if (!width || !height || width <= 0 || height <= 0) return null\n return { width, height }\n}\n\n/** Parse a `viewBox` to its four numbers, or `null` if it isn't well-formed. */\nfunction viewBoxNumbers(viewBox: string | null): [number, number, number, number] | null {\n if (!viewBox) return null\n const parts = viewBox.split(/[\\s,]+/).map(Number)\n if (parts.length !== 4 || !parts.every(Number.isFinite)) return null\n return parts as [number, number, number, number]\n}\n\n/** Parse a length attribute (`\"240\"`, `\"240px\"`) to a number, or `null`. Relative lengths (`\"100%\"`) aren't sizes. */\nfunction parseLength(value: string | null): number | null {\n if (!value || value.includes(\"%\")) return null\n const n = Number.parseFloat(value)\n return Number.isFinite(n) ? n : null\n}\n","import { type CSSProperties, useContext, useLayoutEffect, useRef } from \"react\"\nimport type { AnchorStrayPolicy } from \"../anchor/registry\"\nimport { RivetContext, RivetNodeContext } from \"../context\"\nimport { extractAnchors, type SvgAnchorExtractor } from \"./extract\"\nimport { parseSvg, readIntrinsicSize, type SvgParser } from \"./parse\"\n\nexport type SvgArtworkProps = {\n /** Raw SVG markup — parsed and sanitized through `parse` before mounting. */\n markup: string\n /**\n * The parse seam: markup in, safe live root out, `null` to refuse. Defaults\n * to the built-in strict-and-lossy parser; swap in DOMPurify (or a\n * pass-through for markup you author yourself) to trade strictness for\n * fidelity — the trust decision is yours and explicit.\n */\n parse?: SvgParser\n /**\n * The extract seam: which elements inside the artwork become connectable\n * anchors. Defaults to the attribute-driven `extractAnchors` (`data-anchor`\n * opts in). Only consulted when the artwork renders inside a rivet node;\n * return `[]` to render display-only.\n */\n extract?: SvgAnchorExtractor\n /**\n * Stray-handle chrome policy for the extracted anchors. Defaults to\n * `\"none\"` — a shape edge-connects at its boundary through its grab dots,\n * and a floating perimeter handle beside artwork reads as clutter. Pass\n * `\"dynamic\"` for the inline-text-style perimeter chrome.\n */\n anchorStrays?: AnchorStrayPolicy\n /**\n * Make every extracted anchor clickable, so a press on a shape selects it.\n * Defaults to `false`. An extractor that sets `selectable` on a target wins,\n * so per-element control stays available. Shapes are the case this exists\n * for: the element under the pointer *is* the thing being pointed at, and\n * hit-testing it in the browser follows the path rather than its box.\n */\n selectableAnchors?: boolean\n className?: string\n style?: CSSProperties\n}\n\n/**\n * Renders SVG markup inside a node, filling the node box. The markup is parsed\n * through the `parse` seam, then the root is normalized to scale with the box:\n * `width`/`height` become `100%`, a missing `viewBox` is synthesized from the\n * intrinsic size, and a missing `preserveAspectRatio` defaults to\n * `\"xMidYMid meet\"` (a value the document already carries is respected).\n *\n * Inside a rivet node, elements the `extract` seam selects are registered as\n * anchors on that node — measured, given grab-dot chrome, connectable like any\n * `<Anchor>`. Extraction re-runs when the markup changes, diffing by anchor\n * id: surviving ids are rebound (geometry retained, edges stay put), ids gone\n * from the new markup are unregistered — the shape was edited away for good.\n * Unmounting only *detaches* (stale retention), matching `<Anchor>`. Outside a\n * node — or outside `<Rivet>` entirely — the component is display-only.\n *\n * The wrapper ignores pointer events so a press falls through to the node\n * wrapper — the whole graphic stays draggable. Nothing renders when `parse`\n * refuses the markup.\n */\nexport function SvgArtwork({\n markup,\n parse,\n extract,\n anchorStrays,\n selectableAnchors,\n className,\n style,\n}: SvgArtworkProps) {\n const ref = useRef<HTMLDivElement>(null)\n // Read, not required: outside <Rivet> or outside a node the artwork still\n // renders, it just exposes no anchors.\n const rivet = useContext(RivetContext)\n const node = useContext(RivetNodeContext)\n const store = rivet?.store ?? null\n const nodeId = node?.nodeId\n /** Anchor ids currently registered from this artwork. */\n const wired = useRef(new Set<string>())\n\n useLayoutEffect(() => {\n const container = ref.current\n if (!container) return\n const svg = (parse ?? parseSvg)(markup)\n if (svg) {\n fitToBox(svg)\n container.replaceChildren(svg)\n } else {\n container.replaceChildren()\n }\n\n if (!store || nodeId === undefined) return\n // Refused markup extracts nothing, so its anchors unregister below — the\n // same fate as ids missing from a successful re-parse.\n const targets = svg ? (extract ?? extractAnchors)(svg) : []\n const next = new Set<string>()\n for (const target of targets) {\n next.add(target.anchorId)\n store.registerAnchor(nodeId, target.anchorId, target.element, {\n color: target.color,\n strays: anchorStrays ?? \"none\",\n selectable: target.selectable ?? selectableAnchors,\n data: target.data,\n })\n }\n for (const id of wired.current) {\n if (!next.has(id)) store.unregisterAnchor(nodeId, id)\n }\n wired.current = next\n return () => {\n for (const id of wired.current) store.detachAnchor(nodeId, id)\n }\n }, [markup, parse, extract, anchorStrays, selectableAnchors, store, nodeId])\n\n return (\n <div\n ref={ref}\n data-rivet-svg-artwork=\"\"\n className={className}\n style={{ width: \"100%\", height: \"100%\", pointerEvents: \"none\", ...style }}\n />\n )\n}\n\n/** Make the root scale with its container instead of its authored size. */\nfunction fitToBox(svg: SVGSVGElement): void {\n if (!svg.getAttribute(\"viewBox\")) {\n const size = readIntrinsicSize(svg)\n if (size) svg.setAttribute(\"viewBox\", `0 0 ${size.width} ${size.height}`)\n }\n svg.setAttribute(\"width\", \"100%\")\n svg.setAttribute(\"height\", \"100%\")\n if (!svg.getAttribute(\"preserveAspectRatio\"))\n svg.setAttribute(\"preserveAspectRatio\", \"xMidYMid meet\")\n svg.style.display = \"block\"\n}\n"]}
1
+ {"version":3,"sources":["../../src/svg/extract.ts","../../src/svg/parse.ts","../../src/svg/svg-artwork.tsx"],"names":["extractAnchors","root","targets","taken","element","anchorId","SVG_NS","DENIED_ELEMENTS","parseSvg","markup","doc","svg","el","strip","attr","name","isRef","readIntrinsicSize","box","viewBoxNumbers","width","parseLength","height","viewBox","parts","value","n","SvgArtwork","parse","extract","anchorStrays","selectableAnchors","className","style","ref","useRef","rivet","useContext","RivetContext","node","RivetNodeContext","store","nodeId","wired","useLayoutEffect","container","fitToBox","next","target","id","jsx","size"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The extract seam behind `<SvgArtwork>` — which elements inside a parsed\n * artwork become connectable anchors. The default is attribute-driven and\n * convention-free: markup opts elements in with `data-anchor`. Consumers whose\n * artwork follows other conventions (Figma layer names serialized as `id`s, a\n * tool-specific class scheme) swap the extractor via the `extract` prop —\n * granularity lives here, never in core heuristics.\n */\n\n/** An element inside the artwork to expose as a connectable anchor. */\nexport type SvgAnchorTarget = {\n /**\n * Identifies the anchor within its node. Persisted edge handle ids derive\n * from it, so it must be stable across parses of the same markup.\n */\n anchorId: string\n /** The live element inside the mounted artwork the anchor binds to. */\n element: Element\n /** Accent color for the anchor's grab dots. */\n color?: string\n /**\n * Let a click on this element select the anchor. Falls back to the\n * `selectableAnchors` prop on `<SvgArtwork>` when the extractor leaves it out.\n */\n selectable?: boolean\n /** Opaque payload kept on the anchor record — a label, the bounds it came in at. */\n data?: unknown\n}\n\n/**\n * The extract seam's contract: the mounted, sanitized `<svg>` root in, the\n * anchor targets out. Runs after `parse`, re-runs when the markup changes.\n */\nexport type SvgAnchorExtractor = (root: SVGSVGElement) => SvgAnchorTarget[]\n\n/**\n * The default extractor: elements carrying a `data-anchor` attribute become\n * anchors, in document order. The attribute's value is the anchor id; an empty\n * value falls back to the element's `id`. Elements with neither are skipped,\n * as is anything inside `<defs>` (not rendered, so there is nothing to measure)\n * and any id already taken (first occurrence wins). Bare `id`s alone do not\n * opt in — ids are routinely gradient/clip plumbing, not connection points.\n */\nexport const extractAnchors: SvgAnchorExtractor = (root) => {\n const targets: SvgAnchorTarget[] = []\n const taken = new Set<string>()\n for (const element of root.querySelectorAll(\"[data-anchor]\")) {\n if (element.closest(\"defs\")) continue\n const anchorId = element.getAttribute(\"data-anchor\")?.trim() || element.id\n if (!anchorId || taken.has(anchorId)) continue\n taken.add(anchorId)\n targets.push({ anchorId, element })\n }\n return targets\n}\n","/**\n * The default SVG parser behind `<SvgArtwork>` — markup string in, safe live\n * root out, `null` when the input can't be rendered.\n *\n * The policy is strict-and-lossy: anything with scripting or\n * external-reference potential is dropped, never allowlisted-by-default.\n * Fidelity for exotic markup (filters, embedded rasters, cross-document\n * `<use>`) is regained by swapping the parser via the `parse` prop — the trust\n * decision is explicit and lives in the app, not in flags on this default.\n */\n\nconst SVG_NS = \"http://www.w3.org/2000/svg\"\n\n/**\n * The parse seam's contract: markup in, a safe, detached `<svg>` root out.\n * Returning `null` refuses to render. Consumers swap in their own (DOMPurify,\n * or a pass-through for self-authored markup) via `<SvgArtwork parse>`.\n */\nexport type SvgParser = (markup: string) => SVGSVGElement | null\n\n/** Elements with scripting or arbitrary-content potential, dropped entirely. */\nconst DENIED_ELEMENTS = \"script, foreignObject\"\n\n/**\n * Parse and sanitize SVG markup. Rejects (`null`) inputs that aren't a\n * well-formed SVG document; strips `<script>` and `<foreignObject>` subtrees,\n * every `on*` event attribute, and any `href`/`xlink:href` that isn't a\n * same-document fragment (`#…`) — which covers `javascript:` and external\n * URLs alike.\n */\nexport const parseSvg: SvgParser = (markup) => {\n if (typeof DOMParser === \"undefined\") return null\n const doc = new DOMParser().parseFromString(markup, \"image/svg+xml\")\n if (doc.querySelector(\"parsererror\")) return null\n const root = doc.documentElement\n if (root.namespaceURI !== SVG_NS || root.localName !== \"svg\") return null\n const svg = root as unknown as SVGSVGElement\n\n for (const el of svg.querySelectorAll(DENIED_ELEMENTS)) el.remove()\n\n const strip = (el: Element) => {\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase()\n const isRef = name === \"href\" || name === \"xlink:href\"\n const denied = name.startsWith(\"on\") || (isRef && !attr.value.trim().startsWith(\"#\"))\n if (denied) el.removeAttribute(attr.name)\n }\n }\n strip(svg)\n for (const el of svg.querySelectorAll(\"*\")) strip(el)\n\n return svg\n}\n\n/** An SVG document's natural size, from its attributes or `viewBox`. */\nexport type SvgIntrinsicSize = { width: number; height: number }\n\n/**\n * Read an SVG root's intrinsic size — the `width`/`height` attributes when\n * they carry positive lengths, the `viewBox` extent otherwise, `null` when\n * neither does. Consumers use it for spawn sizing (how large a pasted artwork\n * should drop in); `<SvgArtwork>` uses it to synthesize a missing `viewBox` so\n * the artwork scales with the node box.\n */\nexport function readIntrinsicSize(svg: SVGSVGElement): SvgIntrinsicSize | null {\n const box = viewBoxNumbers(svg.getAttribute(\"viewBox\"))\n const width = parseLength(svg.getAttribute(\"width\")) ?? box?.[2] ?? null\n const height = parseLength(svg.getAttribute(\"height\")) ?? box?.[3] ?? null\n if (!width || !height || width <= 0 || height <= 0) return null\n return { width, height }\n}\n\n/** Parse a `viewBox` to its four numbers, or `null` if it isn't well-formed. */\nfunction viewBoxNumbers(viewBox: string | null): [number, number, number, number] | null {\n if (!viewBox) return null\n const parts = viewBox.split(/[\\s,]+/).map(Number)\n if (parts.length !== 4 || !parts.every(Number.isFinite)) return null\n return parts as [number, number, number, number]\n}\n\n/** Parse a length attribute (`\"240\"`, `\"240px\"`) to a number, or `null`. Relative lengths (`\"100%\"`) aren't sizes. */\nfunction parseLength(value: string | null): number | null {\n if (!value || value.includes(\"%\")) return null\n const n = Number.parseFloat(value)\n return Number.isFinite(n) ? n : null\n}\n","import { type CSSProperties, useContext, useLayoutEffect, useRef } from \"react\"\nimport type { AnchorStrayPolicy } from \"../anchor/registry\"\nimport { RivetContext, RivetNodeContext } from \"../context\"\nimport { extractAnchors, type SvgAnchorExtractor } from \"./extract\"\nimport { parseSvg, readIntrinsicSize, type SvgParser } from \"./parse\"\n\nexport type SvgArtworkProps = {\n /** Raw SVG markup — parsed and sanitized through `parse` before mounting. */\n markup: string\n /**\n * The parse seam: markup in, safe live root out, `null` to refuse. Defaults\n * to the built-in strict-and-lossy parser; swap in DOMPurify (or a\n * pass-through for markup you author yourself) to trade strictness for\n * fidelity — the trust decision is yours and explicit.\n */\n parse?: SvgParser\n /**\n * The extract seam: which elements inside the artwork become connectable\n * anchors. Defaults to the attribute-driven `extractAnchors` (`data-anchor`\n * opts in). Only consulted when the artwork renders inside a rivet node;\n * return `[]` to render display-only.\n */\n extract?: SvgAnchorExtractor\n /**\n * Stray-handle chrome policy for the extracted anchors. Defaults to\n * `\"none\"` — a shape edge-connects at its boundary through its grab dots,\n * and a floating perimeter handle beside artwork reads as clutter. Pass\n * `\"dynamic\"` for the inline-text-style perimeter chrome.\n */\n anchorStrays?: AnchorStrayPolicy\n /**\n * Make every extracted anchor clickable, so a press on a shape selects it.\n * Defaults to `false`. An extractor that sets `selectable` on a target wins,\n * so per-element control stays available. Shapes are the case this exists\n * for: the element under the pointer *is* the thing being pointed at, and\n * hit-testing it in the browser follows the path rather than its box.\n */\n selectableAnchors?: boolean\n className?: string\n style?: CSSProperties\n}\n\n/**\n * Renders SVG markup inside a node, filling the node box. The markup is parsed\n * through the `parse` seam, then the root is normalized to scale with the box:\n * `width`/`height` become `100%`, a missing `viewBox` is synthesized from the\n * intrinsic size, and a missing `preserveAspectRatio` defaults to\n * `\"xMidYMid meet\"` (a value the document already carries is respected).\n *\n * Inside a rivet node, elements the `extract` seam selects are registered as\n * anchors on that node — measured, given grab-dot chrome, connectable like any\n * `<Anchor>`. Extraction re-runs when the markup changes, diffing by anchor\n * id: surviving ids are rebound (geometry retained, edges stay put), ids gone\n * from the new markup are unregistered — the shape was edited away for good.\n * Unmounting only *detaches* (stale retention), matching `<Anchor>`. Outside a\n * node — or outside `<Rivet>` entirely — the component is display-only.\n *\n * The wrapper ignores pointer events so a press falls through to the node\n * wrapper — the whole graphic stays draggable. Nothing renders when `parse`\n * refuses the markup.\n */\nexport function SvgArtwork({\n markup,\n parse,\n extract,\n anchorStrays,\n selectableAnchors,\n className,\n style,\n}: SvgArtworkProps) {\n const ref = useRef<HTMLDivElement>(null)\n // Read, not required: outside <Rivet> or outside a node the artwork still\n // renders, it just exposes no anchors.\n const rivet = useContext(RivetContext)\n const node = useContext(RivetNodeContext)\n const store = rivet?.store ?? null\n const nodeId = node?.nodeId\n /** Anchor ids currently registered from this artwork. */\n const wired = useRef(new Set<string>())\n\n useLayoutEffect(() => {\n const container = ref.current\n if (!container) return\n const svg = (parse ?? parseSvg)(markup)\n if (svg) {\n fitToBox(svg)\n container.replaceChildren(svg)\n } else {\n container.replaceChildren()\n }\n\n if (!store || nodeId === undefined) return\n // Refused markup extracts nothing, so its anchors unregister below — the\n // same fate as ids missing from a successful re-parse.\n const targets = svg ? (extract ?? extractAnchors)(svg) : []\n const next = new Set<string>()\n for (const target of targets) {\n next.add(target.anchorId)\n store.registerAnchor(nodeId, target.anchorId, target.element, {\n color: target.color,\n strays: anchorStrays ?? \"none\",\n selectable: target.selectable ?? selectableAnchors,\n data: target.data,\n })\n }\n for (const id of wired.current) {\n if (!next.has(id)) store.unregisterAnchor(nodeId, id)\n }\n wired.current = next\n return () => {\n for (const id of wired.current) store.detachAnchor(nodeId, id)\n }\n }, [markup, parse, extract, anchorStrays, selectableAnchors, store, nodeId])\n\n return (\n <div\n ref={ref}\n data-rivet-svg-artwork=\"\"\n className={className}\n style={{ width: \"100%\", height: \"100%\", pointerEvents: \"none\", ...style }}\n />\n )\n}\n\n/** Make the root scale with its container instead of its authored size. */\nfunction fitToBox(svg: SVGSVGElement): void {\n if (!svg.getAttribute(\"viewBox\")) {\n const size = readIntrinsicSize(svg)\n if (size) svg.setAttribute(\"viewBox\", `0 0 ${size.width} ${size.height}`)\n }\n svg.setAttribute(\"width\", \"100%\")\n svg.setAttribute(\"height\", \"100%\")\n if (!svg.getAttribute(\"preserveAspectRatio\"))\n svg.setAttribute(\"preserveAspectRatio\", \"xMidYMid meet\")\n svg.style.display = \"block\"\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { R as Rect, e as RivetNode, V as Vec2, S as Size, s as SwimlaneMargin, r as SwimlaneGroup } from '../types-D3aldmsO.js';
2
- export { L as LaneChange, U as Swimlane, t as SwimlaneHeaderProps, u as SwimlaneLabelProps, v as SwimlaneSizeChange } from '../types-D3aldmsO.js';
1
+ import { R as Rect, e as RivetNode, V as Vec2, S as Size, t as SwimlaneMargin, s as SwimlaneGroup } from '../types-BPtqcAVr.js';
2
+ export { L as LaneChange, W as Swimlane, u as SwimlaneHeaderProps, v as SwimlaneLabelProps, w as SwimlaneSizeChange } from '../types-BPtqcAVr.js';
3
3
  import 'react';
4
4
 
5
5
  /** World height of a group's sticky header band. */
@@ -80,7 +80,7 @@ declare function flattenLanes(groups: ResolvedSwimlaneGroup[]): ResolvedLane[];
80
80
  * (ties prefer `preferredLaneId`), or — with no overlap at all — the nearest by
81
81
  * center. Returns null only when there are no lanes.
82
82
  */
83
- declare function findLaneByCoverage(lanes: ResolvedLane[], top: number, height: number, preferredLaneId?: string): ResolvedLane | null;
83
+ declare function findLaneByCoverage(lanes: readonly ResolvedLane[], top: number, height: number, preferredLaneId?: string): ResolvedLane | null;
84
84
  type LaneClampResult = {
85
85
  position: Vec2;
86
86
  laneId: string | null;
@@ -91,6 +91,6 @@ type LaneClampResult = {
91
91
  * — pass `preferredLaneId` to bias toward the node's current lane. Horizontally
92
92
  * only the left edge is bounded, since bands are effectively infinite.
93
93
  */
94
- declare function clampNodeToLanes(position: Vec2, size: Size, lanes: ResolvedLane[], margin: ResolvedMargin, labelWidth: number, preferredLaneId?: string): LaneClampResult;
94
+ declare function clampNodeToLanes(position: Vec2, size: Size, lanes: readonly ResolvedLane[], margin: ResolvedMargin, labelWidth: number, preferredLaneId?: string): LaneClampResult;
95
95
 
96
96
  export { type LaneClampResult, type ResolvedLane, type ResolvedMargin, type ResolvedSwimlaneGroup, SWIMLANE_GROUP_HEADER_SIZE, SWIMLANE_LABEL_WIDTH, SWIMLANE_MIN_HEIGHT, SwimlaneGroup, SwimlaneMargin, type ViewportElements, type VisibleLane, clampNodeToLanes, findLaneByCoverage, flattenLanes, resolveMargin, resolveSwimlaneGroup, resolveSwimlanes, withAlpha };
@@ -1,3 +1,2 @@
1
- export { SWIMLANE_GROUP_HEADER_SIZE, SWIMLANE_LABEL_WIDTH, SWIMLANE_MIN_HEIGHT, clampNodeToLanes, findLaneByCoverage, flattenLanes, resolveMargin, resolveSwimlaneGroup, resolveSwimlanes, withAlpha } from '../chunk-6XRQSAQT.js';
2
- //# sourceMappingURL=index.js.map
1
+ export{a as SWIMLANE_GROUP_HEADER_SIZE,b as SWIMLANE_LABEL_WIDTH,c as SWIMLANE_MIN_HEIGHT,m as clampNodeToLanes,i as findLaneByCoverage,h as flattenLanes,d as resolveMargin,f as resolveSwimlaneGroup,g as resolveSwimlanes,e as withAlpha}from'../chunk-XTQGGEUS.js';import'../chunk-RDSWIH3F.js';//# sourceMappingURL=index.js.map
3
2
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,33 @@
1
1
  import { ComponentType, CSSProperties } from 'react';
2
2
 
3
+ /**
4
+ * The questions edge-end resolution asks about a node's handles, behind an
5
+ * interface rather than over the registry `Map`.
6
+ *
7
+ * Both questions are per-node, and the registry is keyed by
8
+ * `handleKey(nodeId, handleId)` — so answering them by scanning every record
9
+ * costs O(handles) per edge end per frame, for a prefix the key already
10
+ * carries. Two implementations: the store keeps an index up to date as handles
11
+ * register and unregister ({@link createHandleIndex}), and anything holding
12
+ * only a raw registry map — a direct `buildEdges` call, a third-party
13
+ * {@link EdgeRenderer} — scans it ({@link handleIndexOverMap}).
14
+ */
15
+
16
+ interface HandleIndex {
17
+ /** The record registered under exactly this id, or null. */
18
+ get(nodeId: NodeId, handleId: string): HandleRecord | null;
19
+ /** Every handle on `nodeId`, in registration order. */
20
+ onNode(nodeId: NodeId): readonly HandleRecord[];
21
+ /** A handle on `side` of `nodeId` that `role` can use, or null. */
22
+ onSide(nodeId: NodeId, side: HandlePosition, role: EdgeEnd): HandleRecord | null;
23
+ /**
24
+ * The distinct sides of `nodeId` carrying a handle `role` can use. Empty when
25
+ * the node has no compatible handle at all — an unhandled node attaches at
26
+ * side midpoints, so no side is preferable to another.
27
+ */
28
+ reachableSides(nodeId: NodeId, role: EdgeEnd): HandlePosition[];
29
+ }
30
+
3
31
  /** A point in world (graph) space. */
4
32
  type Vec2 = {
5
33
  x: number;
@@ -347,6 +375,22 @@ type ReconnectDelegate = {
347
375
  /** Extra per-frame inputs for the edge renderer. */
348
376
  type EdgeDrawExtras = {
349
377
  handles?: Map<string, HandleRecord>;
378
+ /**
379
+ * The handle registry as the two questions edge-end resolution actually asks
380
+ * — a compatible handle on a node's side, and which sides a node can reach.
381
+ * The store keeps one current as handles register; absent, resolution scans
382
+ * {@link EdgeDrawExtras.handles} instead, which costs a pass over every
383
+ * registered handle per edge end.
384
+ */
385
+ handleIndex?: HandleIndex;
386
+ /**
387
+ * A node's world box. The store's own definition, which honours explicit
388
+ * `width`/`height` over the measured size and folds in a peer's live resize
389
+ * box. Absent, it is derived from `nodes` and
390
+ * {@link EdgeDrawExtras.worldPositions}, which agrees on everything except a
391
+ * peer's in-flight resize.
392
+ */
393
+ getNodeRect?: (nodeId: NodeId) => Rect;
350
394
  pending?: PendingConnection | null;
351
395
  /** Edge under the pointer, drawn with a hover highlight. */
352
396
  hovered?: EdgeId | null;
@@ -368,6 +412,13 @@ type EdgeDrawExtras = {
368
412
  * resolved sides commit on drop, not here.
369
413
  */
370
414
  liveAlignNodes?: ReadonlySet<NodeId>;
415
+ /**
416
+ * How far the losing axis must dominate before a resolved side switches axes
417
+ * (the `edgeAlignmentHysteresis` prop). Absent falls back to
418
+ * `DEFAULT_ALIGNMENT_HYSTERESIS` — a backend that ignores it resolves with
419
+ * the default band rather than none.
420
+ */
421
+ alignmentHysteresis?: number;
371
422
  /**
372
423
  * Places an anchor end along `side` of its node — the anchor's stray-handle
373
424
  * position as % of that side — or null when the anchor is unknown or
@@ -592,4 +643,4 @@ type RivetControls = {
592
643
  setViewport: (viewport: Viewport) => void;
593
644
  };
594
645
 
595
- export type { AlignmentGuide as A, EdgeDrawExtras as B, Connection as C, DefaultEdgeOptions as D, EdgeId as E, EdgeEnd as F, EdgePathFn as G, HandlePosition as H, CenterNodeOptions as I, EdgeMarker as J, EdgePath as K, LaneChange as L, EdgePathParams as M, NodeId as N, EdgeStyle as O, PendingConnection as P, FitViewOptions as Q, Rect as R, Size as S, NodeComponent as T, Swimlane as U, Vec2 as V, NodeChange as a, HandleType as b, EdgeChange as c, RivetEdge as d, RivetNode as e, NodeProps as f, NodeTypes as g, ChangeOrigin as h, HandleRecord as i, ChangeMeta as j, Viewport as k, NodeGestureEvent as l, NodeLockConflictEvent as m, RivetSelection as n, ReconnectDelegate as o, EdgeTypes as p, EdgeRendererFactory as q, SwimlaneGroup as r, SwimlaneMargin as s, SwimlaneHeaderProps as t, SwimlaneLabelProps as u, SwimlaneSizeChange as v, RivetControls as w, RivetSnapshot as x, EdgeRenderer as y, EdgeRendererOptions as z };
646
+ export type { AlignmentGuide as A, EdgeRendererOptions as B, Connection as C, DefaultEdgeOptions as D, EdgeId as E, EdgeDrawExtras as F, EdgeEnd as G, HandlePosition as H, EdgePathFn as I, CenterNodeOptions as J, EdgeMarker as K, LaneChange as L, EdgePath as M, NodeId as N, EdgePathParams as O, PendingConnection as P, EdgeStyle as Q, Rect as R, Size as S, FitViewOptions as T, NodeComponent as U, Vec2 as V, Swimlane as W, NodeChange as a, HandleType as b, EdgeChange as c, RivetEdge as d, RivetNode as e, NodeProps as f, NodeTypes as g, ChangeOrigin as h, HandleRecord as i, HandleIndex as j, ChangeMeta as k, Viewport as l, NodeGestureEvent as m, NodeLockConflictEvent as n, RivetSelection as o, ReconnectDelegate as p, EdgeTypes as q, EdgeRendererFactory as r, SwimlaneGroup as s, SwimlaneMargin as t, SwimlaneHeaderProps as u, SwimlaneLabelProps as v, SwimlaneSizeChange as w, RivetControls as x, RivetSnapshot as y, EdgeRenderer as z };
@@ -1,4 +1,4 @@
1
- import { N as NodeId, E as EdgeId, V as Vec2, b as HandleType, H as HandlePosition, R as Rect } from './types-D3aldmsO.js';
1
+ import { N as NodeId, E as EdgeId, V as Vec2, b as HandleType, H as HandlePosition, R as Rect } from './types-BPtqcAVr.js';
2
2
  import { ComponentType } from 'react';
3
3
 
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolosal-ai/rivet",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Canvas-rendered node graph for React: DOM nodes, Canvas2D edges, swimlanes, anchors, undo/redo",
5
5
  "license": "MIT",
6
6
  "repository": {