@designa8/vibedesign-stripe-deepseek-85ea 0.1.3 → 0.1.4
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/README.md +1 -1
- package/dist/index.cjs +133 -3
- package/dist/index.js +133 -3
- package/dist/styles.css +3 -0
- package/package.json +1 -1
- package/src/atoms/PaymentRoutingCanvas/PaymentRoutingCanvas.tsx +203 -3
- package/src/atoms/PaymentRoutingCanvas/preview.tsx +5 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @designa8/vibedesign-stripe-deepseek-85ea
|
|
2
2
|
|
|
3
|
-
Complete design system generated by Atoms Design, version **0.1.
|
|
3
|
+
Complete design system generated by Atoms Design, version **0.1.4**.
|
|
4
4
|
Tokens, atoms, molecules, organisms, and templates — import from the package root;
|
|
5
5
|
never hardcode visual values in consumer code.
|
|
6
6
|
|
package/dist/index.cjs
CHANGED
|
@@ -12108,6 +12108,9 @@ var PLANE_H = 372;
|
|
|
12108
12108
|
var PLANE_PAD = 18;
|
|
12109
12109
|
var PANEL_W = 296;
|
|
12110
12110
|
var SNAP = 8;
|
|
12111
|
+
var MARQUEE_MIN = 4;
|
|
12112
|
+
var clampTo = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
12113
|
+
var boxesIntersect = (a, b) => a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
12111
12114
|
var DEFAULT_NODES = [
|
|
12112
12115
|
{
|
|
12113
12116
|
id: "source",
|
|
@@ -12307,6 +12310,10 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12307
12310
|
selectionRef.current = selection;
|
|
12308
12311
|
const onSelectionChangeRef = React62.useRef(onSelectionChange);
|
|
12309
12312
|
onSelectionChangeRef.current = onSelectionChange;
|
|
12313
|
+
const [marqueeBox, setMarqueeBox] = React62.useState(null);
|
|
12314
|
+
const [marqueeIds, setMarqueeIds] = React62.useState(null);
|
|
12315
|
+
const marqueeRef = React62.useRef(null);
|
|
12316
|
+
const [modifierHeld, setModifierHeld] = React62.useState(false);
|
|
12310
12317
|
const rootRef = React62.useRef(null);
|
|
12311
12318
|
const planeRef = React62.useRef(null);
|
|
12312
12319
|
const triggerRef = React62.useRef(null);
|
|
@@ -12318,6 +12325,18 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12318
12325
|
React62.useEffect(() => {
|
|
12319
12326
|
setLocalNodes(incomingNodes);
|
|
12320
12327
|
}, [incomingNodes]);
|
|
12328
|
+
React62.useEffect(() => {
|
|
12329
|
+
const syncModifier = (event) => setModifierHeld(Boolean(event.metaKey || event.ctrlKey));
|
|
12330
|
+
const releaseModifier = () => setModifierHeld(false);
|
|
12331
|
+
window.addEventListener("keydown", syncModifier);
|
|
12332
|
+
window.addEventListener("keyup", syncModifier);
|
|
12333
|
+
window.addEventListener("blur", releaseModifier);
|
|
12334
|
+
return () => {
|
|
12335
|
+
window.removeEventListener("keydown", syncModifier);
|
|
12336
|
+
window.removeEventListener("keyup", syncModifier);
|
|
12337
|
+
window.removeEventListener("blur", releaseModifier);
|
|
12338
|
+
};
|
|
12339
|
+
}, []);
|
|
12321
12340
|
React62.useEffect(() => {
|
|
12322
12341
|
const plane = planeRef.current;
|
|
12323
12342
|
if (!plane) return;
|
|
@@ -12387,6 +12406,7 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12387
12406
|
});
|
|
12388
12407
|
const focusedNode = panelNodeId ? nodeById.get(panelNodeId) : void 0;
|
|
12389
12408
|
const selectionSet = React62.useMemo(() => new Set(selection), [selection]);
|
|
12409
|
+
const marqueeSet = React62.useMemo(() => new Set(marqueeIds ?? []), [marqueeIds]);
|
|
12390
12410
|
const commitSelection = React62.useCallback(
|
|
12391
12411
|
(next) => {
|
|
12392
12412
|
const unique = next.filter((id, index) => next.indexOf(id) === index);
|
|
@@ -12530,6 +12550,91 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12530
12550
|
window.addEventListener("pointermove", move);
|
|
12531
12551
|
window.addEventListener("pointerup", end, { once: true });
|
|
12532
12552
|
};
|
|
12553
|
+
const beginMarquee = (event) => {
|
|
12554
|
+
if (event.button !== 0) return;
|
|
12555
|
+
if (nodeDragActive.current) return;
|
|
12556
|
+
const plane = planeRef.current;
|
|
12557
|
+
if (!plane) return;
|
|
12558
|
+
const box = plane.getBoundingClientRect();
|
|
12559
|
+
if (!box.width || !box.height) return;
|
|
12560
|
+
const gestureScale = fit * zoom || 1;
|
|
12561
|
+
const originX = clampTo(event.clientX - box.left, 0, box.width);
|
|
12562
|
+
const originY = clampTo(event.clientY - box.top, 0, box.height);
|
|
12563
|
+
const gesture = {
|
|
12564
|
+
originX,
|
|
12565
|
+
originY,
|
|
12566
|
+
box: { left: box.left, top: box.top, width: box.width, height: box.height },
|
|
12567
|
+
offsetX: PLANE_PAD + pan.x,
|
|
12568
|
+
offsetY: Math.max((planeHeight - SCENE_H * gestureScale) / 2, 0) + pan.y,
|
|
12569
|
+
scale: gestureScale,
|
|
12570
|
+
base: selectionRef.current,
|
|
12571
|
+
rect: { x: originX, y: originY, w: 0, h: 0 }
|
|
12572
|
+
};
|
|
12573
|
+
marqueeRef.current = gesture;
|
|
12574
|
+
setMarqueeBox(gesture.rect);
|
|
12575
|
+
setMarqueeIds([]);
|
|
12576
|
+
const hitsFor = (rect) => {
|
|
12577
|
+
const scene = {
|
|
12578
|
+
x: (rect.x - gesture.offsetX) / gesture.scale,
|
|
12579
|
+
y: (rect.y - gesture.offsetY) / gesture.scale,
|
|
12580
|
+
w: rect.w / gesture.scale,
|
|
12581
|
+
h: rect.h / gesture.scale
|
|
12582
|
+
};
|
|
12583
|
+
return nodesRef.current.filter(
|
|
12584
|
+
(node) => boxesIntersect(scene, {
|
|
12585
|
+
x: node.x,
|
|
12586
|
+
y: node.y,
|
|
12587
|
+
w: NODE_W,
|
|
12588
|
+
h: node.h ?? NODE_H
|
|
12589
|
+
})
|
|
12590
|
+
).map((node) => node.id);
|
|
12591
|
+
};
|
|
12592
|
+
const cleanup = () => {
|
|
12593
|
+
window.removeEventListener("pointermove", move);
|
|
12594
|
+
window.removeEventListener("pointerup", end);
|
|
12595
|
+
window.removeEventListener("pointercancel", cancel);
|
|
12596
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
12597
|
+
};
|
|
12598
|
+
const settle = () => {
|
|
12599
|
+
cleanup();
|
|
12600
|
+
marqueeRef.current = null;
|
|
12601
|
+
setMarqueeBox(null);
|
|
12602
|
+
setMarqueeIds(null);
|
|
12603
|
+
};
|
|
12604
|
+
function cancel() {
|
|
12605
|
+
settle();
|
|
12606
|
+
}
|
|
12607
|
+
function onKeyDown(keyEvent) {
|
|
12608
|
+
if (keyEvent.key !== "Escape") return;
|
|
12609
|
+
keyEvent.preventDefault();
|
|
12610
|
+
cancel();
|
|
12611
|
+
}
|
|
12612
|
+
function move(moveEvent) {
|
|
12613
|
+
const x = clampTo(moveEvent.clientX - gesture.box.left, 0, gesture.box.width);
|
|
12614
|
+
const y = clampTo(moveEvent.clientY - gesture.box.top, 0, gesture.box.height);
|
|
12615
|
+
const rect = {
|
|
12616
|
+
x: Math.min(gesture.originX, x),
|
|
12617
|
+
y: Math.min(gesture.originY, y),
|
|
12618
|
+
w: Math.abs(x - gesture.originX),
|
|
12619
|
+
h: Math.abs(y - gesture.originY)
|
|
12620
|
+
};
|
|
12621
|
+
gesture.rect = rect;
|
|
12622
|
+
setMarqueeBox(rect);
|
|
12623
|
+
setMarqueeIds(hitsFor(rect));
|
|
12624
|
+
}
|
|
12625
|
+
function end() {
|
|
12626
|
+
const rect = gesture.rect;
|
|
12627
|
+
settle();
|
|
12628
|
+
if (Math.max(rect.w, rect.h) < MARQUEE_MIN) return;
|
|
12629
|
+
const hits = hitsFor(rect);
|
|
12630
|
+
if (hits.length === 0) return;
|
|
12631
|
+
commitSelection([...gesture.base, ...hits]);
|
|
12632
|
+
}
|
|
12633
|
+
window.addEventListener("pointermove", move);
|
|
12634
|
+
window.addEventListener("pointerup", end);
|
|
12635
|
+
window.addEventListener("pointercancel", cancel);
|
|
12636
|
+
window.addEventListener("keydown", onKeyDown);
|
|
12637
|
+
};
|
|
12533
12638
|
const moveFocus = (node, key) => {
|
|
12534
12639
|
const column = localNodes.filter((item) => Math.abs(item.x - node.x) < 1).sort((a, b) => a.y - b.y);
|
|
12535
12640
|
let target;
|
|
@@ -12648,7 +12753,10 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12648
12753
|
"aria-label": "Payment routing graph",
|
|
12649
12754
|
"aria-describedby": "routing-selection-status",
|
|
12650
12755
|
tabIndex: 0,
|
|
12651
|
-
onPointerDown:
|
|
12756
|
+
onPointerDown: (event) => {
|
|
12757
|
+
if (event.metaKey || event.ctrlKey) beginMarquee(event);
|
|
12758
|
+
else beginPan(event);
|
|
12759
|
+
},
|
|
12652
12760
|
onKeyDown: (event) => {
|
|
12653
12761
|
const modifier = event.metaKey || event.ctrlKey;
|
|
12654
12762
|
if (modifier && (event.key === "a" || event.key === "A")) {
|
|
@@ -12658,6 +12766,7 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12658
12766
|
}
|
|
12659
12767
|
if (event.key === "Escape") {
|
|
12660
12768
|
event.preventDefault();
|
|
12769
|
+
if (marqueeRef.current) return;
|
|
12661
12770
|
setIssuesOpen(false);
|
|
12662
12771
|
clearSelection();
|
|
12663
12772
|
return;
|
|
@@ -12679,7 +12788,9 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12679
12788
|
}
|
|
12680
12789
|
},
|
|
12681
12790
|
className: cn54(
|
|
12682
|
-
"relative min-h-0 min-w-0 flex-1
|
|
12791
|
+
"relative min-h-0 min-w-0 flex-1 overflow-hidden bg-canvas",
|
|
12792
|
+
// Held Cmd / Ctrl over the background means "drag to select".
|
|
12793
|
+
modifierHeld || marqueeBox ? "cursor-crosshair" : "cursor-grab",
|
|
12683
12794
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-blue focus-visible:ring-inset"
|
|
12684
12795
|
),
|
|
12685
12796
|
style: {
|
|
@@ -12735,7 +12846,7 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12735
12846
|
(issue) => issue.kind === "no-fallback"
|
|
12736
12847
|
);
|
|
12737
12848
|
const active = traversed.has(node.id);
|
|
12738
|
-
const isSelected = selectionSet.has(node.id);
|
|
12849
|
+
const isSelected = selectionSet.has(node.id) || marqueeSet.has(node.id);
|
|
12739
12850
|
return /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(
|
|
12740
12851
|
"div",
|
|
12741
12852
|
{
|
|
@@ -12793,6 +12904,25 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12793
12904
|
]
|
|
12794
12905
|
}
|
|
12795
12906
|
),
|
|
12907
|
+
/* @__PURE__ */ (0, import_jsx_runtime62.jsx)(
|
|
12908
|
+
"div",
|
|
12909
|
+
{
|
|
12910
|
+
"aria-hidden": "true",
|
|
12911
|
+
"data-state": marqueeBox ? "active" : "idle",
|
|
12912
|
+
className: cn54(
|
|
12913
|
+
"pointer-events-none absolute rounded-xs border border-dashed border-primary bg-primary/10",
|
|
12914
|
+
"transition-opacity duration-150 ease-out",
|
|
12915
|
+
marqueeBox ? "opacity-100" : "opacity-0"
|
|
12916
|
+
),
|
|
12917
|
+
style: {
|
|
12918
|
+
left: marqueeBox?.x ?? 0,
|
|
12919
|
+
top: marqueeBox?.y ?? 0,
|
|
12920
|
+
width: marqueeBox?.w ?? 0,
|
|
12921
|
+
height: marqueeBox?.h ?? 0,
|
|
12922
|
+
zIndex: "var(--z-sticky, 1100)"
|
|
12923
|
+
}
|
|
12924
|
+
}
|
|
12925
|
+
),
|
|
12796
12926
|
/* @__PURE__ */ (0, import_jsx_runtime62.jsx)("ol", { className: "sr-only", children: graphEdges.map((edge) => {
|
|
12797
12927
|
const from = nodeById.get(edge.from);
|
|
12798
12928
|
const to = nodeById.get(edge.to);
|
package/dist/index.js
CHANGED
|
@@ -11972,6 +11972,9 @@ var PLANE_H = 372;
|
|
|
11972
11972
|
var PLANE_PAD = 18;
|
|
11973
11973
|
var PANEL_W = 296;
|
|
11974
11974
|
var SNAP = 8;
|
|
11975
|
+
var MARQUEE_MIN = 4;
|
|
11976
|
+
var clampTo = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
11977
|
+
var boxesIntersect = (a, b) => a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
11975
11978
|
var DEFAULT_NODES = [
|
|
11976
11979
|
{
|
|
11977
11980
|
id: "source",
|
|
@@ -12171,6 +12174,10 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12171
12174
|
selectionRef.current = selection;
|
|
12172
12175
|
const onSelectionChangeRef = React62.useRef(onSelectionChange);
|
|
12173
12176
|
onSelectionChangeRef.current = onSelectionChange;
|
|
12177
|
+
const [marqueeBox, setMarqueeBox] = React62.useState(null);
|
|
12178
|
+
const [marqueeIds, setMarqueeIds] = React62.useState(null);
|
|
12179
|
+
const marqueeRef = React62.useRef(null);
|
|
12180
|
+
const [modifierHeld, setModifierHeld] = React62.useState(false);
|
|
12174
12181
|
const rootRef = React62.useRef(null);
|
|
12175
12182
|
const planeRef = React62.useRef(null);
|
|
12176
12183
|
const triggerRef = React62.useRef(null);
|
|
@@ -12182,6 +12189,18 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12182
12189
|
React62.useEffect(() => {
|
|
12183
12190
|
setLocalNodes(incomingNodes);
|
|
12184
12191
|
}, [incomingNodes]);
|
|
12192
|
+
React62.useEffect(() => {
|
|
12193
|
+
const syncModifier = (event) => setModifierHeld(Boolean(event.metaKey || event.ctrlKey));
|
|
12194
|
+
const releaseModifier = () => setModifierHeld(false);
|
|
12195
|
+
window.addEventListener("keydown", syncModifier);
|
|
12196
|
+
window.addEventListener("keyup", syncModifier);
|
|
12197
|
+
window.addEventListener("blur", releaseModifier);
|
|
12198
|
+
return () => {
|
|
12199
|
+
window.removeEventListener("keydown", syncModifier);
|
|
12200
|
+
window.removeEventListener("keyup", syncModifier);
|
|
12201
|
+
window.removeEventListener("blur", releaseModifier);
|
|
12202
|
+
};
|
|
12203
|
+
}, []);
|
|
12185
12204
|
React62.useEffect(() => {
|
|
12186
12205
|
const plane = planeRef.current;
|
|
12187
12206
|
if (!plane) return;
|
|
@@ -12251,6 +12270,7 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12251
12270
|
});
|
|
12252
12271
|
const focusedNode = panelNodeId ? nodeById.get(panelNodeId) : void 0;
|
|
12253
12272
|
const selectionSet = React62.useMemo(() => new Set(selection), [selection]);
|
|
12273
|
+
const marqueeSet = React62.useMemo(() => new Set(marqueeIds ?? []), [marqueeIds]);
|
|
12254
12274
|
const commitSelection = React62.useCallback(
|
|
12255
12275
|
(next) => {
|
|
12256
12276
|
const unique = next.filter((id, index) => next.indexOf(id) === index);
|
|
@@ -12394,6 +12414,91 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12394
12414
|
window.addEventListener("pointermove", move);
|
|
12395
12415
|
window.addEventListener("pointerup", end, { once: true });
|
|
12396
12416
|
};
|
|
12417
|
+
const beginMarquee = (event) => {
|
|
12418
|
+
if (event.button !== 0) return;
|
|
12419
|
+
if (nodeDragActive.current) return;
|
|
12420
|
+
const plane = planeRef.current;
|
|
12421
|
+
if (!plane) return;
|
|
12422
|
+
const box = plane.getBoundingClientRect();
|
|
12423
|
+
if (!box.width || !box.height) return;
|
|
12424
|
+
const gestureScale = fit * zoom || 1;
|
|
12425
|
+
const originX = clampTo(event.clientX - box.left, 0, box.width);
|
|
12426
|
+
const originY = clampTo(event.clientY - box.top, 0, box.height);
|
|
12427
|
+
const gesture = {
|
|
12428
|
+
originX,
|
|
12429
|
+
originY,
|
|
12430
|
+
box: { left: box.left, top: box.top, width: box.width, height: box.height },
|
|
12431
|
+
offsetX: PLANE_PAD + pan.x,
|
|
12432
|
+
offsetY: Math.max((planeHeight - SCENE_H * gestureScale) / 2, 0) + pan.y,
|
|
12433
|
+
scale: gestureScale,
|
|
12434
|
+
base: selectionRef.current,
|
|
12435
|
+
rect: { x: originX, y: originY, w: 0, h: 0 }
|
|
12436
|
+
};
|
|
12437
|
+
marqueeRef.current = gesture;
|
|
12438
|
+
setMarqueeBox(gesture.rect);
|
|
12439
|
+
setMarqueeIds([]);
|
|
12440
|
+
const hitsFor = (rect) => {
|
|
12441
|
+
const scene = {
|
|
12442
|
+
x: (rect.x - gesture.offsetX) / gesture.scale,
|
|
12443
|
+
y: (rect.y - gesture.offsetY) / gesture.scale,
|
|
12444
|
+
w: rect.w / gesture.scale,
|
|
12445
|
+
h: rect.h / gesture.scale
|
|
12446
|
+
};
|
|
12447
|
+
return nodesRef.current.filter(
|
|
12448
|
+
(node) => boxesIntersect(scene, {
|
|
12449
|
+
x: node.x,
|
|
12450
|
+
y: node.y,
|
|
12451
|
+
w: NODE_W,
|
|
12452
|
+
h: node.h ?? NODE_H
|
|
12453
|
+
})
|
|
12454
|
+
).map((node) => node.id);
|
|
12455
|
+
};
|
|
12456
|
+
const cleanup = () => {
|
|
12457
|
+
window.removeEventListener("pointermove", move);
|
|
12458
|
+
window.removeEventListener("pointerup", end);
|
|
12459
|
+
window.removeEventListener("pointercancel", cancel);
|
|
12460
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
12461
|
+
};
|
|
12462
|
+
const settle = () => {
|
|
12463
|
+
cleanup();
|
|
12464
|
+
marqueeRef.current = null;
|
|
12465
|
+
setMarqueeBox(null);
|
|
12466
|
+
setMarqueeIds(null);
|
|
12467
|
+
};
|
|
12468
|
+
function cancel() {
|
|
12469
|
+
settle();
|
|
12470
|
+
}
|
|
12471
|
+
function onKeyDown(keyEvent) {
|
|
12472
|
+
if (keyEvent.key !== "Escape") return;
|
|
12473
|
+
keyEvent.preventDefault();
|
|
12474
|
+
cancel();
|
|
12475
|
+
}
|
|
12476
|
+
function move(moveEvent) {
|
|
12477
|
+
const x = clampTo(moveEvent.clientX - gesture.box.left, 0, gesture.box.width);
|
|
12478
|
+
const y = clampTo(moveEvent.clientY - gesture.box.top, 0, gesture.box.height);
|
|
12479
|
+
const rect = {
|
|
12480
|
+
x: Math.min(gesture.originX, x),
|
|
12481
|
+
y: Math.min(gesture.originY, y),
|
|
12482
|
+
w: Math.abs(x - gesture.originX),
|
|
12483
|
+
h: Math.abs(y - gesture.originY)
|
|
12484
|
+
};
|
|
12485
|
+
gesture.rect = rect;
|
|
12486
|
+
setMarqueeBox(rect);
|
|
12487
|
+
setMarqueeIds(hitsFor(rect));
|
|
12488
|
+
}
|
|
12489
|
+
function end() {
|
|
12490
|
+
const rect = gesture.rect;
|
|
12491
|
+
settle();
|
|
12492
|
+
if (Math.max(rect.w, rect.h) < MARQUEE_MIN) return;
|
|
12493
|
+
const hits = hitsFor(rect);
|
|
12494
|
+
if (hits.length === 0) return;
|
|
12495
|
+
commitSelection([...gesture.base, ...hits]);
|
|
12496
|
+
}
|
|
12497
|
+
window.addEventListener("pointermove", move);
|
|
12498
|
+
window.addEventListener("pointerup", end);
|
|
12499
|
+
window.addEventListener("pointercancel", cancel);
|
|
12500
|
+
window.addEventListener("keydown", onKeyDown);
|
|
12501
|
+
};
|
|
12397
12502
|
const moveFocus = (node, key) => {
|
|
12398
12503
|
const column = localNodes.filter((item) => Math.abs(item.x - node.x) < 1).sort((a, b) => a.y - b.y);
|
|
12399
12504
|
let target;
|
|
@@ -12512,7 +12617,10 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12512
12617
|
"aria-label": "Payment routing graph",
|
|
12513
12618
|
"aria-describedby": "routing-selection-status",
|
|
12514
12619
|
tabIndex: 0,
|
|
12515
|
-
onPointerDown:
|
|
12620
|
+
onPointerDown: (event) => {
|
|
12621
|
+
if (event.metaKey || event.ctrlKey) beginMarquee(event);
|
|
12622
|
+
else beginPan(event);
|
|
12623
|
+
},
|
|
12516
12624
|
onKeyDown: (event) => {
|
|
12517
12625
|
const modifier = event.metaKey || event.ctrlKey;
|
|
12518
12626
|
if (modifier && (event.key === "a" || event.key === "A")) {
|
|
@@ -12522,6 +12630,7 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12522
12630
|
}
|
|
12523
12631
|
if (event.key === "Escape") {
|
|
12524
12632
|
event.preventDefault();
|
|
12633
|
+
if (marqueeRef.current) return;
|
|
12525
12634
|
setIssuesOpen(false);
|
|
12526
12635
|
clearSelection();
|
|
12527
12636
|
return;
|
|
@@ -12543,7 +12652,9 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12543
12652
|
}
|
|
12544
12653
|
},
|
|
12545
12654
|
className: cn54(
|
|
12546
|
-
"relative min-h-0 min-w-0 flex-1
|
|
12655
|
+
"relative min-h-0 min-w-0 flex-1 overflow-hidden bg-canvas",
|
|
12656
|
+
// Held Cmd / Ctrl over the background means "drag to select".
|
|
12657
|
+
modifierHeld || marqueeBox ? "cursor-crosshair" : "cursor-grab",
|
|
12547
12658
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-blue focus-visible:ring-inset"
|
|
12548
12659
|
),
|
|
12549
12660
|
style: {
|
|
@@ -12599,7 +12710,7 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12599
12710
|
(issue) => issue.kind === "no-fallback"
|
|
12600
12711
|
);
|
|
12601
12712
|
const active = traversed.has(node.id);
|
|
12602
|
-
const isSelected = selectionSet.has(node.id);
|
|
12713
|
+
const isSelected = selectionSet.has(node.id) || marqueeSet.has(node.id);
|
|
12603
12714
|
return /* @__PURE__ */ jsx62(
|
|
12604
12715
|
"div",
|
|
12605
12716
|
{
|
|
@@ -12657,6 +12768,25 @@ var PaymentRoutingCanvas = React62.forwardRef(
|
|
|
12657
12768
|
]
|
|
12658
12769
|
}
|
|
12659
12770
|
),
|
|
12771
|
+
/* @__PURE__ */ jsx62(
|
|
12772
|
+
"div",
|
|
12773
|
+
{
|
|
12774
|
+
"aria-hidden": "true",
|
|
12775
|
+
"data-state": marqueeBox ? "active" : "idle",
|
|
12776
|
+
className: cn54(
|
|
12777
|
+
"pointer-events-none absolute rounded-xs border border-dashed border-primary bg-primary/10",
|
|
12778
|
+
"transition-opacity duration-150 ease-out",
|
|
12779
|
+
marqueeBox ? "opacity-100" : "opacity-0"
|
|
12780
|
+
),
|
|
12781
|
+
style: {
|
|
12782
|
+
left: marqueeBox?.x ?? 0,
|
|
12783
|
+
top: marqueeBox?.y ?? 0,
|
|
12784
|
+
width: marqueeBox?.w ?? 0,
|
|
12785
|
+
height: marqueeBox?.h ?? 0,
|
|
12786
|
+
zIndex: "var(--z-sticky, 1100)"
|
|
12787
|
+
}
|
|
12788
|
+
}
|
|
12789
|
+
),
|
|
12660
12790
|
/* @__PURE__ */ jsx62("ol", { className: "sr-only", children: graphEdges.map((edge) => {
|
|
12661
12791
|
const from = nodeById.get(edge.from);
|
|
12662
12792
|
const to = nodeById.get(edge.to);
|
package/dist/styles.css
CHANGED
package/package.json
CHANGED
|
@@ -29,6 +29,42 @@ const PLANE_PAD = 18;
|
|
|
29
29
|
const PANEL_W = 296;
|
|
30
30
|
/** Drag and keyboard nudge both land on this grid. */
|
|
31
31
|
const SNAP = 8;
|
|
32
|
+
/** A rubber-band narrower than this on both axes is a background click. */
|
|
33
|
+
const MARQUEE_MIN = 4;
|
|
34
|
+
|
|
35
|
+
/** A rectangle in whatever space the caller is working in. */
|
|
36
|
+
interface Box {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
w: number;
|
|
40
|
+
h: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const clampTo = (value: number, min: number, max: number) =>
|
|
44
|
+
Math.min(Math.max(value, min), max);
|
|
45
|
+
|
|
46
|
+
const boxesIntersect = (a: Box, b: Box) =>
|
|
47
|
+
a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One rubber-band gesture. The plane transform is captured at pointer-down so
|
|
51
|
+
* the screen-space rectangle can be mapped back into scene coordinates at any
|
|
52
|
+
* zoom / pan level without re-reading layout mid-drag.
|
|
53
|
+
*/
|
|
54
|
+
interface MarqueeGesture {
|
|
55
|
+
/** Pointer-down point, in plane-local screen coordinates. */
|
|
56
|
+
originX: number;
|
|
57
|
+
originY: number;
|
|
58
|
+
box: { left: number; top: number; width: number; height: number };
|
|
59
|
+
/** Plane-local x of the scene origin. */
|
|
60
|
+
offsetX: number;
|
|
61
|
+
/** Plane-local y of the scene origin. */
|
|
62
|
+
offsetY: number;
|
|
63
|
+
scale: number;
|
|
64
|
+
/** Selection the marquee is added to on release. */
|
|
65
|
+
base: string[];
|
|
66
|
+
rect: Box;
|
|
67
|
+
}
|
|
32
68
|
|
|
33
69
|
export interface CanvasNode {
|
|
34
70
|
id: string;
|
|
@@ -334,6 +370,14 @@ export const PaymentRoutingCanvas = React.forwardRef<
|
|
|
334
370
|
const onSelectionChangeRef = React.useRef(onSelectionChange);
|
|
335
371
|
onSelectionChangeRef.current = onSelectionChange;
|
|
336
372
|
|
|
373
|
+
/** The rubber-band rectangle, in plane-local screen coordinates. */
|
|
374
|
+
const [marqueeBox, setMarqueeBox] = React.useState<Box | null>(null);
|
|
375
|
+
/** Live preview of what the rubber-band currently covers. */
|
|
376
|
+
const [marqueeIds, setMarqueeIds] = React.useState<string[] | null>(null);
|
|
377
|
+
const marqueeRef = React.useRef<MarqueeGesture | null>(null);
|
|
378
|
+
/** True while Cmd / Ctrl is held, so the plane can show a crosshair. */
|
|
379
|
+
const [modifierHeld, setModifierHeld] = React.useState(false);
|
|
380
|
+
|
|
337
381
|
const rootRef = React.useRef<HTMLDivElement>(null);
|
|
338
382
|
const planeRef = React.useRef<HTMLDivElement>(null);
|
|
339
383
|
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
@@ -348,6 +392,20 @@ export const PaymentRoutingCanvas = React.forwardRef<
|
|
|
348
392
|
setLocalNodes(incomingNodes);
|
|
349
393
|
}, [incomingNodes]);
|
|
350
394
|
|
|
395
|
+
React.useEffect(() => {
|
|
396
|
+
const syncModifier = (event: KeyboardEvent) =>
|
|
397
|
+
setModifierHeld(Boolean(event.metaKey || event.ctrlKey));
|
|
398
|
+
const releaseModifier = () => setModifierHeld(false);
|
|
399
|
+
window.addEventListener("keydown", syncModifier);
|
|
400
|
+
window.addEventListener("keyup", syncModifier);
|
|
401
|
+
window.addEventListener("blur", releaseModifier);
|
|
402
|
+
return () => {
|
|
403
|
+
window.removeEventListener("keydown", syncModifier);
|
|
404
|
+
window.removeEventListener("keyup", syncModifier);
|
|
405
|
+
window.removeEventListener("blur", releaseModifier);
|
|
406
|
+
};
|
|
407
|
+
}, []);
|
|
408
|
+
|
|
351
409
|
React.useEffect(() => {
|
|
352
410
|
const plane = planeRef.current;
|
|
353
411
|
if (!plane) return;
|
|
@@ -428,6 +486,8 @@ export const PaymentRoutingCanvas = React.forwardRef<
|
|
|
428
486
|
|
|
429
487
|
const focusedNode = panelNodeId ? nodeById.get(panelNodeId) : undefined;
|
|
430
488
|
const selectionSet = React.useMemo(() => new Set(selection), [selection]);
|
|
489
|
+
/** Nodes under the rubber-band preview as selected before the release. */
|
|
490
|
+
const marqueeSet = React.useMemo(() => new Set(marqueeIds ?? []), [marqueeIds]);
|
|
431
491
|
|
|
432
492
|
/** Single funnel for selection writes — keeps the controlled path honest. */
|
|
433
493
|
const commitSelection = React.useCallback(
|
|
@@ -606,6 +666,118 @@ export const PaymentRoutingCanvas = React.forwardRef<
|
|
|
606
666
|
window.addEventListener("pointerup", end, { once: true });
|
|
607
667
|
};
|
|
608
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Rubber-band selection. Cmd (macOS) / Ctrl elsewhere plus a pointer-down on
|
|
671
|
+
* empty plane background draws a rectangle instead of panning. The gesture
|
|
672
|
+
* tracks through window listeners so it keeps following the pointer outside
|
|
673
|
+
* the plane, with the drawn rectangle clamped to the plane bounds.
|
|
674
|
+
*/
|
|
675
|
+
const beginMarquee = (event: React.PointerEvent) => {
|
|
676
|
+
if (event.button !== 0) return;
|
|
677
|
+
// A node gesture owns the pointer — never start a marquee over it.
|
|
678
|
+
if (nodeDragActive.current) return;
|
|
679
|
+
const plane = planeRef.current;
|
|
680
|
+
if (!plane) return;
|
|
681
|
+
const box = plane.getBoundingClientRect();
|
|
682
|
+
if (!box.width || !box.height) return;
|
|
683
|
+
|
|
684
|
+
const gestureScale = fit * zoom || 1;
|
|
685
|
+
const originX = clampTo(event.clientX - box.left, 0, box.width);
|
|
686
|
+
const originY = clampTo(event.clientY - box.top, 0, box.height);
|
|
687
|
+
|
|
688
|
+
const gesture: MarqueeGesture = {
|
|
689
|
+
originX,
|
|
690
|
+
originY,
|
|
691
|
+
box: { left: box.left, top: box.top, width: box.width, height: box.height },
|
|
692
|
+
offsetX: PLANE_PAD + pan.x,
|
|
693
|
+
offsetY: Math.max((planeHeight - SCENE_H * gestureScale) / 2, 0) + pan.y,
|
|
694
|
+
scale: gestureScale,
|
|
695
|
+
base: selectionRef.current,
|
|
696
|
+
rect: { x: originX, y: originY, w: 0, h: 0 },
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
marqueeRef.current = gesture;
|
|
700
|
+
setMarqueeBox(gesture.rect);
|
|
701
|
+
setMarqueeIds([]);
|
|
702
|
+
|
|
703
|
+
/** Screen-space rectangle → the scene-space ids whose card box it covers. */
|
|
704
|
+
const hitsFor = (rect: Box) => {
|
|
705
|
+
const scene: Box = {
|
|
706
|
+
x: (rect.x - gesture.offsetX) / gesture.scale,
|
|
707
|
+
y: (rect.y - gesture.offsetY) / gesture.scale,
|
|
708
|
+
w: rect.w / gesture.scale,
|
|
709
|
+
h: rect.h / gesture.scale,
|
|
710
|
+
};
|
|
711
|
+
return nodesRef.current
|
|
712
|
+
.filter((node) =>
|
|
713
|
+
boxesIntersect(scene, {
|
|
714
|
+
x: node.x,
|
|
715
|
+
y: node.y,
|
|
716
|
+
w: NODE_W,
|
|
717
|
+
h: node.h ?? NODE_H,
|
|
718
|
+
}),
|
|
719
|
+
)
|
|
720
|
+
.map((node) => node.id);
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
const cleanup = () => {
|
|
724
|
+
window.removeEventListener("pointermove", move);
|
|
725
|
+
window.removeEventListener("pointerup", end);
|
|
726
|
+
window.removeEventListener("pointercancel", cancel);
|
|
727
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const settle = () => {
|
|
731
|
+
cleanup();
|
|
732
|
+
marqueeRef.current = null;
|
|
733
|
+
setMarqueeBox(null);
|
|
734
|
+
setMarqueeIds(null);
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
function cancel() {
|
|
738
|
+
// Escape abandons the gesture. Nothing was committed during the drag, so
|
|
739
|
+
// the selection from before the marquee is still in place.
|
|
740
|
+
settle();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function onKeyDown(keyEvent: KeyboardEvent) {
|
|
744
|
+
if (keyEvent.key !== "Escape") return;
|
|
745
|
+
keyEvent.preventDefault();
|
|
746
|
+
cancel();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function move(moveEvent: PointerEvent) {
|
|
750
|
+
const x = clampTo(moveEvent.clientX - gesture.box.left, 0, gesture.box.width);
|
|
751
|
+
const y = clampTo(moveEvent.clientY - gesture.box.top, 0, gesture.box.height);
|
|
752
|
+
const rect: Box = {
|
|
753
|
+
x: Math.min(gesture.originX, x),
|
|
754
|
+
y: Math.min(gesture.originY, y),
|
|
755
|
+
w: Math.abs(x - gesture.originX),
|
|
756
|
+
h: Math.abs(y - gesture.originY),
|
|
757
|
+
};
|
|
758
|
+
gesture.rect = rect;
|
|
759
|
+
setMarqueeBox(rect);
|
|
760
|
+
setMarqueeIds(hitsFor(rect));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function end() {
|
|
764
|
+
const rect = gesture.rect;
|
|
765
|
+
settle();
|
|
766
|
+
// Too small to be a marquee — treat it as a click on the background and
|
|
767
|
+
// leave the selection exactly as it was.
|
|
768
|
+
if (Math.max(rect.w, rect.h) < MARQUEE_MIN) return;
|
|
769
|
+
const hits = hitsFor(rect);
|
|
770
|
+
if (hits.length === 0) return;
|
|
771
|
+
// Cmd / Ctrl marquee is additive: extend what was already selected.
|
|
772
|
+
commitSelection([...gesture.base, ...hits]);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
window.addEventListener("pointermove", move);
|
|
776
|
+
window.addEventListener("pointerup", end);
|
|
777
|
+
window.addEventListener("pointercancel", cancel);
|
|
778
|
+
window.addEventListener("keydown", onKeyDown);
|
|
779
|
+
};
|
|
780
|
+
|
|
609
781
|
const moveFocus = (node: CanvasNode, key: string) => {
|
|
610
782
|
const column = localNodes
|
|
611
783
|
.filter((item) => Math.abs(item.x - node.x) < 1)
|
|
@@ -733,7 +905,12 @@ export const PaymentRoutingCanvas = React.forwardRef<
|
|
|
733
905
|
aria-label="Payment routing graph"
|
|
734
906
|
aria-describedby="routing-selection-status"
|
|
735
907
|
tabIndex={0}
|
|
736
|
-
onPointerDown={
|
|
908
|
+
onPointerDown={(event) => {
|
|
909
|
+
// Cmd / Ctrl on empty background draws a rubber band; a plain
|
|
910
|
+
// drag keeps panning the plane.
|
|
911
|
+
if (event.metaKey || event.ctrlKey) beginMarquee(event);
|
|
912
|
+
else beginPan(event);
|
|
913
|
+
}}
|
|
737
914
|
onKeyDown={(event) => {
|
|
738
915
|
const modifier = event.metaKey || event.ctrlKey;
|
|
739
916
|
if (modifier && (event.key === "a" || event.key === "A")) {
|
|
@@ -743,6 +920,9 @@ export const PaymentRoutingCanvas = React.forwardRef<
|
|
|
743
920
|
}
|
|
744
921
|
if (event.key === "Escape") {
|
|
745
922
|
event.preventDefault();
|
|
923
|
+
// A live marquee cancels itself and restores the selection it
|
|
924
|
+
// started from — do not also clear the selection here.
|
|
925
|
+
if (marqueeRef.current) return;
|
|
746
926
|
setIssuesOpen(false);
|
|
747
927
|
clearSelection();
|
|
748
928
|
return;
|
|
@@ -770,7 +950,9 @@ export const PaymentRoutingCanvas = React.forwardRef<
|
|
|
770
950
|
}
|
|
771
951
|
}}
|
|
772
952
|
className={cn(
|
|
773
|
-
"relative min-h-0 min-w-0 flex-1
|
|
953
|
+
"relative min-h-0 min-w-0 flex-1 overflow-hidden bg-canvas",
|
|
954
|
+
// Held Cmd / Ctrl over the background means "drag to select".
|
|
955
|
+
modifierHeld || marqueeBox ? "cursor-crosshair" : "cursor-grab",
|
|
774
956
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-blue focus-visible:ring-inset",
|
|
775
957
|
)}
|
|
776
958
|
style={{
|
|
@@ -840,7 +1022,7 @@ export const PaymentRoutingCanvas = React.forwardRef<
|
|
|
840
1022
|
(issue) => issue.kind === "no-fallback",
|
|
841
1023
|
);
|
|
842
1024
|
const active = traversed.has(node.id);
|
|
843
|
-
const isSelected = selectionSet.has(node.id);
|
|
1025
|
+
const isSelected = selectionSet.has(node.id) || marqueeSet.has(node.id);
|
|
844
1026
|
|
|
845
1027
|
return (
|
|
846
1028
|
<div
|
|
@@ -912,6 +1094,24 @@ export const PaymentRoutingCanvas = React.forwardRef<
|
|
|
912
1094
|
})}
|
|
913
1095
|
</div>
|
|
914
1096
|
|
|
1097
|
+
{/* Rubber band — screen space, above the edges and the cards. */}
|
|
1098
|
+
<div
|
|
1099
|
+
aria-hidden="true"
|
|
1100
|
+
data-state={marqueeBox ? "active" : "idle"}
|
|
1101
|
+
className={cn(
|
|
1102
|
+
"pointer-events-none absolute rounded-xs border border-dashed border-primary bg-primary/10",
|
|
1103
|
+
"transition-opacity duration-150 ease-out",
|
|
1104
|
+
marqueeBox ? "opacity-100" : "opacity-0",
|
|
1105
|
+
)}
|
|
1106
|
+
style={{
|
|
1107
|
+
left: marqueeBox?.x ?? 0,
|
|
1108
|
+
top: marqueeBox?.y ?? 0,
|
|
1109
|
+
width: marqueeBox?.w ?? 0,
|
|
1110
|
+
height: marqueeBox?.h ?? 0,
|
|
1111
|
+
zIndex: "var(--z-sticky, 1100)",
|
|
1112
|
+
}}
|
|
1113
|
+
/>
|
|
1114
|
+
|
|
915
1115
|
<ol className="sr-only">
|
|
916
1116
|
{graphEdges.map((edge) => {
|
|
917
1117
|
const from = nodeById.get(edge.from);
|
|
@@ -21,6 +21,11 @@ export default function Preview() {
|
|
|
21
21
|
node moves the whole group. Cmd/Ctrl + A selects every node, Shift + arrows
|
|
22
22
|
nudge the selection, and Escape or a click on empty canvas clears it.
|
|
23
23
|
</p>
|
|
24
|
+
<p className="text-caption text-muted">
|
|
25
|
+
Hold Cmd (macOS) or Ctrl, then drag from empty background to draw a
|
|
26
|
+
selection rectangle — every card it touches is added to the selection on
|
|
27
|
+
release. Escape during the drag cancels it.
|
|
28
|
+
</p>
|
|
24
29
|
</div>
|
|
25
30
|
|
|
26
31
|
<PaymentRoutingCanvas
|