@kolosal-ai/rivet 0.2.0 → 0.4.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 +31 -4
- package/dist/anchor/index.js +22 -6
- package/dist/anchor/index.js.map +1 -1
- package/dist/chunk-TT2N7KWU.js +676 -0
- package/dist/chunk-TT2N7KWU.js.map +1 -0
- package/dist/{chunk-AXT35ZJV.js → chunk-W5HBJ4VF.js} +25 -3
- package/dist/chunk-W5HBJ4VF.js.map +1 -0
- package/dist/index.d.ts +159 -24
- package/dist/index.js +644 -149
- package/dist/index.js.map +1 -1
- package/dist/presence/index.d.ts +158 -4
- package/dist/presence/index.js +24 -2
- package/dist/presence/index.js.map +1 -1
- package/dist/{registry-t5ldbviG.d.ts → registry-Z_oiPyjm.d.ts} +43 -2
- package/dist/svg/index.d.ts +18 -3
- package/dist/svg/index.js +5 -2
- package/dist/svg/index.js.map +1 -1
- package/dist/swimlane/index.d.ts +2 -2
- package/dist/{types-CQqnm4yW.d.ts → types-D3aldmsO.d.ts} +27 -1
- package/dist/use-node-lock-CbS59CRE.d.ts +716 -0
- package/package.json +1 -1
- package/dist/chunk-AXT35ZJV.js.map +0 -1
- package/dist/chunk-RG5ALD3X.js +0 -306
- package/dist/chunk-RG5ALD3X.js.map +0 -1
- package/dist/use-node-lock-Csg9pUeC.d.ts +0 -323
package/dist/presence/index.d.ts
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import { P as PeerRecord } from '../use-node-lock-
|
|
2
|
-
export { C as CanInteractWithLocked, D as DEFAULT_PRESENCE_OPTIONS, L as LOCK_DEFAULT_REFUSED,
|
|
3
|
-
import '../types-
|
|
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';
|
|
4
4
|
import 'react';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Who else is in the document — ids, names, colours and `data`, and nothing
|
|
8
|
+
* about what anyone is doing.
|
|
9
|
+
*
|
|
10
|
+
* The same roster {@link usePeers} reports, on the narrower channel: this
|
|
11
|
+
* re-renders when someone joins, leaves, is renamed or recoloured, and stays
|
|
12
|
+
* silent while peers select and drag things. With a room of any size that's the
|
|
13
|
+
* difference between an avatar stack re-rendering on human events and
|
|
14
|
+
* re-rendering continuously, since at 15+ participants somebody is always
|
|
15
|
+
* clicking something.
|
|
16
|
+
*
|
|
17
|
+
* Reach for {@link usePeers} when the chrome actually shows claims (a "3 people
|
|
18
|
+
* on this node" badge); reach for this when it shows people.
|
|
19
|
+
*/
|
|
20
|
+
declare function usePeerIdentities(): readonly PeerIdentity[];
|
|
21
|
+
|
|
6
22
|
/**
|
|
7
23
|
* The peers currently in the document — for participant chrome you render
|
|
8
24
|
* yourself: an avatar stack, a follow button, a "3 others editing" line.
|
|
@@ -16,4 +32,142 @@ import 'react';
|
|
|
16
32
|
*/
|
|
17
33
|
declare function usePeers(): readonly PeerRecord[];
|
|
18
34
|
|
|
19
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Which local changes belong on the wire, and at which speed.
|
|
37
|
+
*
|
|
38
|
+
* Publishing a change batch to peers means answering three questions per
|
|
39
|
+
* change: is this the document (send it, keep it), is it a live gesture frame
|
|
40
|
+
* (send it, throw it away on the next one), or is it this client talking to
|
|
41
|
+
* itself (never send it). The answers are rivet semantics — they depend on
|
|
42
|
+
* which fields are gesture-bounded and which flags mark a frame as uncommitted
|
|
43
|
+
* — and every one of them fails *silently* when a consumer guesses wrong:
|
|
44
|
+
* nothing throws, the shared graph just misbehaves in ways that read like
|
|
45
|
+
* transport bugs.
|
|
46
|
+
*
|
|
47
|
+
* So the classification lives here. Transport policy does not: throttling the
|
|
48
|
+
* ephemeral half, batching or coalescing the durable half, and deciding what a
|
|
49
|
+
* frame even looks like on your wire are all yours.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A change that can carry an uncommitted gesture frame — the only two kinds a
|
|
54
|
+
* gesture produces mid-flight. Narrower than {@link NodeChange} so that the
|
|
55
|
+
* ephemeral half always has an `id` to key a transform by.
|
|
56
|
+
*/
|
|
57
|
+
type EphemeralNodeChange<TData = unknown> = Extract<NodeChange<TData>, {
|
|
58
|
+
type: "position" | "dimensions";
|
|
59
|
+
}>;
|
|
60
|
+
/** The two halves of a local batch, as returned by {@link splitLocalNodeChanges}. */
|
|
61
|
+
type LocalNodeChangeSplit<TData = unknown> = {
|
|
62
|
+
/**
|
|
63
|
+
* Document mutations. Send these, and expect peers to keep them: they're
|
|
64
|
+
* what a client that reloads has to see.
|
|
65
|
+
*/
|
|
66
|
+
durable: NodeChange<TData>[];
|
|
67
|
+
/**
|
|
68
|
+
* Uncommitted gesture frames — where this client currently has a node, not
|
|
69
|
+
* where it has agreed to put it. Send them as ephemeral state (rivet's own
|
|
70
|
+
* inbound door for this is `setPeerNodeTransform`, which paints without
|
|
71
|
+
* touching the graph); drop them freely under load, because the commit that
|
|
72
|
+
* closes the gesture arrives in {@link LocalNodeChangeSplit.durable}.
|
|
73
|
+
*
|
|
74
|
+
* One gesture frame can appear twice in a batch — a resize dragged from a
|
|
75
|
+
* top-left handle emits both a `dimensions` and a `position` change for the
|
|
76
|
+
* same node — so key by `id` rather than counting.
|
|
77
|
+
*/
|
|
78
|
+
ephemeral: EphemeralNodeChange<TData>[];
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Split a local `onNodesChange` batch into the half peers must keep and the
|
|
82
|
+
* half they only need until the next frame. Changes that must not travel at
|
|
83
|
+
* all are in neither array.
|
|
84
|
+
*
|
|
85
|
+
* | change | bucket | why |
|
|
86
|
+
* | --- | --- | --- |
|
|
87
|
+
* | `add` / `remove` / `replace` | durable | the document |
|
|
88
|
+
* | `position`, `dragging` unset or `false` | durable | the commit that closes a move |
|
|
89
|
+
* | `position` with `dragging: true` | ephemeral | a live drag frame |
|
|
90
|
+
* | `dimensions` with `resizing: false` | durable | the commit that closes a resize |
|
|
91
|
+
* | `dimensions` with `resizing: true` | ephemeral | a live resize frame |
|
|
92
|
+
* | `dimensions` with no `resizing` flag | **dropped** | a measurement |
|
|
93
|
+
* | `select` | **dropped** | selection is presence |
|
|
94
|
+
*
|
|
95
|
+
* The two drops are the ones worth having in the library. A `select` on the
|
|
96
|
+
* wire makes every client select in unison — selection is a fact about a
|
|
97
|
+
* person, and it travels through `onLocalPresence` instead. And an unflagged
|
|
98
|
+
* `dimensions` is this client reporting its own measured DOM box, which every
|
|
99
|
+
* client produces for itself; publishing yours makes peers fight each other's
|
|
100
|
+
* layout. That distinction — a measurement echo versus a real resize — is
|
|
101
|
+
* invisible from outside without reading the store.
|
|
102
|
+
*
|
|
103
|
+
* Only call this on batches whose `ChangeMeta.origin` is `"local"`; a batch
|
|
104
|
+
* re-emitted after `applyRemote` is a peer's edit coming back, and sending it
|
|
105
|
+
* on is the echo loop.
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* onNodesChange={(changes, meta) => {
|
|
109
|
+
* setNodes((current) => applyNodeChanges(changes, current))
|
|
110
|
+
* if (meta.origin !== "local") return
|
|
111
|
+
* const { durable, ephemeral } = splitLocalNodeChanges(changes)
|
|
112
|
+
* if (durable.length > 0) channel.send({ kind: "nodes", changes: durable })
|
|
113
|
+
* for (const change of ephemeral) {
|
|
114
|
+
* channel.send({ kind: "transform", id: change.id, rect: rivet.getNodeRect(change.id) })
|
|
115
|
+
* }
|
|
116
|
+
* }}
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* Edge changes need no equivalent: they carry no gesture-bounded fields, so
|
|
120
|
+
* dropping `select` is the whole rule.
|
|
121
|
+
*/
|
|
122
|
+
declare function splitLocalNodeChanges<TData = unknown>(changes: readonly NodeChange<TData>[]): LocalNodeChangeSplit<TData>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Claims, merged per target — what a {@link PresenceOptions.outlineComponent}
|
|
126
|
+
* mounts a wrapper for.
|
|
127
|
+
*
|
|
128
|
+
* The canvas paint answers "whose is this?" with one stroke and moves on: it
|
|
129
|
+
* walks peers and locks in a fixed order, skips whatever it has already
|
|
130
|
+
* outlined, and never builds a list. A component can say more than a stroke,
|
|
131
|
+
* so this collects the same facts the other way round — keyed by target, with
|
|
132
|
+
* every claimant on it — and leaves the editorial decisions (stack, badge,
|
|
133
|
+
* truncate) to the consumer.
|
|
134
|
+
*
|
|
135
|
+
* Pure and allocation-free on the empty case, because it runs on the frame
|
|
136
|
+
* channel: claims arrive on a React-safe channel, but a peer's live gesture
|
|
137
|
+
* doesn't (transform frames never notify), and their frames can beat their
|
|
138
|
+
* roster entry. Deriving the set per frame is the only way to be right about
|
|
139
|
+
* both, and it's the same union the renderer takes.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/** One claimed target: what it is, who claims it, and whether it's locked. */
|
|
143
|
+
type PeerOutlineEntry = {
|
|
144
|
+
/** The wire key — stable across frames, so it keys the wrapper element. */
|
|
145
|
+
key: PresenceTargetKey;
|
|
146
|
+
target: PresenceTarget;
|
|
147
|
+
claims: readonly PeerClaim[];
|
|
148
|
+
lock: {
|
|
149
|
+
holderId: string;
|
|
150
|
+
peer?: PeerIdentity;
|
|
151
|
+
} | null;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Every target somebody else has a claim on, in the order a reader would rank
|
|
155
|
+
* them: each peer's held targets before their selected ones, peers in roster
|
|
156
|
+
* order, locks last.
|
|
157
|
+
*
|
|
158
|
+
* `identities` is the registry's cached identity snapshot rather than the
|
|
159
|
+
* records themselves — it keeps its object identity across claim changes, so a
|
|
160
|
+
* component that re-renders when claims move still sees the same `peer` prop it
|
|
161
|
+
* saw last time. A holder no identity covers (a lock outliving its tab) is
|
|
162
|
+
* synthesized from the id, which is where its colour comes from anyway.
|
|
163
|
+
*/
|
|
164
|
+
declare function collectPeerOutlines(peers: readonly PeerRecord[], identities: readonly PeerIdentity[], locks: ReadonlyMap<PresenceTargetKey, string>): readonly PeerOutlineEntry[];
|
|
165
|
+
/**
|
|
166
|
+
* Whether two collections say the same thing — the test that keeps a frame from
|
|
167
|
+
* re-rendering React. Compares what a component is handed (which targets, who
|
|
168
|
+
* claims them and how, who holds the lock) and nothing else: geometry moves
|
|
169
|
+
* every frame and is written to the wrapper element instead.
|
|
170
|
+
*/
|
|
171
|
+
declare function peerOutlinesEqual(a: readonly PeerOutlineEntry[], b: readonly PeerOutlineEntry[]): boolean;
|
|
172
|
+
|
|
173
|
+
export { type EphemeralNodeChange, type LocalNodeChangeSplit, PeerClaim, PeerIdentity, type PeerOutlineEntry, PeerRecord, PresenceTarget, PresenceTargetKey, collectPeerOutlines, peerOutlinesEqual, splitLocalNodeChanges, usePeerIdentities, usePeers };
|
package/dist/presence/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DEFAULT_PRESENCE_OPTIONS, LOCK_DEFAULT_REFUSED, PEER_COLORS, peerColor, useNodeLock, useNodeLockAllows } from '../chunk-
|
|
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
2
|
import { useRivetContext } from '../chunk-VQYN27OK.js';
|
|
3
3
|
import { useSyncExternalStore } from 'react';
|
|
4
4
|
|
|
@@ -11,6 +11,28 @@ function usePeers() {
|
|
|
11
11
|
);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
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 };
|
|
15
37
|
//# sourceMappingURL=index.js.map
|
|
16
38
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/use-peers.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","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"]}
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import { H as HandlePosition, N as NodeId, C as Connection } from './types-
|
|
1
|
+
import { H as HandlePosition, N as NodeId, R as Rect, C as Connection } from './types-D3aldmsO.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Anchor — a connectable point bound to a DOM element rendered inside a node.
|
|
@@ -62,6 +62,14 @@ type AnchorRecord = {
|
|
|
62
62
|
color?: string;
|
|
63
63
|
/** Stray chrome policy. Absent = `"dynamic"`. */
|
|
64
64
|
strays?: AnchorStrayPolicy;
|
|
65
|
+
/** Whether a press on the element selects the anchor. Absent = `false`. */
|
|
66
|
+
selectable?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Whatever the consumer attached at registration — a shape's label, the
|
|
69
|
+
* bounds it was imported at, the layer it came from. Rivet stores and returns
|
|
70
|
+
* it, and never reads it.
|
|
71
|
+
*/
|
|
72
|
+
data?: unknown;
|
|
65
73
|
element: Element | null;
|
|
66
74
|
/** Null until the first successful measurement. */
|
|
67
75
|
geometry: AnchorGeometry | null;
|
|
@@ -70,7 +78,27 @@ type AnchorRegistrationOptions = {
|
|
|
70
78
|
color?: string;
|
|
71
79
|
/** Stray-handle chrome policy for this anchor. Default `"dynamic"`. */
|
|
72
80
|
strays?: AnchorStrayPolicy;
|
|
81
|
+
/**
|
|
82
|
+
* Make the element pointer-interactive, so a click on it selects the anchor.
|
|
83
|
+
* Default `false`: an anchor around inline text usually sits in editable
|
|
84
|
+
* content, where swallowing a press would stop the caret from landing. Shapes
|
|
85
|
+
* are the opposite case — the element *is* the thing you mean to click.
|
|
86
|
+
*/
|
|
87
|
+
selectable?: boolean;
|
|
88
|
+
/** Opaque consumer payload, returned on the record untouched. */
|
|
89
|
+
data?: unknown;
|
|
90
|
+
};
|
|
91
|
+
/** Identifies one anchor: which node holds it, and which anchor on that node. */
|
|
92
|
+
type AnchorRef = {
|
|
93
|
+
nodeId: NodeId;
|
|
94
|
+
anchorId: string;
|
|
73
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* The selectable anchor a press landed in, or `null` for a press anywhere else
|
|
98
|
+
* on the node. Innermost wins: anchors can nest (a shape inside a group that is
|
|
99
|
+
* itself an anchor), and the deepest one is the one under the pointer.
|
|
100
|
+
*/
|
|
101
|
+
declare function findSelectableAnchor(records: readonly AnchorRecord[], target: Element): AnchorRecord | null;
|
|
74
102
|
/**
|
|
75
103
|
* Imperative registration handle for headless consumers (rich-text editors and
|
|
76
104
|
* other renderers that own their DOM). Returned by `RivetInstance.registerAnchor`.
|
|
@@ -140,6 +168,19 @@ type AnchorRectLike = {
|
|
|
140
168
|
* client line rects, and its bounding box. Pure — callers supply the rects.
|
|
141
169
|
*/
|
|
142
170
|
declare function computeAnchorGeometry(root: AnchorRectLike, first: AnchorRectLike, last: AnchorRectLike, box: AnchorRectLike): AnchorGeometry;
|
|
171
|
+
/**
|
|
172
|
+
* The box an anchor's element occupies inside `node`, in the same space that
|
|
173
|
+
* rect is given in — how chrome that annotates a whole anchor (a peer's claim
|
|
174
|
+
* on it, say) gets a rect out of measurements taken as percentages.
|
|
175
|
+
*
|
|
176
|
+
* Derived from the grab dots, which sit on the element's edges: left and top on
|
|
177
|
+
* its first line box, right and bottom on its last. For anything that fits on
|
|
178
|
+
* one line that *is* the element's box. A line-wrapped element instead reports
|
|
179
|
+
* the span from where it starts to where it ends — the union of its line boxes
|
|
180
|
+
* would be the node's whole text column, which describes the paragraph rather
|
|
181
|
+
* than the anchor.
|
|
182
|
+
*/
|
|
183
|
+
declare function anchorRect(node: Rect, geometry: AnchorGeometry): Rect;
|
|
143
184
|
/**
|
|
144
185
|
* Where an anchor's stray handle sits along `side`, as % of that side — the
|
|
145
186
|
* measured row (left/right) or column (top/bottom) nearest the anchor, nudged
|
|
@@ -148,4 +189,4 @@ declare function computeAnchorGeometry(root: AnchorRectLike, first: AnchorRectLi
|
|
|
148
189
|
*/
|
|
149
190
|
declare function strayPlacementPct(geometry: AnchorGeometry, side: HandlePosition, options: ResolvedAnchorOptions): number;
|
|
150
191
|
|
|
151
|
-
export {
|
|
192
|
+
export { type AnchorRef as A, DEFAULT_ANCHOR_OPTIONS as D, type ResolvedAnchorOptions as R, ANCHOR_SIDES as a, type AnchorGeometry as b, type AnchorOptions as c, type AnchorPoint as d, type AnchorRecord as e, type AnchorRegistration as f, type AnchorRegistrationOptions as g, type AnchorStrayPolicy as h, anchorAutoHandleId as i, anchorDotHandleId as j, anchorHandleId as k, anchorRect as l, computeAnchorGeometry as m, findSelectableAnchor as n, normalizeAnchorConnection as o, parseAnchorAutoHandleId as p, parseAnchorDotHandleId as q, parseAnchorHandleId as r, strayPlacementPct as s };
|
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 {
|
|
4
|
-
import '../types-
|
|
3
|
+
import { h as AnchorStrayPolicy } from '../registry-Z_oiPyjm.js';
|
|
4
|
+
import '../types-D3aldmsO.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The extract seam behind `<SvgArtwork>` — which elements inside a parsed
|
|
@@ -22,6 +22,13 @@ type SvgAnchorTarget = {
|
|
|
22
22
|
element: Element;
|
|
23
23
|
/** Accent color for the anchor's grab dots. */
|
|
24
24
|
color?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Let a click on this element select the anchor. Falls back to the
|
|
27
|
+
* `selectableAnchors` prop on `<SvgArtwork>` when the extractor leaves it out.
|
|
28
|
+
*/
|
|
29
|
+
selectable?: boolean;
|
|
30
|
+
/** Opaque payload kept on the anchor record — a label, the bounds it came in at. */
|
|
31
|
+
data?: unknown;
|
|
25
32
|
};
|
|
26
33
|
/**
|
|
27
34
|
* The extract seam's contract: the mounted, sanitized `<svg>` root in, the
|
|
@@ -100,6 +107,14 @@ type SvgArtworkProps = {
|
|
|
100
107
|
* `"dynamic"` for the inline-text-style perimeter chrome.
|
|
101
108
|
*/
|
|
102
109
|
anchorStrays?: AnchorStrayPolicy;
|
|
110
|
+
/**
|
|
111
|
+
* Make every extracted anchor clickable, so a press on a shape selects it.
|
|
112
|
+
* Defaults to `false`. An extractor that sets `selectable` on a target wins,
|
|
113
|
+
* so per-element control stays available. Shapes are the case this exists
|
|
114
|
+
* for: the element under the pointer *is* the thing being pointed at, and
|
|
115
|
+
* hit-testing it in the browser follows the path rather than its box.
|
|
116
|
+
*/
|
|
117
|
+
selectableAnchors?: boolean;
|
|
103
118
|
className?: string;
|
|
104
119
|
style?: CSSProperties;
|
|
105
120
|
};
|
|
@@ -122,6 +137,6 @@ type SvgArtworkProps = {
|
|
|
122
137
|
* wrapper — the whole graphic stays draggable. Nothing renders when `parse`
|
|
123
138
|
* refuses the markup.
|
|
124
139
|
*/
|
|
125
|
-
declare function SvgArtwork({ markup, parse, extract, anchorStrays, className, style, }: SvgArtworkProps): react.JSX.Element;
|
|
140
|
+
declare function SvgArtwork({ markup, parse, extract, anchorStrays, selectableAnchors, className, style, }: SvgArtworkProps): react.JSX.Element;
|
|
126
141
|
|
|
127
142
|
export { type SvgAnchorExtractor, type SvgAnchorTarget, SvgArtwork, type SvgArtworkProps, type SvgIntrinsicSize, type SvgParser, extractAnchors, parseSvg, readIntrinsicSize };
|
package/dist/svg/index.js
CHANGED
|
@@ -62,6 +62,7 @@ function SvgArtwork({
|
|
|
62
62
|
parse,
|
|
63
63
|
extract,
|
|
64
64
|
anchorStrays,
|
|
65
|
+
selectableAnchors,
|
|
65
66
|
className,
|
|
66
67
|
style
|
|
67
68
|
}) {
|
|
@@ -88,7 +89,9 @@ function SvgArtwork({
|
|
|
88
89
|
next.add(target.anchorId);
|
|
89
90
|
store.registerAnchor(nodeId, target.anchorId, target.element, {
|
|
90
91
|
color: target.color,
|
|
91
|
-
strays: anchorStrays ?? "none"
|
|
92
|
+
strays: anchorStrays ?? "none",
|
|
93
|
+
selectable: target.selectable ?? selectableAnchors,
|
|
94
|
+
data: target.data
|
|
92
95
|
});
|
|
93
96
|
}
|
|
94
97
|
for (const id of wired.current) {
|
|
@@ -98,7 +101,7 @@ function SvgArtwork({
|
|
|
98
101
|
return () => {
|
|
99
102
|
for (const id of wired.current) store.detachAnchor(nodeId, id);
|
|
100
103
|
};
|
|
101
|
-
}, [markup, parse, extract, anchorStrays, store, nodeId]);
|
|
104
|
+
}, [markup, parse, extract, anchorStrays, selectableAnchors, store, nodeId]);
|
|
102
105
|
return /* @__PURE__ */ jsx(
|
|
103
106
|
"div",
|
|
104
107
|
{
|
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\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 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 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 })\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, 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":[],"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,
|
|
2
|
-
export { L as LaneChange, U as Swimlane, t as SwimlaneHeaderProps, u as SwimlaneLabelProps, v as SwimlaneSizeChange } from '../types-
|
|
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';
|
|
3
3
|
import 'react';
|
|
4
4
|
|
|
5
5
|
/** World height of a group's sticky header band. */
|
|
@@ -374,6 +374,21 @@ type EdgeDrawExtras = {
|
|
|
374
374
|
* unmeasured (the end falls back to the side's midpoint).
|
|
375
375
|
*/
|
|
376
376
|
anchorPlacement?: (nodeId: NodeId, anchorId: string, side: HandlePosition) => number | null;
|
|
377
|
+
/**
|
|
378
|
+
* Edges another client has a claim on, `edgeId -> colour`. Drawn as a halo
|
|
379
|
+
* under the edge's own stroke, which is why it arrives as a colour rather
|
|
380
|
+
* than as a claim: an edge has one stroke, and spending it on somebody else's
|
|
381
|
+
* selection would take the local hover and selection styling with it.
|
|
382
|
+
*
|
|
383
|
+
* Selections, live gestures and locks all flatten into this one map. Peer
|
|
384
|
+
* outlines make the same trade on nodes for the same reason — a second visual
|
|
385
|
+
* vocabulary asks the reader to learn the difference between advisory and
|
|
386
|
+
* enforced, which is not a difference they can act on.
|
|
387
|
+
*
|
|
388
|
+
* Optional, so third-party {@link EdgeRenderer} implementations that never
|
|
389
|
+
* read it keep compiling and simply don't paint claims.
|
|
390
|
+
*/
|
|
391
|
+
peerEdges?: ReadonlyMap<EdgeId, string>;
|
|
377
392
|
};
|
|
378
393
|
/**
|
|
379
394
|
* The seam that keeps graph and interaction code independent of how edges are
|
|
@@ -384,6 +399,17 @@ interface EdgeRenderer {
|
|
|
384
399
|
draw(edges: RivetEdge[], nodes: Map<NodeId, RivetNode>, viewport: Viewport, extras?: EdgeDrawExtras): void;
|
|
385
400
|
/** Screen-space label anchor per edge that has a label, from the last {@link draw}. */
|
|
386
401
|
getLabels(): Map<EdgeId, Vec2>;
|
|
402
|
+
/**
|
|
403
|
+
* Screen-space anchor point per edge named in {@link EdgeDrawExtras.peerEdges}
|
|
404
|
+
* last {@link draw} — a point on the drawn path, for DOM chrome that sits on
|
|
405
|
+
* an edge somebody has claimed (`presenceOptions.outlineComponent`).
|
|
406
|
+
*
|
|
407
|
+
* Its own map rather than {@link getLabels}, which is only ever asked about
|
|
408
|
+
* labelled edges and would have to grow an entry per edge per frame to answer
|
|
409
|
+
* this. Optional: a backend without it simply gives claimed edges no DOM
|
|
410
|
+
* chrome, and the halo it paints under them is unaffected.
|
|
411
|
+
*/
|
|
412
|
+
getPeerEdgeAnchors?(): ReadonlyMap<EdgeId, Vec2>;
|
|
387
413
|
/** Return the edge under a screen-space point, or null. `tolerance` overrides
|
|
388
414
|
* the backend's default pick distance (used for hover hysteresis). */
|
|
389
415
|
pick(x: number, y: number, tolerance?: number): EdgeId | null;
|
|
@@ -566,4 +592,4 @@ type RivetControls = {
|
|
|
566
592
|
setViewport: (viewport: Viewport) => void;
|
|
567
593
|
};
|
|
568
594
|
|
|
569
|
-
export type { AlignmentGuide as A, EdgeDrawExtras as B, Connection as C, DefaultEdgeOptions as D,
|
|
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 };
|