@kolosal-ai/rivet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/anchor/index.d.ts +32 -0
- package/dist/anchor/index.js +21 -0
- package/dist/anchor/index.js.map +1 -0
- package/dist/chunk-6XRQSAQT.js +148 -0
- package/dist/chunk-6XRQSAQT.js.map +1 -0
- package/dist/chunk-AXT35ZJV.js +98 -0
- package/dist/chunk-AXT35ZJV.js.map +1 -0
- package/dist/chunk-VQYN27OK.js +36 -0
- package/dist/chunk-VQYN27OK.js.map +1 -0
- package/dist/index.d.ts +1012 -0
- package/dist/index.js +4849 -0
- package/dist/index.js.map +1 -0
- package/dist/registry-Dkk4ZKt-.d.ts +151 -0
- package/dist/svg/index.d.ts +127 -0
- package/dist/svg/index.js +126 -0
- package/dist/svg/index.js.map +1 -0
- package/dist/swimlane/index.d.ts +96 -0
- package/dist/swimlane/index.js +3 -0
- package/dist/swimlane/index.js.map +1 -0
- package/dist/types-B8AAJ60T.d.ts +515 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kolosal AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @kolosal-ai/rivet
|
|
2
|
+
|
|
3
|
+
A from-scratch, canvas-rendered node graph for React. Nodes are real DOM/React
|
|
4
|
+
components; edges, the grid, and swimlane bands render to Canvas2D, so React
|
|
5
|
+
never re-renders during pan, zoom, drag, or connect.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @kolosal-ai/rivet
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { Rivet, useRivet } from "@kolosal-ai/rivet"
|
|
13
|
+
|
|
14
|
+
const nodes = [
|
|
15
|
+
{ id: "a", position: { x: 0, y: 0 }, data: { label: "Start" } },
|
|
16
|
+
{ id: "b", position: { x: 260, y: 120 }, data: { label: "End" } },
|
|
17
|
+
]
|
|
18
|
+
const edges = [{ id: "e1", source: "a", target: "b" }]
|
|
19
|
+
|
|
20
|
+
export function App() {
|
|
21
|
+
return <Rivet defaultNodes={nodes} defaultEdges={edges} />
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Highlights
|
|
26
|
+
|
|
27
|
+
- **DOM nodes, canvas edges** — keep interactive React components as nodes while
|
|
28
|
+
pan/zoom/drag stay off the React render path.
|
|
29
|
+
- **Controlled or uncontrolled** — mirror the graph in your own state via change
|
|
30
|
+
batches (`onNodesChange`/`onEdgesChange` + `applyNodeChanges`), or let the store own it.
|
|
31
|
+
- **Built-in history** — microtask-coalesced undo/redo over every mutation made
|
|
32
|
+
through the library.
|
|
33
|
+
- **Swimlanes** — lane bands with clamping, drag-driven lane reassignment, resize
|
|
34
|
+
and auto-fit (`@kolosal-ai/rivet/swimlane`).
|
|
35
|
+
- **Anchors** — library-managed connection points on arbitrary elements inside a
|
|
36
|
+
node, measured live (`@kolosal-ai/rivet/anchor`).
|
|
37
|
+
- **SVG artwork nodes** — parse and render SVG inside nodes, with anchors on
|
|
38
|
+
elements inside the artwork (`@kolosal-ai/rivet/svg`).
|
|
39
|
+
- **Pluggable edge renderer** — swap the Canvas2D backend through the
|
|
40
|
+
`renderer` prop (`EdgeRendererFactory`).
|
|
41
|
+
- **Keyboard + ARIA** — roving node focus, grab-to-move, keyboard connect, live
|
|
42
|
+
announcements.
|
|
43
|
+
|
|
44
|
+
## Documentation
|
|
45
|
+
|
|
46
|
+
Guides, concepts, and the full API reference live in the
|
|
47
|
+
[repository docs](https://github.com/KolosalAI/rivet). React 18.3+ or 19 required.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
export { A as ANCHOR_SIDES, a as AnchorGeometry, b as AnchorOptions, c as AnchorPoint, d as AnchorRecord, e as AnchorRegistration, f as AnchorRegistrationOptions, g as AnchorStrayPolicy, D as DEFAULT_ANCHOR_OPTIONS, R as ResolvedAnchorOptions, h as anchorAutoHandleId, i as anchorDotHandleId, j as anchorHandleId, k as computeAnchorGeometry, n as normalizeAnchorConnection, p as parseAnchorAutoHandleId, l as parseAnchorDotHandleId, m as parseAnchorHandleId, s as strayPlacementPct } from '../registry-Dkk4ZKt-.js';
|
|
3
|
+
import '../types-B8AAJ60T.js';
|
|
4
|
+
|
|
5
|
+
type AnchorProps = {
|
|
6
|
+
/**
|
|
7
|
+
* Identifies the anchor within its node. Persisted edge handle ids derive
|
|
8
|
+
* from it, so keep it stable across renders and sessions.
|
|
9
|
+
*/
|
|
10
|
+
id: string;
|
|
11
|
+
/** Accent color for the anchor's grab dots and stray handles. */
|
|
12
|
+
color?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Declarative anchor registration — wraps any inline content rendered inside a
|
|
19
|
+
* node and binds it as a connectable point. Sugar over the imperative registry
|
|
20
|
+
* (`RivetInstance.registerAnchor`) for consumers whose React tree owns the
|
|
21
|
+
* DOM; the wrapper is a plain unstyled `<span>`, so it doesn't affect inline
|
|
22
|
+
* layout. Rivet measures the span's line boxes and renders grab dots and stray
|
|
23
|
+
* perimeter handles as overlay chrome — nothing is injected here.
|
|
24
|
+
*
|
|
25
|
+
* Unmounting **detaches** the anchor (stale retention: geometry and edges stay
|
|
26
|
+
* put, for display/editor swaps) rather than unregistering it. Removal is an
|
|
27
|
+
* explicit consumer action via `RivetInstance.unregisterAnchor` — e.g. when
|
|
28
|
+
* the content the anchor referred to is edited away for good.
|
|
29
|
+
*/
|
|
30
|
+
declare function Anchor({ id, color, className, style, children }: AnchorProps): React.ReactElement;
|
|
31
|
+
|
|
32
|
+
export { Anchor, type AnchorProps };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { ANCHOR_SIDES, DEFAULT_ANCHOR_OPTIONS, anchorAutoHandleId, anchorDotHandleId, anchorHandleId, computeAnchorGeometry, normalizeAnchorConnection, parseAnchorAutoHandleId, parseAnchorDotHandleId, parseAnchorHandleId, strayPlacementPct } from '../chunk-AXT35ZJV.js';
|
|
2
|
+
import { useRivetContext, useRivetNodeContext } from '../chunk-VQYN27OK.js';
|
|
3
|
+
import { useRef, useLayoutEffect } from 'react';
|
|
4
|
+
import { jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
function Anchor({ id, color, className, style, children }) {
|
|
7
|
+
const { store } = useRivetContext();
|
|
8
|
+
const { nodeId } = useRivetNodeContext();
|
|
9
|
+
const ref = useRef(null);
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
const el = ref.current;
|
|
12
|
+
if (!el) return;
|
|
13
|
+
store.registerAnchor(nodeId, id, el, color === void 0 ? void 0 : { color });
|
|
14
|
+
return () => store.detachAnchor(nodeId, id);
|
|
15
|
+
}, [store, nodeId, id, color]);
|
|
16
|
+
return /* @__PURE__ */ jsx("span", { ref, "data-rivet-anchor": id, className, style, children });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { Anchor };
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/anchor.tsx"],"names":[],"mappings":";;;;;AA6BO,SAAS,OAAO,EAAE,EAAA,EAAI,OAAO,SAAA,EAAW,KAAA,EAAO,UAAS,EAAoC;AACjG,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,eAAA,EAAgB;AAClC,EAAA,MAAM,EAAE,MAAA,EAAO,GAAI,mBAAA,EAAoB;AACvC,EAAA,MAAM,GAAA,GAAM,OAAwB,IAAI,CAAA;AAExC,EAAA,eAAA,CAAgB,MAAM;AACpB,IAAA,MAAM,KAAK,GAAA,CAAI,OAAA;AACf,IAAA,IAAI,CAAC,EAAA,EAAI;AACT,IAAA,KAAA,CAAM,cAAA,CAAe,QAAQ,EAAA,EAAI,EAAA,EAAI,UAAU,MAAA,GAAY,MAAA,GAAY,EAAE,KAAA,EAAO,CAAA;AAChF,IAAA,OAAO,MAAM,KAAA,CAAM,YAAA,CAAa,MAAA,EAAQ,EAAE,CAAA;AAAA,EAC5C,GAAG,CAAC,KAAA,EAAO,MAAA,EAAQ,EAAA,EAAI,KAAK,CAAC,CAAA;AAE7B,EAAA,2BACG,MAAA,EAAA,EAAK,GAAA,EAAU,qBAAmB,EAAA,EAAI,SAAA,EAAsB,OAC1D,QAAA,EACH,CAAA;AAEJ","file":"index.js","sourcesContent":["import { type CSSProperties, type ReactNode, useLayoutEffect, useRef } from \"react\"\nimport { useRivetContext, useRivetNodeContext } from \"../context\"\n\nexport type AnchorProps = {\n /**\n * Identifies the anchor within its node. Persisted edge handle ids derive\n * from it, so keep it stable across renders and sessions.\n */\n id: string\n /** Accent color for the anchor's grab dots and stray handles. */\n color?: string\n className?: string\n style?: CSSProperties\n children: ReactNode\n}\n\n/**\n * Declarative anchor registration — wraps any inline content rendered inside a\n * node and binds it as a connectable point. Sugar over the imperative registry\n * (`RivetInstance.registerAnchor`) for consumers whose React tree owns the\n * DOM; the wrapper is a plain unstyled `<span>`, so it doesn't affect inline\n * layout. Rivet measures the span's line boxes and renders grab dots and stray\n * perimeter handles as overlay chrome — nothing is injected here.\n *\n * Unmounting **detaches** the anchor (stale retention: geometry and edges stay\n * put, for display/editor swaps) rather than unregistering it. Removal is an\n * explicit consumer action via `RivetInstance.unregisterAnchor` — e.g. when\n * the content the anchor referred to is edited away for good.\n */\nexport function Anchor({ id, color, className, style, children }: AnchorProps): React.ReactElement {\n const { store } = useRivetContext()\n const { nodeId } = useRivetNodeContext()\n const ref = useRef<HTMLSpanElement>(null)\n\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n store.registerAnchor(nodeId, id, el, color === undefined ? undefined : { color })\n return () => store.detachAnchor(nodeId, id)\n }, [store, nodeId, id, color])\n\n return (\n <span ref={ref} data-rivet-anchor={id} className={className} style={style}>\n {children}\n </span>\n )\n}\n"]}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// ../utils/src/math.ts
|
|
2
|
+
function clamp(value, min, max) {
|
|
3
|
+
return Math.min(Math.max(value, min), max);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/swimlane/lanes.ts
|
|
7
|
+
var SWIMLANE_GROUP_HEADER_SIZE = 32;
|
|
8
|
+
var SWIMLANE_LABEL_WIDTH = 24;
|
|
9
|
+
var SWIMLANE_MIN_HEIGHT = 80;
|
|
10
|
+
function resolveMargin(margin) {
|
|
11
|
+
if (typeof margin === "number") {
|
|
12
|
+
return { top: margin, right: margin, bottom: margin, left: margin };
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
top: margin?.top ?? 16,
|
|
16
|
+
right: margin?.right ?? 16,
|
|
17
|
+
bottom: margin?.bottom ?? 16,
|
|
18
|
+
left: margin?.left ?? 16
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
var ORIGIN = { x: 0, y: 0 };
|
|
22
|
+
function withAlpha(color, alpha) {
|
|
23
|
+
const hex = color.trim();
|
|
24
|
+
if (!hex.startsWith("#")) return color;
|
|
25
|
+
const h = hex.slice(1);
|
|
26
|
+
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
|
27
|
+
if (full.length !== 6) return color;
|
|
28
|
+
const n = Number.parseInt(full, 16);
|
|
29
|
+
if (Number.isNaN(n)) return color;
|
|
30
|
+
return `rgba(${n >> 16 & 255}, ${n >> 8 & 255}, ${n & 255}, ${alpha})`;
|
|
31
|
+
}
|
|
32
|
+
function resolveSwimlaneGroup(group, headerSize = SWIMLANE_GROUP_HEADER_SIZE) {
|
|
33
|
+
const pos = group.position ?? ORIGIN;
|
|
34
|
+
const lanes = [];
|
|
35
|
+
let offset = pos.y + headerSize;
|
|
36
|
+
for (const lane of group.lanes) {
|
|
37
|
+
lanes.push({
|
|
38
|
+
id: lane.id,
|
|
39
|
+
groupId: group.id,
|
|
40
|
+
label: lane.label,
|
|
41
|
+
// Only an explicit per-lane color tints a lane; the group's accent color
|
|
42
|
+
// is header-only, never a lane background wash.
|
|
43
|
+
color: lane.color,
|
|
44
|
+
top: offset,
|
|
45
|
+
height: lane.size,
|
|
46
|
+
x: pos.x,
|
|
47
|
+
width: group.length
|
|
48
|
+
});
|
|
49
|
+
offset += lane.size;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
id: group.id,
|
|
53
|
+
label: group.label,
|
|
54
|
+
color: group.color,
|
|
55
|
+
x: pos.x,
|
|
56
|
+
width: group.length,
|
|
57
|
+
top: pos.y,
|
|
58
|
+
headerSize,
|
|
59
|
+
bottom: offset,
|
|
60
|
+
lanes
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function resolveSwimlanes(groups, headerSize = SWIMLANE_GROUP_HEADER_SIZE) {
|
|
64
|
+
return groups.map((group) => resolveSwimlaneGroup(group, headerSize));
|
|
65
|
+
}
|
|
66
|
+
function flattenLanes(groups) {
|
|
67
|
+
return groups.flatMap((group) => group.lanes);
|
|
68
|
+
}
|
|
69
|
+
function findLaneByCoverage(lanes, top, height, preferredLaneId) {
|
|
70
|
+
if (lanes.length === 0) return null;
|
|
71
|
+
const bottom = top + height;
|
|
72
|
+
let best = null;
|
|
73
|
+
let bestOverlap = -1;
|
|
74
|
+
for (const lane of lanes) {
|
|
75
|
+
const overlap = Math.max(0, Math.min(bottom, lane.top + lane.height) - Math.max(top, lane.top));
|
|
76
|
+
if (overlap > bestOverlap || overlap === bestOverlap && lane.id === preferredLaneId) {
|
|
77
|
+
bestOverlap = overlap;
|
|
78
|
+
best = lane;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (bestOverlap > 0) return best;
|
|
82
|
+
const center = top + height / 2;
|
|
83
|
+
let nearest = null;
|
|
84
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
85
|
+
for (const lane of lanes) {
|
|
86
|
+
const dist = Math.abs(lane.top + lane.height / 2 - center);
|
|
87
|
+
if (dist < bestDist) {
|
|
88
|
+
bestDist = dist;
|
|
89
|
+
nearest = lane;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return nearest;
|
|
93
|
+
}
|
|
94
|
+
function requiredLaneHeight(lane, nodes, marginBottom) {
|
|
95
|
+
let min = SWIMLANE_MIN_HEIGHT;
|
|
96
|
+
for (const node of nodes) {
|
|
97
|
+
if (node.laneId !== lane.id || !node.size) continue;
|
|
98
|
+
const need = node.position.y + node.size.height + marginBottom - lane.top;
|
|
99
|
+
if (need > min) min = need;
|
|
100
|
+
}
|
|
101
|
+
return min;
|
|
102
|
+
}
|
|
103
|
+
function swimlaneChromeAt(groups, viewport, point, headerHeight, labelWidth) {
|
|
104
|
+
const { x: vx, y: vy, zoom } = viewport;
|
|
105
|
+
const headerBottomByGroup = /* @__PURE__ */ new Map();
|
|
106
|
+
for (const group of groups) {
|
|
107
|
+
const screenTop = group.top * zoom + vy;
|
|
108
|
+
const screenBottom = group.bottom * zoom + vy;
|
|
109
|
+
const stickyY = Math.max(0, Math.min(screenTop, screenBottom - headerHeight));
|
|
110
|
+
headerBottomByGroup.set(group.id, stickyY + headerHeight);
|
|
111
|
+
if (screenBottom > 0 && point.y >= stickyY && point.y <= stickyY + headerHeight) {
|
|
112
|
+
return { type: "group", id: group.id };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const group of groups) {
|
|
116
|
+
const headerBottom = headerBottomByGroup.get(group.id) ?? 0;
|
|
117
|
+
for (const lane of group.lanes) {
|
|
118
|
+
const screenX = lane.x * zoom + vx;
|
|
119
|
+
const screenTop = lane.top * zoom + vy;
|
|
120
|
+
const screenBottom = screenTop + lane.height * zoom;
|
|
121
|
+
const screenEndX = screenX + lane.width * zoom;
|
|
122
|
+
const stickyX = Math.max(0, Math.min(screenX, screenEndX - labelWidth));
|
|
123
|
+
const top = Math.max(headerBottom, screenTop);
|
|
124
|
+
if (point.x >= stickyX && point.x <= stickyX + labelWidth && point.y >= top && point.y <= screenBottom) {
|
|
125
|
+
return { type: "lane", id: lane.id };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
function clampNodeToLanes(position, size, lanes, margin, labelWidth, preferredLaneId) {
|
|
132
|
+
const lane = findLaneByCoverage(lanes, position.y, size.height, preferredLaneId);
|
|
133
|
+
if (!lane) return { position, laneId: preferredLaneId ?? null };
|
|
134
|
+
const minY = lane.top + margin.top;
|
|
135
|
+
const maxY = Math.max(minY, lane.top + lane.height - size.height - margin.bottom);
|
|
136
|
+
const minX = lane.x + labelWidth + margin.left;
|
|
137
|
+
return {
|
|
138
|
+
position: {
|
|
139
|
+
x: Math.max(minX, position.x),
|
|
140
|
+
y: clamp(position.y, minY, maxY)
|
|
141
|
+
},
|
|
142
|
+
laneId: lane.id
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { SWIMLANE_GROUP_HEADER_SIZE, SWIMLANE_LABEL_WIDTH, SWIMLANE_MIN_HEIGHT, clamp, clampNodeToLanes, findLaneByCoverage, flattenLanes, requiredLaneHeight, resolveMargin, resolveSwimlaneGroup, resolveSwimlanes, swimlaneChromeAt, withAlpha };
|
|
147
|
+
//# sourceMappingURL=chunk-6XRQSAQT.js.map
|
|
148
|
+
//# sourceMappingURL=chunk-6XRQSAQT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../utils/src/math.ts","../src/swimlane/lanes.ts"],"names":[],"mappings":"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","file":"chunk-6XRQSAQT.js","sourcesContent":["/** Clamp `value` into the inclusive range `[min, max]`. */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\n/** Linear interpolation between `a` and `b` by `t` (unclamped). */\nexport function lerp(a: number, b: number, t: number): number {\n return a + (b - a) * t\n}\n\n/** Round `value` to `step`'s grid (used for snap-to-grid). */\nexport function snap(value: number, step: number): number {\n if (step <= 0) return value\n return Math.round(value / step) * step\n}\n","import { clamp } from \"@rivet/utils\"\nimport type { Rect, RivetNode, Size, SwimlaneGroup, SwimlaneMargin, Vec2, Viewport } from \"../types\"\n\n/** World height of a group's sticky header band. */\nexport const SWIMLANE_GROUP_HEADER_SIZE = 32\n/** Screen width of the sticky vertical lane-label strip, in px. */\nexport const SWIMLANE_LABEL_WIDTH = 24\n/** Floor for a lane's height when resized, in world units. */\nexport const SWIMLANE_MIN_HEIGHT = 80\n\n/** A margin with every side filled in. */\nexport type ResolvedMargin = { top: number; right: number; bottom: number; left: number }\n\n/** Normalize a `swimlaneMargin` prop (number shorthand or per-side) to all sides. */\nexport function resolveMargin(margin?: number | SwimlaneMargin): ResolvedMargin {\n if (typeof margin === \"number\") {\n return { top: margin, right: margin, bottom: margin, left: margin }\n }\n return {\n top: margin?.top ?? 16,\n right: margin?.right ?? 16,\n bottom: margin?.bottom ?? 16,\n left: margin?.left ?? 16,\n }\n}\n\n/** A lane resolved to world-space geometry (its body, below the group header). */\nexport type ResolvedLane = {\n id: string\n groupId: string\n label?: string\n color?: string\n /** World-space top edge of the lane body. */\n top: number\n height: number\n /** World x origin and nominal width (bands render effectively infinite). */\n x: number\n width: number\n}\n\n/** A group resolved to world-space geometry: its header band plus its lanes. */\nexport type ResolvedSwimlaneGroup = {\n id: string\n label?: string\n color?: string\n x: number\n width: number\n /** World top edge of the header band. */\n top: number\n /** Header band height in world units. */\n headerSize: number\n /** World bottom edge of the whole group (its last lane's bottom). */\n bottom: number\n lanes: ResolvedLane[]\n}\n\n/** A resolved lane plus how much of it is currently on screen. */\nexport type VisibleLane = ResolvedLane & {\n /** Area (world units²) of the lane's overlap with the viewport. */\n visibleArea: number\n}\n\n/**\n * A snapshot of what the pane is currently showing, produced by\n * `getViewportElements`. Read imperatively — it never subscribes to the\n * viewport, so calling it does not re-render.\n */\nexport type ViewportElements<TNodeData = unknown> = {\n /** The world-space rect currently visible in the pane. */\n rect: Rect\n /** Nodes whose world box intersects the viewport. */\n nodes: RivetNode<TNodeData>[]\n /** Visible lanes, largest on-screen area first. `lanes[0]` is the dominant lane. */\n lanes: VisibleLane[]\n}\n\nconst ORIGIN: Vec2 = { x: 0, y: 0 }\n\n/**\n * Return `color` as an `rgba()` with the given alpha. Hex (`#rgb`/`#rrggbb`) is\n * converted; any other form (already-rgba, named) is returned unchanged. Used so\n * a solid accent color renders as a subtle tint, never an opaque wash.\n */\nexport function withAlpha(color: string, alpha: number): string {\n const hex = color.trim()\n if (!hex.startsWith(\"#\")) return color\n const h = hex.slice(1)\n const full =\n h.length === 3\n ? h\n .split(\"\")\n .map((c) => c + c)\n .join(\"\")\n : h\n if (full.length !== 6) return color\n const n = Number.parseInt(full, 16)\n if (Number.isNaN(n)) return color\n return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`\n}\n\n/** Lay a group out: a header band at `position.y`, then its lanes stacked below. */\nexport function resolveSwimlaneGroup(\n group: SwimlaneGroup,\n headerSize = SWIMLANE_GROUP_HEADER_SIZE,\n): ResolvedSwimlaneGroup {\n const pos = group.position ?? ORIGIN\n const lanes: ResolvedLane[] = []\n let offset = pos.y + headerSize\n for (const lane of group.lanes) {\n lanes.push({\n id: lane.id,\n groupId: group.id,\n label: lane.label,\n // Only an explicit per-lane color tints a lane; the group's accent color\n // is header-only, never a lane background wash.\n color: lane.color,\n top: offset,\n height: lane.size,\n x: pos.x,\n width: group.length,\n })\n offset += lane.size\n }\n return {\n id: group.id,\n label: group.label,\n color: group.color,\n x: pos.x,\n width: group.length,\n top: pos.y,\n headerSize,\n bottom: offset,\n lanes,\n }\n}\n\n/** Resolve every group in a `swimlane` prop to world-space geometry. */\nexport function resolveSwimlanes(\n groups: SwimlaneGroup[],\n headerSize = SWIMLANE_GROUP_HEADER_SIZE,\n): ResolvedSwimlaneGroup[] {\n return groups.map((group) => resolveSwimlaneGroup(group, headerSize))\n}\n\n/** Flatten every group's lanes into a single list. */\nexport function flattenLanes(groups: ResolvedSwimlaneGroup[]): ResolvedLane[] {\n return groups.flatMap((group) => group.lanes)\n}\n\n/**\n * The lane a node box `[top, top+height]` belongs to: the one it overlaps most\n * (ties prefer `preferredLaneId`), or — with no overlap at all — the nearest by\n * center. Returns null only when there are no lanes.\n */\nexport function findLaneByCoverage(\n lanes: ResolvedLane[],\n top: number,\n height: number,\n preferredLaneId?: string,\n): ResolvedLane | null {\n if (lanes.length === 0) return null\n const bottom = top + height\n let best: ResolvedLane | null = null\n let bestOverlap = -1\n for (const lane of lanes) {\n const overlap = Math.max(0, Math.min(bottom, lane.top + lane.height) - Math.max(top, lane.top))\n if (overlap > bestOverlap || (overlap === bestOverlap && lane.id === preferredLaneId)) {\n bestOverlap = overlap\n best = lane\n }\n }\n if (bestOverlap > 0) return best\n\n // No vertical overlap with any lane — snap to the nearest by center.\n const center = top + height / 2\n let nearest: ResolvedLane | null = null\n let bestDist = Number.POSITIVE_INFINITY\n for (const lane of lanes) {\n const dist = Math.abs(lane.top + lane.height / 2 - center)\n if (dist < bestDist) {\n bestDist = dist\n nearest = lane\n }\n }\n return nearest\n}\n\n/**\n * The smallest height `lane` can take while still containing every node assigned\n * to it (by `laneId`), keeping `marginBottom` clearance below the lowest one.\n * Never returns less than {@link SWIMLANE_MIN_HEIGHT}. Backs both the resize\n * floor and auto-fit growth.\n */\nexport function requiredLaneHeight(\n lane: ResolvedLane,\n nodes: Iterable<RivetNode>,\n marginBottom: number,\n): number {\n let min = SWIMLANE_MIN_HEIGHT\n for (const node of nodes) {\n if (node.laneId !== lane.id || !node.size) continue\n const need = node.position.y + node.size.height + marginBottom - lane.top\n if (need > min) min = need\n }\n return min\n}\n\n/** A hit on sticky swimlane chrome (a group header or a lane label). */\nexport type SwimlaneChromeHit = { type: \"group\" | \"lane\"; id: string }\n\n/**\n * Which piece of sticky swimlane chrome sits under a pane-relative `point`, or\n * null. Mirrors the overlay's sticky math so a pane context-menu handler can\n * route right-clicks without the overlay needing to capture pointer events.\n */\nexport function swimlaneChromeAt(\n groups: ResolvedSwimlaneGroup[],\n viewport: Viewport,\n point: Vec2,\n headerHeight: number,\n labelWidth: number,\n): SwimlaneChromeHit | null {\n const { x: vx, y: vy, zoom } = viewport\n const headerBottomByGroup = new Map<string, number>()\n\n for (const group of groups) {\n const screenTop = group.top * zoom + vy\n const screenBottom = group.bottom * zoom + vy\n const stickyY = Math.max(0, Math.min(screenTop, screenBottom - headerHeight))\n headerBottomByGroup.set(group.id, stickyY + headerHeight)\n if (screenBottom > 0 && point.y >= stickyY && point.y <= stickyY + headerHeight) {\n return { type: \"group\", id: group.id }\n }\n }\n\n for (const group of groups) {\n const headerBottom = headerBottomByGroup.get(group.id) ?? 0\n for (const lane of group.lanes) {\n const screenX = lane.x * zoom + vx\n const screenTop = lane.top * zoom + vy\n const screenBottom = screenTop + lane.height * zoom\n const screenEndX = screenX + lane.width * zoom\n const stickyX = Math.max(0, Math.min(screenX, screenEndX - labelWidth))\n const top = Math.max(headerBottom, screenTop)\n if (\n point.x >= stickyX &&\n point.x <= stickyX + labelWidth &&\n point.y >= top &&\n point.y <= screenBottom\n ) {\n return { type: \"lane\", id: lane.id }\n }\n }\n }\n return null\n}\n\nexport type LaneClampResult = { position: Vec2; laneId: string | null }\n\n/**\n * Clamp a node so its whole body sits inside one lane, honoring `margin` (and\n * the left label strip). The target lane is chosen by {@link findLaneByCoverage}\n * — pass `preferredLaneId` to bias toward the node's current lane. Horizontally\n * only the left edge is bounded, since bands are effectively infinite.\n */\nexport function clampNodeToLanes(\n position: Vec2,\n size: Size,\n lanes: ResolvedLane[],\n margin: ResolvedMargin,\n labelWidth: number,\n preferredLaneId?: string,\n): LaneClampResult {\n const lane = findLaneByCoverage(lanes, position.y, size.height, preferredLaneId)\n if (!lane) return { position, laneId: preferredLaneId ?? null }\n\n const minY = lane.top + margin.top\n const maxY = Math.max(minY, lane.top + lane.height - size.height - margin.bottom)\n const minX = lane.x + labelWidth + margin.left\n return {\n position: {\n x: Math.max(minX, position.x),\n y: clamp(position.y, minY, maxY),\n },\n laneId: lane.id,\n }\n}\n"]}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/anchor/registry.ts
|
|
2
|
+
var ANCHOR_SIDES = ["top", "right", "bottom", "left"];
|
|
3
|
+
var DEFAULT_ANCHOR_OPTIONS = {
|
|
4
|
+
defaultSide: "top",
|
|
5
|
+
centerKeepoutPct: 14,
|
|
6
|
+
cornerMarginPct: 8,
|
|
7
|
+
dotSize: 7,
|
|
8
|
+
strayHandleSize: 8
|
|
9
|
+
};
|
|
10
|
+
var STRAY_PREFIX = "inode-";
|
|
11
|
+
var DOT_PREFIX = "inode-text-";
|
|
12
|
+
function isSide(value) {
|
|
13
|
+
return ANCHOR_SIDES.includes(value);
|
|
14
|
+
}
|
|
15
|
+
function anchorHandleId(anchorId, side) {
|
|
16
|
+
return `${STRAY_PREFIX}${anchorId}-${side}`;
|
|
17
|
+
}
|
|
18
|
+
function parseAnchorHandleId(handleId) {
|
|
19
|
+
if (!handleId?.startsWith(STRAY_PREFIX) || handleId.startsWith(DOT_PREFIX)) return null;
|
|
20
|
+
const lastDash = handleId.lastIndexOf("-");
|
|
21
|
+
const side = handleId.slice(lastDash + 1);
|
|
22
|
+
if (!isSide(side)) return null;
|
|
23
|
+
const anchorId = handleId.slice(STRAY_PREFIX.length, lastDash);
|
|
24
|
+
if (!anchorId) return null;
|
|
25
|
+
return { anchorId, side };
|
|
26
|
+
}
|
|
27
|
+
var AUTO_SIDE = "auto";
|
|
28
|
+
function anchorAutoHandleId(anchorId) {
|
|
29
|
+
return `${STRAY_PREFIX}${anchorId}-${AUTO_SIDE}`;
|
|
30
|
+
}
|
|
31
|
+
function parseAnchorAutoHandleId(handleId) {
|
|
32
|
+
if (!handleId?.startsWith(STRAY_PREFIX) || handleId.startsWith(DOT_PREFIX)) return null;
|
|
33
|
+
const lastDash = handleId.lastIndexOf("-");
|
|
34
|
+
if (handleId.slice(lastDash + 1) !== AUTO_SIDE) return null;
|
|
35
|
+
const anchorId = handleId.slice(STRAY_PREFIX.length, lastDash);
|
|
36
|
+
return anchorId || null;
|
|
37
|
+
}
|
|
38
|
+
function anchorDotHandleId(anchorId, side) {
|
|
39
|
+
return `${DOT_PREFIX}${anchorId}-${side}`;
|
|
40
|
+
}
|
|
41
|
+
function parseAnchorDotHandleId(handleId) {
|
|
42
|
+
if (!handleId?.startsWith(DOT_PREFIX)) return null;
|
|
43
|
+
const lastDash = handleId.lastIndexOf("-");
|
|
44
|
+
const side = handleId.slice(lastDash + 1);
|
|
45
|
+
if (!isSide(side)) return null;
|
|
46
|
+
const anchorId = handleId.slice(DOT_PREFIX.length, lastDash);
|
|
47
|
+
if (!anchorId) return null;
|
|
48
|
+
return { anchorId, side };
|
|
49
|
+
}
|
|
50
|
+
function normalizeAnchorConnection(connection) {
|
|
51
|
+
const source = parseAnchorDotHandleId(connection.sourceHandle);
|
|
52
|
+
const target = parseAnchorDotHandleId(connection.targetHandle);
|
|
53
|
+
if (!source && !target) return connection;
|
|
54
|
+
return {
|
|
55
|
+
...connection,
|
|
56
|
+
sourceHandle: source ? anchorHandleId(source.anchorId, source.side) : connection.sourceHandle,
|
|
57
|
+
targetHandle: target ? anchorHandleId(target.anchorId, target.side) : connection.targetHandle
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function computeAnchorGeometry(root, first, last, box) {
|
|
61
|
+
const point = (x, y) => ({
|
|
62
|
+
xPct: (x - root.left) / root.width * 100,
|
|
63
|
+
yPct: (y - root.top) / root.height * 100
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
firstLineYPct: (first.top + first.height / 2 - root.top) / root.height * 100,
|
|
67
|
+
lastLineYPct: (last.top + last.height / 2 - root.top) / root.height * 100,
|
|
68
|
+
centerXPct: (box.left + box.width / 2 - root.left) / root.width * 100,
|
|
69
|
+
dots: {
|
|
70
|
+
top: point(first.left + first.width / 2, first.top),
|
|
71
|
+
bottom: point(last.left + last.width / 2, last.top + last.height),
|
|
72
|
+
left: point(first.left, first.top + first.height / 2),
|
|
73
|
+
right: point(last.left + last.width, last.top + last.height / 2)
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
var GEOMETRY_TOLERANCE_PCT = 0.1;
|
|
78
|
+
function pointsEqual(a, b) {
|
|
79
|
+
return Math.abs(a.xPct - b.xPct) < GEOMETRY_TOLERANCE_PCT && Math.abs(a.yPct - b.yPct) < GEOMETRY_TOLERANCE_PCT;
|
|
80
|
+
}
|
|
81
|
+
function anchorGeometryEqual(a, b) {
|
|
82
|
+
if (a === b) return true;
|
|
83
|
+
if (!a || !b) return false;
|
|
84
|
+
return Math.abs(a.firstLineYPct - b.firstLineYPct) < GEOMETRY_TOLERANCE_PCT && Math.abs(a.lastLineYPct - b.lastLineYPct) < GEOMETRY_TOLERANCE_PCT && Math.abs(a.centerXPct - b.centerXPct) < GEOMETRY_TOLERANCE_PCT && ANCHOR_SIDES.every((side) => pointsEqual(a.dots[side], b.dots[side]));
|
|
85
|
+
}
|
|
86
|
+
function placeAlongSide(rawPct, options) {
|
|
87
|
+
const clamped = Math.min(Math.max(rawPct, options.cornerMarginPct), 100 - options.cornerMarginPct);
|
|
88
|
+
if (Math.abs(clamped - 50) >= options.centerKeepoutPct) return clamped;
|
|
89
|
+
return clamped < 50 ? 50 - options.centerKeepoutPct : 50 + options.centerKeepoutPct;
|
|
90
|
+
}
|
|
91
|
+
function strayPlacementPct(geometry, side, options) {
|
|
92
|
+
const raw = side === "left" ? geometry.firstLineYPct : side === "right" ? geometry.lastLineYPct : geometry.centerXPct;
|
|
93
|
+
return placeAlongSide(raw, options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { ANCHOR_SIDES, DEFAULT_ANCHOR_OPTIONS, anchorAutoHandleId, anchorDotHandleId, anchorGeometryEqual, anchorHandleId, computeAnchorGeometry, normalizeAnchorConnection, parseAnchorAutoHandleId, parseAnchorDotHandleId, parseAnchorHandleId, strayPlacementPct };
|
|
97
|
+
//# sourceMappingURL=chunk-AXT35ZJV.js.map
|
|
98
|
+
//# sourceMappingURL=chunk-AXT35ZJV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/anchor/registry.ts"],"names":[],"mappings":"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","file":"chunk-AXT35ZJV.js","sourcesContent":["/**\n * Anchor — a connectable point bound to a DOM element rendered inside a node.\n *\n * Rivet knows nothing about what the element means (an inline text span, an\n * icon, a table row — that's the consumer's business); it owns the geometry:\n * measuring the element's line boxes within the node, rendering grab dots and\n * stray perimeter handles from those measurements, and rewriting dot handle\n * ids to the persisted stray ids when a connection commits.\n *\n * Placement derives from the element's *first* and *last* client line rects —\n * never from CSS positioning inside the inline element, which Chrome anchors\n * to the first line box only — so a line-wrapped span places each dot where it\n * visually belongs. All values are percentages of the node box: client rects\n * scale together under the viewport transform, so the ratio cancels zoom.\n */\n\nimport type { Connection, HandlePosition, NodeId } from \"../types\"\n\n/** The four sides an anchor can expose handles on. */\nexport const ANCHOR_SIDES = [\"top\", \"right\", \"bottom\", \"left\"] as const\n\n/** A measured point within a node's box, as percentages (zoom-independent). */\nexport type AnchorPoint = { xPct: number; yPct: number }\n\n/** Where an anchor's element sits within its node box, from measured line rects. */\nexport type AnchorGeometry = {\n /** Vertical center of the element's first line box, as % of the node height. */\n firstLineYPct: number\n /** Vertical center of the element's last line box, as % of the node height. */\n lastLineYPct: number\n /** Horizontal center of the element's bounding box, as % of the node width. */\n centerXPct: number\n /**\n * Grab dot positions: left/top on the first line box, right/bottom on the\n * last, so a wrapped element gets each dot on the line it belongs to.\n */\n dots: Record<HandlePosition, AnchorPoint>\n}\n\n/**\n * Stray-handle chrome policy, chosen at registration:\n *\n * - `\"dynamic\"` — one perimeter handle per connected side, a single\n * default-side handle while unconnected. The inline-text default.\n * - `\"none\"` — no stray chrome at all; the anchor connects through its grab\n * dots only. For anchors whose element boundary *is* the visual connection\n * point (an SVG shape), where a floating perimeter handle reads as clutter.\n * Purely visual: edges bound to stray ids still resolve at the measured\n * placement — the renderer derives endpoints from geometry, not chrome.\n */\nexport type AnchorStrayPolicy = \"dynamic\" | \"none\"\n\n/**\n * A registered anchor. `element` is `null` while detached — the consumer\n * unmounted the display (or an editor replaced the DOM node) without\n * unregistering, so the last-known `geometry` is retained and edges stay put\n * (stale retention). Unregistering is the explicit removal signal.\n */\nexport type AnchorRecord = {\n nodeId: NodeId\n anchorId: string\n /** Accent color for the anchor's dots and stray handles. */\n color?: string\n /** Stray chrome policy. Absent = `\"dynamic\"`. */\n strays?: AnchorStrayPolicy\n element: Element | null\n /** Null until the first successful measurement. */\n geometry: AnchorGeometry | null\n}\n\nexport type AnchorRegistrationOptions = {\n color?: string\n /** Stray-handle chrome policy for this anchor. Default `\"dynamic\"`. */\n strays?: AnchorStrayPolicy\n}\n\n/**\n * Imperative registration handle for headless consumers (rich-text editors and\n * other renderers that own their DOM). Returned by `RivetInstance.registerAnchor`.\n */\nexport type AnchorRegistration = {\n /**\n * Re-attach the anchor after the renderer replaced its DOM node. Passing\n * `null` detaches instead: geometry is retained and edges stay put until the\n * element (or a replacement) is rebound or the anchor is unregistered.\n */\n rebind: (element: Element | null) => void\n /** Explicit removal — drops the anchor and the chrome rendered for it. */\n unregister: () => void\n}\n\n/** Placement policy for anchor chrome, set via the `anchorOptions` prop. */\nexport type AnchorOptions = {\n /** Side an unconnected anchor shows its single stray handle on. Default `\"top\"`. */\n defaultSide?: HandlePosition\n /**\n * Minimum distance (in % of the side) a stray handle keeps from the side's\n * center, where a node's own side-center handles usually sit. Default `14`.\n */\n centerKeepoutPct?: number\n /** Minimum distance (in % of the side) a stray handle keeps from the corners. Default `8`. */\n cornerMarginPct?: number\n /** Grab dot diameter in px. Default `7`. */\n dotSize?: number\n /** Stray handle diameter in px. Default `8`. */\n strayHandleSize?: number\n}\n\nexport type ResolvedAnchorOptions = Required<AnchorOptions>\n\nexport const DEFAULT_ANCHOR_OPTIONS: ResolvedAnchorOptions = {\n defaultSide: \"top\",\n centerKeepoutPct: 14,\n cornerMarginPct: 8,\n dotSize: 7,\n strayHandleSize: 8,\n}\n\n// ============================================================================\n// Handle-id grammar\n// ============================================================================\n//\n// The string forms are the persisted wire format for anchor connectivity:\n// `inode-<anchorId>-<side>` for the stray (perimeter) handles that edges\n// reference, `inode-text-<anchorId>-<side>` for the in-content grab dots that\n// only ever *start* a drag. The `inode-` prefix is the historical grammar from\n// the pre-primitive consumer implementation, kept verbatim so existing graphs\n// load without migration; the structured edge-end model replaces these strings\n// later without changing what's stored.\n\nconst STRAY_PREFIX = \"inode-\"\nconst DOT_PREFIX = \"inode-text-\"\n\nfunction isSide(value: string): value is HandlePosition {\n return (ANCHOR_SIDES as readonly string[]).includes(value)\n}\n\n/** Build the persisted handle id for an anchor's stray handle on `side`. */\nexport function anchorHandleId(anchorId: string, side: HandlePosition): string {\n return `${STRAY_PREFIX}${anchorId}-${side}`\n}\n\n/** Parse a stray anchor handle id back into `{ anchorId, side }`, or null. */\nexport function parseAnchorHandleId(\n handleId: string | null | undefined,\n): { anchorId: string; side: HandlePosition } | null {\n // A dot handle id shares the stray prefix; it is a local-only affordance,\n // never a persisted edge handle, so it must not be read as a stray here.\n if (!handleId?.startsWith(STRAY_PREFIX) || handleId.startsWith(DOT_PREFIX)) return null\n const lastDash = handleId.lastIndexOf(\"-\")\n const side = handleId.slice(lastDash + 1)\n if (!isSide(side)) return null\n const anchorId = handleId.slice(STRAY_PREFIX.length, lastDash)\n if (!anchorId) return null\n return { anchorId, side }\n}\n\n/**\n * The auto-side marker in the anchor id grammar. `inode-<anchorId>-auto` is an\n * anchor end with *no pinned side*: the renderer resolves the side per frame to\n * face the peer node (a floating end). Not a side, so it never collides with\n * the stray forms — but an `anchorId` should not itself end in `-auto` or a\n * `-<side>` suffix, which the grammar cannot distinguish.\n */\nconst AUTO_SIDE = \"auto\"\n\n/** Build the handle id for an anchor end with an auto (render-resolved) side. */\nexport function anchorAutoHandleId(anchorId: string): string {\n return `${STRAY_PREFIX}${anchorId}-${AUTO_SIDE}`\n}\n\n/** Parse an auto anchor handle id back into its `anchorId`, or null. */\nexport function parseAnchorAutoHandleId(handleId: string | null | undefined): string | null {\n if (!handleId?.startsWith(STRAY_PREFIX) || handleId.startsWith(DOT_PREFIX)) return null\n const lastDash = handleId.lastIndexOf(\"-\")\n if (handleId.slice(lastDash + 1) !== AUTO_SIDE) return null\n const anchorId = handleId.slice(STRAY_PREFIX.length, lastDash)\n return anchorId || null\n}\n\n/** Build the handle id for an anchor's in-content grab dot on `side`. */\nexport function anchorDotHandleId(anchorId: string, side: HandlePosition): string {\n return `${DOT_PREFIX}${anchorId}-${side}`\n}\n\n/** Parse a grab dot handle id back into `{ anchorId, side }`, or null. */\nexport function parseAnchorDotHandleId(\n handleId: string | null | undefined,\n): { anchorId: string; side: HandlePosition } | null {\n if (!handleId?.startsWith(DOT_PREFIX)) return null\n const lastDash = handleId.lastIndexOf(\"-\")\n const side = handleId.slice(lastDash + 1)\n if (!isSide(side)) return null\n const anchorId = handleId.slice(DOT_PREFIX.length, lastDash)\n if (!anchorId) return null\n return { anchorId, side }\n}\n\n/**\n * Rewrite grab dot ends to the persisted stray ids — same anchor, same side —\n * before a connection commits. Intent-preserving: the side comes from the dot\n * the user actually grabbed or dropped on. Runs before any consumer\n * `mapConnection`; connections not touching a dot pass through unchanged.\n */\nexport function normalizeAnchorConnection(connection: Connection): Connection {\n const source = parseAnchorDotHandleId(connection.sourceHandle)\n const target = parseAnchorDotHandleId(connection.targetHandle)\n if (!source && !target) return connection\n return {\n ...connection,\n sourceHandle: source ? anchorHandleId(source.anchorId, source.side) : connection.sourceHandle,\n targetHandle: target ? anchorHandleId(target.anchorId, target.side) : connection.targetHandle,\n }\n}\n\n// ============================================================================\n// Measurement\n// ============================================================================\n\n/** The rect fields measurement reads — satisfied by DOMRect. */\nexport type AnchorRectLike = {\n left: number\n top: number\n width: number\n height: number\n}\n\n/**\n * Derive an anchor's geometry from its node box (`root`), its first and last\n * client line rects, and its bounding box. Pure — callers supply the rects.\n */\nexport function computeAnchorGeometry(\n root: AnchorRectLike,\n first: AnchorRectLike,\n last: AnchorRectLike,\n box: AnchorRectLike,\n): AnchorGeometry {\n const point = (x: number, y: number): AnchorPoint => ({\n xPct: ((x - root.left) / root.width) * 100,\n yPct: ((y - root.top) / root.height) * 100,\n })\n return {\n firstLineYPct: ((first.top + first.height / 2 - root.top) / root.height) * 100,\n lastLineYPct: ((last.top + last.height / 2 - root.top) / root.height) * 100,\n centerXPct: ((box.left + box.width / 2 - root.left) / root.width) * 100,\n dots: {\n top: point(first.left + first.width / 2, first.top),\n bottom: point(last.left + last.width / 2, last.top + last.height),\n left: point(first.left, first.top + first.height / 2),\n right: point(last.left + last.width, last.top + last.height / 2),\n },\n }\n}\n\nconst GEOMETRY_TOLERANCE_PCT = 0.1\n\nfunction pointsEqual(a: AnchorPoint, b: AnchorPoint): boolean {\n return (\n Math.abs(a.xPct - b.xPct) < GEOMETRY_TOLERANCE_PCT &&\n Math.abs(a.yPct - b.yPct) < GEOMETRY_TOLERANCE_PCT\n )\n}\n\n/** Sub-tolerance equality, so measurement echoes don't churn subscribers. */\nexport function anchorGeometryEqual(a: AnchorGeometry | null, b: AnchorGeometry | null): boolean {\n if (a === b) return true\n if (!a || !b) return false\n return (\n Math.abs(a.firstLineYPct - b.firstLineYPct) < GEOMETRY_TOLERANCE_PCT &&\n Math.abs(a.lastLineYPct - b.lastLineYPct) < GEOMETRY_TOLERANCE_PCT &&\n Math.abs(a.centerXPct - b.centerXPct) < GEOMETRY_TOLERANCE_PCT &&\n ANCHOR_SIDES.every((side) => pointsEqual(a.dots[side], b.dots[side]))\n )\n}\n\n// ============================================================================\n// Stray-handle placement along a node side\n// ============================================================================\n\n/**\n * Nudge a raw position (in % of the side) out of the side-center keep-out band\n * — where the node's own handles usually sit — and off the corners.\n */\nexport function placeAlongSide(rawPct: number, options: ResolvedAnchorOptions): number {\n const clamped = Math.min(Math.max(rawPct, options.cornerMarginPct), 100 - options.cornerMarginPct)\n if (Math.abs(clamped - 50) >= options.centerKeepoutPct) return clamped\n return clamped < 50 ? 50 - options.centerKeepoutPct : 50 + options.centerKeepoutPct\n}\n\n/**\n * Where an anchor's stray handle sits along `side`, as % of that side — the\n * measured row (left/right) or column (top/bottom) nearest the anchor, nudged\n * by {@link placeAlongSide}. Shared by the stray-handle chrome and by the edge\n * renderer when it places an auto anchor end on a render-resolved side.\n */\nexport function strayPlacementPct(\n geometry: AnchorGeometry,\n side: HandlePosition,\n options: ResolvedAnchorOptions,\n): number {\n const raw =\n side === \"left\"\n ? geometry.firstLineYPct\n : side === \"right\"\n ? geometry.lastLineYPct\n : geometry.centerXPct\n return placeAlongSide(raw, options)\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createContext, useContext, useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/context.ts
|
|
4
|
+
var RivetContext = createContext(null);
|
|
5
|
+
function useRivetContext() {
|
|
6
|
+
const ctx = useContext(RivetContext);
|
|
7
|
+
if (!ctx) throw new Error("rivet components must be rendered inside <Rivet>");
|
|
8
|
+
return ctx;
|
|
9
|
+
}
|
|
10
|
+
function useRivetControls() {
|
|
11
|
+
return useRivetContext().controls;
|
|
12
|
+
}
|
|
13
|
+
function useRivetViewport() {
|
|
14
|
+
const { store } = useRivetContext();
|
|
15
|
+
return useSyncExternalStore(store.subscribeViewport, store.getViewport, store.getViewport);
|
|
16
|
+
}
|
|
17
|
+
function useRivetHistory() {
|
|
18
|
+
const { store } = useRivetContext();
|
|
19
|
+
const canUndo = useSyncExternalStore(store.subscribeHistory, store.canUndo, store.canUndo);
|
|
20
|
+
const canRedo = useSyncExternalStore(store.subscribeHistory, store.canRedo, store.canRedo);
|
|
21
|
+
return { canUndo, canRedo, undo: store.undo, redo: store.redo, clearHistory: store.clearHistory };
|
|
22
|
+
}
|
|
23
|
+
function useRivetFocusedNode() {
|
|
24
|
+
const { store } = useRivetContext();
|
|
25
|
+
return useSyncExternalStore(store.subscribeFocus, store.getFocusedNode, store.getFocusedNode);
|
|
26
|
+
}
|
|
27
|
+
var RivetNodeContext = createContext(null);
|
|
28
|
+
function useRivetNodeContext() {
|
|
29
|
+
const ctx = useContext(RivetNodeContext);
|
|
30
|
+
if (!ctx) throw new Error("<Handle> must be rendered inside a rivet node component");
|
|
31
|
+
return ctx;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { RivetContext, RivetNodeContext, useRivetContext, useRivetControls, useRivetFocusedNode, useRivetHistory, useRivetNodeContext, useRivetViewport };
|
|
35
|
+
//# sourceMappingURL=chunk-VQYN27OK.js.map
|
|
36
|
+
//# sourceMappingURL=chunk-VQYN27OK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.ts"],"names":[],"mappings":";;;AA4FO,IAAM,YAAA,GAAe,cAAwC,IAAI;AAEjE,SAAS,eAAA,GAAqC;AACnD,EAAA,MAAM,GAAA,GAAM,WAAW,YAAY,CAAA;AACnC,EAAA,IAAI,CAAC,GAAA,EAAK,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAC5E,EAAA,OAAO,GAAA;AACT;AAGO,SAAS,gBAAA,GAAkC;AAChD,EAAA,OAAO,iBAAgB,CAAE,QAAA;AAC3B;AAGO,SAAS,gBAAA,GAA6B;AAC3C,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,eAAA,EAAgB;AAClC,EAAA,OAAO,qBAAqB,KAAA,CAAM,iBAAA,EAAmB,KAAA,CAAM,WAAA,EAAa,MAAM,WAAW,CAAA;AAC3F;AAGO,SAAS,eAAA,GAMd;AACA,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,eAAA,EAAgB;AAClC,EAAA,MAAM,UAAU,oBAAA,CAAqB,KAAA,CAAM,kBAAkB,KAAA,CAAM,OAAA,EAAS,MAAM,OAAO,CAAA;AACzF,EAAA,MAAM,UAAU,oBAAA,CAAqB,KAAA,CAAM,kBAAkB,KAAA,CAAM,OAAA,EAAS,MAAM,OAAO,CAAA;AACzF,EAAA,OAAO,EAAE,OAAA,EAAS,OAAA,EAAS,IAAA,EAAM,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,CAAM,IAAA,EAAM,YAAA,EAAc,KAAA,CAAM,YAAA,EAAa;AAClG;AAOO,SAAS,mBAAA,GAAqC;AACnD,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,eAAA,EAAgB;AAClC,EAAA,OAAO,qBAAqB,KAAA,CAAM,cAAA,EAAgB,KAAA,CAAM,cAAA,EAAgB,MAAM,cAAc,CAAA;AAC9F;AAQO,IAAM,gBAAA,GAAmB,cAA4C,IAAI;AAEzE,SAAS,mBAAA,GAA6C;AAC3D,EAAA,MAAM,GAAA,GAAM,WAAW,gBAAgB,CAAA;AACvC,EAAA,IAAI,CAAC,GAAA,EAAK,MAAM,IAAI,MAAM,yDAAyD,CAAA;AACnF,EAAA,OAAO,GAAA;AACT","file":"chunk-VQYN27OK.js","sourcesContent":["import {\n createContext,\n type ReactNode,\n type RefObject,\n useContext,\n useSyncExternalStore,\n} from \"react\"\nimport type { ResolvedAnchorOptions } from \"./anchor/registry\"\nimport type { RivetStore } from \"./store\"\nimport type {\n ResolvedLane,\n ResolvedMargin,\n ResolvedSwimlaneGroup,\n ViewportElements,\n} from \"./swimlane/lanes\"\nimport type {\n Connection,\n DefaultEdgeOptions,\n LaneChange,\n NodeId,\n NodeTypes,\n RivetControls,\n SwimlaneHeaderProps,\n SwimlaneLabelProps,\n SwimlaneSizeChange,\n Viewport,\n} from \"./types\"\n\nexport type RivetContextValue = {\n store: RivetStore\n nodeTypes: NodeTypes\n paneRef: RefObject<HTMLDivElement | null>\n /** Imperative viewport controls (zoom, fit-view). */\n controls: RivetControls\n /** Keys that make a click add to (toggle in) the selection. */\n multiSelectionKeys: string[]\n /**\n * Publish a message to the pane's visually-hidden live region, so screen\n * readers narrate keyboard interactions (grab/move/drop, connect).\n */\n announce: (message: string) => void\n /** Id of the pane's hidden keyboard-instructions element (`aria-describedby` target). */\n nodeDescriptionId: string\n /** Defaults merged into every edge — the label layer reads `labelStyle`. */\n defaultEdgeOptions?: DefaultEdgeOptions\n /** Grid to snap dragged nodes to (`[x, y]` world units), or null for free movement. */\n snapGrid: [number, number] | null\n /** Whether a single-node drag shows alignment guides and snaps to neighbors. */\n alignmentGuides: boolean\n /** Resolved placement policy for anchor chrome (dots + stray handles). */\n anchorOptions: ResolvedAnchorOptions\n /** Snapshot the nodes and lanes currently on screen. Imperative — never subscribes. */\n getViewportElements: () => ViewportElements\n /** Resolved swimlane groups (header + lanes, world space). Empty when unused. */\n swimlaneGroups: ResolvedSwimlaneGroup[]\n /** Every group's lanes, flattened — the clamp targets. */\n swimlaneLanes: ResolvedLane[]\n /** Whether node drags are bounded to a lane and reassign lane membership. */\n clampToSwimlane: boolean\n /** Inner clearance kept between a clamped node and its lane edges. */\n swimlaneMargin: ResolvedMargin\n /** Screen width of the sticky lane-label strip, in px. */\n swimlaneLabelWidth: number\n /** Screen height of the sticky group header band, in px. */\n swimlaneHeaderSize: number\n /** Whether lanes show a bottom resize handle. */\n swimlaneResizable: boolean\n /**\n * Resize a lane. `commit` fires the consumer's `onSwimlaneSizeChange` with the\n * new height; `reason` tags it (`\"resize\"` for a drag, `\"autofit\"` for a\n * fit-to-content snap). Defaults to `\"resize\"`.\n */\n resizeSwimlane: (\n laneId: string,\n height: number,\n commit: boolean,\n reason?: SwimlaneSizeChange[\"reason\"],\n ) => void\n /** Consumer override for the sticky group header content. */\n renderSwimlaneHeader?: (props: SwimlaneHeaderProps) => ReactNode\n /** Consumer override for the sticky lane label content. */\n renderSwimlaneLabel?: (props: SwimlaneLabelProps) => ReactNode\n /** Called when a dragged node crosses into a different lane. */\n onLaneChange?: (change: LaneChange) => void\n /** Veto a proposed connection; return false to reject. */\n isValidConnection?: (connection: Connection) => boolean\n /** Rewrite a validated connection before commit; `null` cancels the drop. */\n mapConnection?: (connection: Connection) => Connection | null\n /** Called after a valid connection is made. */\n onConnect?: (connection: Connection) => void\n}\n\nexport const RivetContext = createContext<RivetContextValue | null>(null)\n\nexport function useRivetContext(): RivetContextValue {\n const ctx = useContext(RivetContext)\n if (!ctx) throw new Error(\"rivet components must be rendered inside <Rivet>\")\n return ctx\n}\n\n/** Access the imperative viewport controls from inside `<Rivet>`. */\nexport function useRivetControls(): RivetControls {\n return useRivetContext().controls\n}\n\n/** Subscribe to the live viewport — re-renders on every pan/zoom. */\nexport function useRivetViewport(): Viewport {\n const { store } = useRivetContext()\n return useSyncExternalStore(store.subscribeViewport, store.getViewport, store.getViewport)\n}\n\n/** Reactive undo/redo state + actions — re-renders when `canUndo`/`canRedo` flip. */\nexport function useRivetHistory(): {\n canUndo: boolean\n canRedo: boolean\n undo: () => boolean\n redo: () => boolean\n clearHistory: () => void\n} {\n const { store } = useRivetContext()\n const canUndo = useSyncExternalStore(store.subscribeHistory, store.canUndo, store.canUndo)\n const canRedo = useSyncExternalStore(store.subscribeHistory, store.canRedo, store.canRedo)\n return { canUndo, canRedo, undo: store.undo, redo: store.redo, clearHistory: store.clearHistory }\n}\n\n/**\n * The node holding keyboard focus, or `null` — re-renders as focus roves.\n * Rendering a focus indicator is the consumer's job (rivet is headless); this\n * is the state to drive it from, along with the wrapper's native `:focus-visible`.\n */\nexport function useRivetFocusedNode(): NodeId | null {\n const { store } = useRivetContext()\n return useSyncExternalStore(store.subscribeFocus, store.getFocusedNode, store.getFocusedNode)\n}\n\n/** Per-node context so `<Handle>` can find its node and measure against it. */\nexport type RivetNodeContextValue = {\n nodeId: NodeId\n wrapperRef: RefObject<HTMLDivElement | null>\n}\n\nexport const RivetNodeContext = createContext<RivetNodeContextValue | null>(null)\n\nexport function useRivetNodeContext(): RivetNodeContextValue {\n const ctx = useContext(RivetNodeContext)\n if (!ctx) throw new Error(\"<Handle> must be rendered inside a rivet node component\")\n return ctx\n}\n"]}
|