@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.
- package/dist/anchor/index.d.ts +3 -3
- package/dist/anchor/index.js +1 -36
- package/dist/anchor/index.js.map +1 -1
- package/dist/chunk-B3YKB2QN.js +2 -0
- package/dist/chunk-B3YKB2QN.js.map +1 -0
- package/dist/chunk-CVSEMPYO.js +2 -0
- package/dist/chunk-CVSEMPYO.js.map +1 -0
- package/dist/chunk-NLE6BOPW.js +2 -0
- package/dist/chunk-NLE6BOPW.js.map +1 -0
- package/dist/chunk-RDSWIH3F.js +2 -0
- package/dist/chunk-RDSWIH3F.js.map +1 -0
- package/dist/chunk-X4VW3W4W.js +2 -0
- package/dist/chunk-X4VW3W4W.js.map +1 -0
- package/dist/chunk-XTQGGEUS.js +2 -0
- package/dist/chunk-XTQGGEUS.js.map +1 -0
- package/dist/index.d.ts +127 -40
- package/dist/index.js +1 -6176
- package/dist/index.js.map +1 -1
- package/dist/presence/index.d.ts +3 -3
- package/dist/presence/index.js +1 -37
- package/dist/presence/index.js.map +1 -1
- package/dist/{registry-ClCPnBac.d.ts → registry-Vc6r10MJ.d.ts} +24 -7
- package/dist/svg/index.d.ts +2 -2
- package/dist/svg/index.js +2 -128
- package/dist/svg/index.js.map +1 -1
- package/dist/swimlane/index.d.ts +4 -4
- package/dist/swimlane/index.js +1 -2
- package/dist/{types-D3aldmsO.d.ts → types-BPtqcAVr.d.ts} +52 -1
- package/dist/{use-node-lock-CbS59CRE.d.ts → use-node-lock-BFQQAr8M.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-6XRQSAQT.js +0 -148
- package/dist/chunk-6XRQSAQT.js.map +0 -1
- package/dist/chunk-QHMB4WYT.js +0 -148
- package/dist/chunk-QHMB4WYT.js.map +0 -1
- package/dist/chunk-TT2N7KWU.js +0 -676
- package/dist/chunk-TT2N7KWU.js.map +0 -1
- package/dist/chunk-VQYN27OK.js +0 -36
- package/dist/chunk-VQYN27OK.js.map +0 -1
package/dist/presence/index.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
3
|
-
import { a as NodeChange } from '../types-
|
|
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
|
/**
|
package/dist/presence/index.js
CHANGED
|
@@ -1,38 +1,2 @@
|
|
|
1
|
-
export
|
|
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":"
|
|
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-
|
|
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.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
|
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
|
};
|
package/dist/svg/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { CSSProperties } from 'react';
|
|
3
|
-
import { h as AnchorStrayPolicy } from '../registry-
|
|
4
|
-
import '../types-
|
|
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 {
|
|
2
|
-
|
|
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
|
package/dist/svg/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/svg/extract.ts","../../src/svg/parse.ts","../../src/svg/svg-artwork.tsx"],"names":[],"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
|
+
{"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"]}
|
package/dist/swimlane/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as Rect, e as RivetNode, V as Vec2, S as Size,
|
|
2
|
-
export { L as LaneChange,
|
|
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 };
|
package/dist/swimlane/index.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
export
|
|
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,
|
|
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-
|
|
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
|
/**
|