@sentropic/design-system-svelte 0.14.0 → 0.16.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/ForceGraph.svelte +239 -2
- package/dist/ForceGraph.svelte.d.ts +32 -0
- package/dist/ForceGraph.svelte.d.ts.map +1 -1
- package/dist/Popper.svelte +317 -0
- package/dist/Popper.svelte.d.ts +70 -0
- package/dist/Popper.svelte.d.ts.map +1 -0
- package/dist/Portal.svelte +80 -0
- package/dist/Portal.svelte.d.ts +22 -0
- package/dist/Portal.svelte.d.ts.map +1 -0
- package/dist/SelectableList.svelte +186 -0
- package/dist/SelectableList.svelte.d.ts +30 -0
- package/dist/SelectableList.svelte.d.ts.map +1 -0
- package/dist/SelectableRow.svelte +291 -0
- package/dist/SelectableRow.svelte.d.ts +63 -0
- package/dist/SelectableRow.svelte.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/package.json +1 -1
package/dist/ForceGraph.svelte
CHANGED
|
@@ -249,6 +249,30 @@
|
|
|
249
249
|
* null when the hover/focus ends. Intended for syncing an external panel.
|
|
250
250
|
*/
|
|
251
251
|
onNodeHover?: (node: ForceGraphNode | null) => void;
|
|
252
|
+
/**
|
|
253
|
+
* Reconciliation merge animation (CLIENT-ONLY — driven by `$effect`, which
|
|
254
|
+
* never runs during SSR, so no merge is animated or resolved server-side).
|
|
255
|
+
*
|
|
256
|
+
* Pass a `{ id, from, into }` where both `from` and `into` exist in `nodes`:
|
|
257
|
+
* the `from` node animates toward the position of `into` while fading out
|
|
258
|
+
* (the node and its incident edges), then `onMergeComplete(pair)` fires
|
|
259
|
+
* exactly ONCE for that `id`. Purely visual — the component never mutates the
|
|
260
|
+
* data; the consumer removes `from` from `nodes` after the callback.
|
|
261
|
+
*
|
|
262
|
+
* Idempotent on `id`: re-passing the SAME `id` (even with a new identity for
|
|
263
|
+
* the object) is a no-op — the animation/callback are not replayed. Passing a
|
|
264
|
+
* NEW `id` (re)plays the merge, even for the same `from`/`into` pair. After
|
|
265
|
+
* completion the `from` node stays MASKED (hidden) until the consumer removes
|
|
266
|
+
* it or a new `mergePair` is supplied, so it does not flash back when the
|
|
267
|
+
* prop returns to null. Pass null (the default) for no merge in flight.
|
|
268
|
+
*/
|
|
269
|
+
mergePair?: { id: string; from: string; into: string } | null;
|
|
270
|
+
/**
|
|
271
|
+
* Fired once the merge animation for the current `mergePair` completes (or
|
|
272
|
+
* immediately, on a microtask, under reduced motion). Fires at most ONCE per
|
|
273
|
+
* `id`. Receives the same pair so the consumer can drop `from` from the data.
|
|
274
|
+
*/
|
|
275
|
+
onMergeComplete?: (pair: { id: string; from: string; into: string }) => void;
|
|
252
276
|
class?: string;
|
|
253
277
|
};
|
|
254
278
|
|
|
@@ -270,6 +294,8 @@
|
|
|
270
294
|
edgeCurve = 0.15,
|
|
271
295
|
repulsion = 1,
|
|
272
296
|
onNodeHover,
|
|
297
|
+
mergePair = null,
|
|
298
|
+
onMergeComplete,
|
|
273
299
|
class: className
|
|
274
300
|
}: ForceGraphProps = $props();
|
|
275
301
|
|
|
@@ -320,6 +346,26 @@
|
|
|
320
346
|
};
|
|
321
347
|
}
|
|
322
348
|
|
|
349
|
+
// Stable seed from the SET of node ids (sorted), not from ns.length/es.length.
|
|
350
|
+
// A length-based seed reshuffled the whole layout whenever a node was added or
|
|
351
|
+
// removed (notably after a reconciliation merge), making the graph "jump". A
|
|
352
|
+
// hash over the sorted ids keeps the same topology → same layout, so removing
|
|
353
|
+
// one node leaves the rest essentially in place. (FNV-1a 32-bit over the joined
|
|
354
|
+
// sorted ids; deterministic and order-independent.)
|
|
355
|
+
function stableSeed(ns: ForceGraphNode[]): number {
|
|
356
|
+
const ids = ns.map((n) => n.id).sort();
|
|
357
|
+
let h = 0x811c9dc5; // FNV offset basis
|
|
358
|
+
const joined = ids.join("|");
|
|
359
|
+
for (let i = 0; i < joined.length; i++) {
|
|
360
|
+
h ^= joined.charCodeAt(i);
|
|
361
|
+
h = Math.imul(h, 0x01000193); // FNV prime
|
|
362
|
+
}
|
|
363
|
+
// Fold in the count too so wholly different graphs of equal id-hash still
|
|
364
|
+
// differ, but the dominant term is the (order-independent) id hash.
|
|
365
|
+
h ^= ns.length;
|
|
366
|
+
return h >>> 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
323
369
|
function runSimulation(
|
|
324
370
|
ns: ForceGraphNode[],
|
|
325
371
|
es: ForceGraphEdge[],
|
|
@@ -330,7 +376,9 @@
|
|
|
330
376
|
): Map<string, { x: number; y: number }> {
|
|
331
377
|
const cx = w / 2;
|
|
332
378
|
const cy = h / 2;
|
|
333
|
-
|
|
379
|
+
// Seed from the stable id-set hash so adding/removing a node does not
|
|
380
|
+
// reshuffle the whole layout (same topology → same layout).
|
|
381
|
+
const rand = mulberry32(stableSeed(ns));
|
|
334
382
|
const idIndex = new Map<string, number>();
|
|
335
383
|
const sim: SimNode[] = ns.map((n, i) => {
|
|
336
384
|
idIndex.set(n.id, i);
|
|
@@ -549,6 +597,176 @@
|
|
|
549
597
|
.filter((e): e is NonNullable<typeof e> => e !== null);
|
|
550
598
|
});
|
|
551
599
|
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// Merge animation (reconciliation): when `mergePair` becomes a new valid pair,
|
|
602
|
+
// the `from` node slides toward `into` while fading out (node + incident
|
|
603
|
+
// edges), then `onMergeComplete` fires. Purely visual — never mutates props.
|
|
604
|
+
// Driven by a single $state holding per-merge progress (0→1). Under reduced
|
|
605
|
+
// motion / SSR we skip straight to completion on a microtask.
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
const MERGE_DURATION_MS = 450;
|
|
608
|
+
// ease-in-out (cubic) for a smooth glide.
|
|
609
|
+
const easeInOut = (t: number) =>
|
|
610
|
+
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
611
|
+
|
|
612
|
+
type MergeState = {
|
|
613
|
+
id: string;
|
|
614
|
+
from: string;
|
|
615
|
+
into: string;
|
|
616
|
+
/** Animation progress, 0..1. */
|
|
617
|
+
progress: number;
|
|
618
|
+
/** Pixel delta from the `from` start position to `into`, captured at start. */
|
|
619
|
+
dx: number;
|
|
620
|
+
dy: number;
|
|
621
|
+
} | null;
|
|
622
|
+
|
|
623
|
+
let mergeState = $state<MergeState>(null);
|
|
624
|
+
let mergeRaf: number | null = null;
|
|
625
|
+
// The id currently being (or already) handled, so the $effect only reacts to a
|
|
626
|
+
// genuinely NEW id. Re-passing the same id (even a fresh object) is a no-op.
|
|
627
|
+
let handledMergeId: string | null = null;
|
|
628
|
+
// The id of the `from` node to keep MASKED after a completed merge, until the
|
|
629
|
+
// consumer drops it from `nodes` (or a new pair arrives). Decouples the mask
|
|
630
|
+
// from `mergePair` returning to null (otherwise the node would flash back).
|
|
631
|
+
let maskedFromId = $state<string | null>(null);
|
|
632
|
+
// Set true on the component's own teardown so no queued microtask/frame fires a
|
|
633
|
+
// callback or touches reactive state after unmount.
|
|
634
|
+
let disposed = false;
|
|
635
|
+
|
|
636
|
+
function cancelMergeRaf() {
|
|
637
|
+
if (mergeRaf != null && typeof cancelAnimationFrame === "function") {
|
|
638
|
+
cancelAnimationFrame(mergeRaf);
|
|
639
|
+
}
|
|
640
|
+
mergeRaf = null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Component-lifetime teardown guard (separate from the per-pair effect below):
|
|
644
|
+
// marks the instance disposed and cancels any in-flight frame on unmount.
|
|
645
|
+
$effect(() => {
|
|
646
|
+
return () => {
|
|
647
|
+
disposed = true;
|
|
648
|
+
cancelMergeRaf();
|
|
649
|
+
};
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
$effect(() => {
|
|
653
|
+
const pair = mergePair;
|
|
654
|
+
const id = pair ? pair.id : null;
|
|
655
|
+
|
|
656
|
+
// Idempotent on id: same id (or still null) means nothing to (re)start. A new
|
|
657
|
+
// id always (re)plays, even for the same from/into pair.
|
|
658
|
+
if (id === handledMergeId) return;
|
|
659
|
+
handledMergeId = id;
|
|
660
|
+
|
|
661
|
+
// Tear down any in-flight animation for a previous pair.
|
|
662
|
+
cancelMergeRaf();
|
|
663
|
+
mergeState = null;
|
|
664
|
+
|
|
665
|
+
if (!pair) return;
|
|
666
|
+
|
|
667
|
+
// A genuinely new pair supersedes any lingering mask from a prior merge.
|
|
668
|
+
maskedFromId = null;
|
|
669
|
+
|
|
670
|
+
// Validate: both endpoints must currently exist.
|
|
671
|
+
const fromPos = layout.get(pair.from);
|
|
672
|
+
const intoPos = layout.get(pair.into);
|
|
673
|
+
if (!fromPos || !intoPos) return; // invalid pair: no-op, no crash, no callback
|
|
674
|
+
|
|
675
|
+
const captured = { id: pair.id, from: pair.from, into: pair.into };
|
|
676
|
+
|
|
677
|
+
const complete = () => {
|
|
678
|
+
// Keep `from` hidden until the consumer removes it (or a new pair arrives).
|
|
679
|
+
maskedFromId = captured.from;
|
|
680
|
+
onMergeComplete?.(captured);
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// Reduced motion: no animation, resolve on a microtask. Guarded so a late
|
|
684
|
+
// microtask after unmount or after a newer id took over is a no-op.
|
|
685
|
+
if (prefersReducedMotion || typeof requestAnimationFrame !== "function") {
|
|
686
|
+
queueMicrotask(() => {
|
|
687
|
+
if (disposed || handledMergeId !== id) return;
|
|
688
|
+
complete();
|
|
689
|
+
});
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const dx = intoPos.x - fromPos.x;
|
|
694
|
+
const dy = intoPos.y - fromPos.y;
|
|
695
|
+
mergeState = { id: captured.id, from: captured.from, into: captured.into, progress: 0, dx, dy };
|
|
696
|
+
|
|
697
|
+
// Anchor the clock to the FIRST frame's own timestamp. rAF timestamps and
|
|
698
|
+
// performance.now() can use different time origins (notably under jsdom), so
|
|
699
|
+
// mixing them yields a negative/garbage elapsed time. Using the frame clock
|
|
700
|
+
// for both start and elapsed keeps the easing monotonic everywhere.
|
|
701
|
+
let start: number | null = null;
|
|
702
|
+
const tick = (now: number) => {
|
|
703
|
+
// Bail cleanly if the instance went away or a newer id superseded us — no
|
|
704
|
+
// callback, no dangling state.
|
|
705
|
+
if (disposed || handledMergeId !== id) {
|
|
706
|
+
mergeRaf = null;
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
// Re-validate both endpoints every frame: if either disappears mid-flight
|
|
710
|
+
// (e.g. the consumer removed a node), cancel the glide WITHOUT firing
|
|
711
|
+
// onMergeComplete (no double-tir) and without a dangling frame.
|
|
712
|
+
if (!layout.has(captured.from) || !layout.has(captured.into)) {
|
|
713
|
+
cancelMergeRaf();
|
|
714
|
+
mergeState = null;
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (start === null) start = now;
|
|
718
|
+
const t = Math.min(1, Math.max(0, (now - start) / MERGE_DURATION_MS));
|
|
719
|
+
if (mergeState) mergeState = { ...mergeState, progress: t };
|
|
720
|
+
if (t < 1) {
|
|
721
|
+
mergeRaf = requestAnimationFrame(tick);
|
|
722
|
+
} else {
|
|
723
|
+
mergeRaf = null;
|
|
724
|
+
mergeState = null;
|
|
725
|
+
complete();
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
mergeRaf = requestAnimationFrame(tick);
|
|
729
|
+
|
|
730
|
+
return () => cancelMergeRaf();
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// ---------------------------------------------------------------------------
|
|
734
|
+
// The `from` node is hidden while it is the active merge source AND while it
|
|
735
|
+
// remains masked post-completion (until the consumer removes it). Both feed the
|
|
736
|
+
// same opacity helpers below.
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
const mergeFromId = $derived(mergeState?.from ?? null);
|
|
739
|
+
const mergeEased = $derived(mergeState ? easeInOut(mergeState.progress) : 0);
|
|
740
|
+
|
|
741
|
+
/** True when this id is the (post-merge) masked node — render it fully hidden. */
|
|
742
|
+
function isMasked(id: string): boolean {
|
|
743
|
+
return maskedFromId === id;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Extra translation applied to the merging node so it glides toward `into`. */
|
|
747
|
+
function mergeOffset(id: string): { x: number; y: number } {
|
|
748
|
+
if (!mergeState || mergeState.from !== id) return { x: 0, y: 0 };
|
|
749
|
+
return { x: mergeState.dx * mergeEased, y: mergeState.dy * mergeEased };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Opacity for a node during/after a merge. The animating `from` fades 1->0; a
|
|
754
|
+
* masked `from` (merge done, awaiting removal) stays at 0. Others unaffected.
|
|
755
|
+
*/
|
|
756
|
+
function mergeNodeOpacity(id: string): number | null {
|
|
757
|
+
if (isMasked(id)) return 0;
|
|
758
|
+
if (mergeFromId !== id) return null;
|
|
759
|
+
return 1 - mergeEased;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/** Opacity for an edge incident to the merging/masked `from` node (fades too). */
|
|
763
|
+
function mergeEdgeOpacity(e: ForceGraphEdge): number | null {
|
|
764
|
+
const fromId = mergeFromId ?? maskedFromId;
|
|
765
|
+
if (fromId == null) return null;
|
|
766
|
+
if (e.source !== fromId && e.target !== fromId) return null;
|
|
767
|
+
return isMasked(fromId) ? 0 : 1 - mergeEased;
|
|
768
|
+
}
|
|
769
|
+
|
|
552
770
|
let hoveredNodeIndex: number | null = $state(null);
|
|
553
771
|
let hoveredEdgeIndex: number | null = $state(null);
|
|
554
772
|
|
|
@@ -779,6 +997,7 @@
|
|
|
779
997
|
onmouseleave={() => { hoveredEdgeIndex = null; }}
|
|
780
998
|
/>
|
|
781
999
|
{/if}
|
|
1000
|
+
{@const mEdgeOpacity = mergeEdgeOpacity(e.edge)}
|
|
782
1001
|
{#if e.path}
|
|
783
1002
|
<path
|
|
784
1003
|
class="st-forceGraph__edge"
|
|
@@ -786,10 +1005,12 @@
|
|
|
786
1005
|
class:st-forceGraph__edge--emphasis={e.edge.emphasis}
|
|
787
1006
|
class:st-forceGraph__edge--hovered={hoveredEdgeIndex === e.i}
|
|
788
1007
|
class:st-forceGraph__edge--dim={isEdgeSelectionDimmed(e.edge) || isHoverDimmedEdge(e.edge)}
|
|
1008
|
+
class:st-forceGraph__edge--merging={mEdgeOpacity !== null}
|
|
789
1009
|
d={e.path}
|
|
790
1010
|
fill="none"
|
|
791
1011
|
stroke-dasharray={e.dashArray}
|
|
792
1012
|
stroke-width={e.strokeWidth}
|
|
1013
|
+
opacity={mEdgeOpacity}
|
|
793
1014
|
pointer-events="none"
|
|
794
1015
|
/>
|
|
795
1016
|
{:else}
|
|
@@ -799,12 +1020,14 @@
|
|
|
799
1020
|
class:st-forceGraph__edge--emphasis={e.edge.emphasis}
|
|
800
1021
|
class:st-forceGraph__edge--hovered={hoveredEdgeIndex === e.i}
|
|
801
1022
|
class:st-forceGraph__edge--dim={isEdgeSelectionDimmed(e.edge) || isHoverDimmedEdge(e.edge)}
|
|
1023
|
+
class:st-forceGraph__edge--merging={mEdgeOpacity !== null}
|
|
802
1024
|
x1={e.x1}
|
|
803
1025
|
y1={e.y1}
|
|
804
1026
|
x2={e.x2}
|
|
805
1027
|
y2={e.y2}
|
|
806
1028
|
stroke-dasharray={e.dashArray}
|
|
807
1029
|
stroke-width={e.strokeWidth}
|
|
1030
|
+
opacity={mEdgeOpacity}
|
|
808
1031
|
pointer-events="none"
|
|
809
1032
|
/>
|
|
810
1033
|
{/if}
|
|
@@ -813,12 +1036,18 @@
|
|
|
813
1036
|
|
|
814
1037
|
<g class="st-forceGraph__nodes">
|
|
815
1038
|
{#each positionedNodes as p (p.node.id)}
|
|
1039
|
+
{@const mOff = mergeOffset(p.node.id)}
|
|
1040
|
+
{@const mOpacity = mergeNodeOpacity(p.node.id)}
|
|
1041
|
+
{@const mMasked = isMasked(p.node.id)}
|
|
816
1042
|
<g
|
|
817
1043
|
class="st-forceGraph__node st-forceGraph__node--{p.tone}"
|
|
818
1044
|
class:st-forceGraph__node--dim={isHoverDimmedNode(p.node.id) || isSelectionDimmed(p.node.id)}
|
|
819
1045
|
class:st-forceGraph__node--selected={selectedSet.has(p.node.id)}
|
|
820
1046
|
class:st-forceGraph__node--focus={focusId === p.node.id}
|
|
821
|
-
|
|
1047
|
+
class:st-forceGraph__node--merging={mergeFromId === p.node.id || mMasked}
|
|
1048
|
+
aria-hidden={mMasked ? "true" : undefined}
|
|
1049
|
+
opacity={mOpacity}
|
|
1050
|
+
transform="translate({p.x + mOff.x} {p.y + mOff.y})"
|
|
822
1051
|
>
|
|
823
1052
|
{#if p.shapePath}
|
|
824
1053
|
<path
|
|
@@ -1022,6 +1251,14 @@
|
|
|
1022
1251
|
.st-forceGraph__node { transition: opacity 120ms ease; }
|
|
1023
1252
|
.st-forceGraph__node--dim { opacity: 0.3; }
|
|
1024
1253
|
|
|
1254
|
+
/* During a merge the opacity/transform are driven per-frame via rAF, so the
|
|
1255
|
+
CSS transitions are disabled to avoid a lag that would smear the glide. The
|
|
1256
|
+
fading/masked `from` node is also taken out of hit-testing so the invisible
|
|
1257
|
+
node cannot be hovered, focused or clicked. */
|
|
1258
|
+
.st-forceGraph__node--merging,
|
|
1259
|
+
.st-forceGraph__edge--merging { transition: none; }
|
|
1260
|
+
.st-forceGraph__node--merging { pointer-events: none; }
|
|
1261
|
+
|
|
1025
1262
|
.st-forceGraph__dot {
|
|
1026
1263
|
cursor: pointer;
|
|
1027
1264
|
fill-opacity: 0.9;
|
|
@@ -138,6 +138,38 @@ type ForceGraphProps = {
|
|
|
138
138
|
* null when the hover/focus ends. Intended for syncing an external panel.
|
|
139
139
|
*/
|
|
140
140
|
onNodeHover?: (node: ForceGraphNode | null) => void;
|
|
141
|
+
/**
|
|
142
|
+
* Reconciliation merge animation (CLIENT-ONLY — driven by `$effect`, which
|
|
143
|
+
* never runs during SSR, so no merge is animated or resolved server-side).
|
|
144
|
+
*
|
|
145
|
+
* Pass a `{ id, from, into }` where both `from` and `into` exist in `nodes`:
|
|
146
|
+
* the `from` node animates toward the position of `into` while fading out
|
|
147
|
+
* (the node and its incident edges), then `onMergeComplete(pair)` fires
|
|
148
|
+
* exactly ONCE for that `id`. Purely visual — the component never mutates the
|
|
149
|
+
* data; the consumer removes `from` from `nodes` after the callback.
|
|
150
|
+
*
|
|
151
|
+
* Idempotent on `id`: re-passing the SAME `id` (even with a new identity for
|
|
152
|
+
* the object) is a no-op — the animation/callback are not replayed. Passing a
|
|
153
|
+
* NEW `id` (re)plays the merge, even for the same `from`/`into` pair. After
|
|
154
|
+
* completion the `from` node stays MASKED (hidden) until the consumer removes
|
|
155
|
+
* it or a new `mergePair` is supplied, so it does not flash back when the
|
|
156
|
+
* prop returns to null. Pass null (the default) for no merge in flight.
|
|
157
|
+
*/
|
|
158
|
+
mergePair?: {
|
|
159
|
+
id: string;
|
|
160
|
+
from: string;
|
|
161
|
+
into: string;
|
|
162
|
+
} | null;
|
|
163
|
+
/**
|
|
164
|
+
* Fired once the merge animation for the current `mergePair` completes (or
|
|
165
|
+
* immediately, on a microtask, under reduced motion). Fires at most ONCE per
|
|
166
|
+
* `id`. Receives the same pair so the consumer can drop `from` from the data.
|
|
167
|
+
*/
|
|
168
|
+
onMergeComplete?: (pair: {
|
|
169
|
+
id: string;
|
|
170
|
+
from: string;
|
|
171
|
+
into: string;
|
|
172
|
+
}) => void;
|
|
141
173
|
class?: string;
|
|
142
174
|
};
|
|
143
175
|
declare const ForceGraph: import("svelte").Component<ForceGraphProps, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ForceGraph.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ForceGraph.svelte.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"ForceGraph.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ForceGraph.svelte.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import Portal from "./Portal.svelte";
|
|
4
|
+
|
|
5
|
+
export type PopperStrategy = "absolute" | "fixed";
|
|
6
|
+
|
|
7
|
+
export type PopperPlacement =
|
|
8
|
+
| "top"
|
|
9
|
+
| "bottom"
|
|
10
|
+
| "left"
|
|
11
|
+
| "right"
|
|
12
|
+
| "top-start"
|
|
13
|
+
| "top-end"
|
|
14
|
+
| "bottom-start"
|
|
15
|
+
| "bottom-end"
|
|
16
|
+
| "left-start"
|
|
17
|
+
| "left-end"
|
|
18
|
+
| "right-start"
|
|
19
|
+
| "right-end";
|
|
20
|
+
|
|
21
|
+
export type PopperSide = "top" | "bottom" | "left" | "right";
|
|
22
|
+
export type PopperAlign = "start" | "center" | "end";
|
|
23
|
+
|
|
24
|
+
export type PopperProps = {
|
|
25
|
+
/** Reference element the panel is positioned against. */
|
|
26
|
+
anchor: HTMLElement | null;
|
|
27
|
+
/** Controlled open state. When false (or no anchor) nothing renders. */
|
|
28
|
+
open?: boolean;
|
|
29
|
+
/** Wanted placement of the panel relative to the anchor. */
|
|
30
|
+
placement?: PopperPlacement;
|
|
31
|
+
/** Main-axis distance (px) between the anchor and the panel. */
|
|
32
|
+
offset?: number;
|
|
33
|
+
/** Flip to the opposite side when the panel would overflow the viewport. */
|
|
34
|
+
flip?: boolean;
|
|
35
|
+
/** Shift along the cross axis to keep the panel within the viewport. */
|
|
36
|
+
shift?: boolean;
|
|
37
|
+
/** Expose a positioned arrow element. */
|
|
38
|
+
arrow?: boolean;
|
|
39
|
+
/** CSS positioning strategy. */
|
|
40
|
+
strategy?: PopperStrategy;
|
|
41
|
+
/** Render the panel into `document.body` via a Portal. */
|
|
42
|
+
portal?: boolean;
|
|
43
|
+
/** Optional class applied to the floating panel. */
|
|
44
|
+
class?: string;
|
|
45
|
+
/** Notified whenever the resolved placement changes (after flip). */
|
|
46
|
+
onPlacementChange?: (placement: PopperPlacement) => void;
|
|
47
|
+
children?: Snippet;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Split a placement into its side and (optional) alignment. */
|
|
51
|
+
export function splitPlacement(placement: PopperPlacement): {
|
|
52
|
+
side: PopperSide;
|
|
53
|
+
align: PopperAlign;
|
|
54
|
+
} {
|
|
55
|
+
const [side, align] = placement.split("-") as [PopperSide, PopperAlign?];
|
|
56
|
+
return { side, align: align ?? "center" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Recompose a side + alignment into a placement string. */
|
|
60
|
+
export function joinPlacement(side: PopperSide, align: PopperAlign): PopperPlacement {
|
|
61
|
+
return (align === "center" ? side : `${side}-${align}`) as PopperPlacement;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const OPPOSITE: Record<PopperSide, PopperSide> = {
|
|
65
|
+
top: "bottom",
|
|
66
|
+
bottom: "top",
|
|
67
|
+
left: "right",
|
|
68
|
+
right: "left"
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type Rect = {
|
|
72
|
+
top: number;
|
|
73
|
+
left: number;
|
|
74
|
+
right: number;
|
|
75
|
+
bottom: number;
|
|
76
|
+
width: number;
|
|
77
|
+
height: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Pure geometry: compute the panel coordinates (in the chosen strategy's
|
|
82
|
+
* coordinate space) given the anchor rect, the panel size, and options.
|
|
83
|
+
* Returns the resolved placement (after flip) and the top/left coordinates,
|
|
84
|
+
* plus the arrow offset along the main edge.
|
|
85
|
+
*
|
|
86
|
+
* Coordinates are viewport-relative; callers add scroll offsets for the
|
|
87
|
+
* `absolute` strategy. No DOM access here — safe to unit test.
|
|
88
|
+
*/
|
|
89
|
+
export function computePosition(
|
|
90
|
+
anchorRect: Rect,
|
|
91
|
+
panelWidth: number,
|
|
92
|
+
panelHeight: number,
|
|
93
|
+
options: {
|
|
94
|
+
placement: PopperPlacement;
|
|
95
|
+
offset: number;
|
|
96
|
+
flip: boolean;
|
|
97
|
+
shift: boolean;
|
|
98
|
+
viewportWidth: number;
|
|
99
|
+
viewportHeight: number;
|
|
100
|
+
}
|
|
101
|
+
): { placement: PopperPlacement; top: number; left: number } {
|
|
102
|
+
const { offset, flip, shift, viewportWidth, viewportHeight } = options;
|
|
103
|
+
let { side, align } = splitPlacement(options.placement);
|
|
104
|
+
|
|
105
|
+
const place = (s: PopperSide, a: PopperAlign) => {
|
|
106
|
+
let top = 0;
|
|
107
|
+
let left = 0;
|
|
108
|
+
if (s === "top" || s === "bottom") {
|
|
109
|
+
top = s === "top" ? anchorRect.top - panelHeight - offset : anchorRect.bottom + offset;
|
|
110
|
+
if (a === "start") left = anchorRect.left;
|
|
111
|
+
else if (a === "end") left = anchorRect.right - panelWidth;
|
|
112
|
+
else left = anchorRect.left + anchorRect.width / 2 - panelWidth / 2;
|
|
113
|
+
} else {
|
|
114
|
+
left = s === "left" ? anchorRect.left - panelWidth - offset : anchorRect.right + offset;
|
|
115
|
+
if (a === "start") top = anchorRect.top;
|
|
116
|
+
else if (a === "end") top = anchorRect.bottom - panelHeight;
|
|
117
|
+
else top = anchorRect.top + anchorRect.height / 2 - panelHeight / 2;
|
|
118
|
+
}
|
|
119
|
+
return { top, left };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Flip: if the panel overflows on the chosen side, try the opposite side.
|
|
123
|
+
if (flip) {
|
|
124
|
+
const candidate = place(side, align);
|
|
125
|
+
const overflows =
|
|
126
|
+
(side === "top" && candidate.top < 0) ||
|
|
127
|
+
(side === "bottom" && candidate.top + panelHeight > viewportHeight) ||
|
|
128
|
+
(side === "left" && candidate.left < 0) ||
|
|
129
|
+
(side === "right" && candidate.left + panelWidth > viewportWidth);
|
|
130
|
+
if (overflows) {
|
|
131
|
+
const flipped = OPPOSITE[side];
|
|
132
|
+
const flippedPos = place(flipped, align);
|
|
133
|
+
const flippedOverflows =
|
|
134
|
+
(flipped === "top" && flippedPos.top < 0) ||
|
|
135
|
+
(flipped === "bottom" && flippedPos.top + panelHeight > viewportHeight) ||
|
|
136
|
+
(flipped === "left" && flippedPos.left < 0) ||
|
|
137
|
+
(flipped === "right" && flippedPos.left + panelWidth > viewportWidth);
|
|
138
|
+
// Only flip if the opposite side fits better.
|
|
139
|
+
if (!flippedOverflows) side = flipped;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let { top, left } = place(side, align);
|
|
144
|
+
|
|
145
|
+
// Shift: clamp along the cross axis so the panel stays in the viewport.
|
|
146
|
+
if (shift) {
|
|
147
|
+
if (side === "top" || side === "bottom") {
|
|
148
|
+
const max = Math.max(0, viewportWidth - panelWidth);
|
|
149
|
+
left = Math.min(Math.max(0, left), max);
|
|
150
|
+
} else {
|
|
151
|
+
const max = Math.max(0, viewportHeight - panelHeight);
|
|
152
|
+
top = Math.min(Math.max(0, top), max);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { placement: joinPlacement(side, align), top, left };
|
|
157
|
+
}
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<script lang="ts">
|
|
161
|
+
let {
|
|
162
|
+
anchor,
|
|
163
|
+
open = false,
|
|
164
|
+
placement = "bottom",
|
|
165
|
+
offset = 8,
|
|
166
|
+
flip = true,
|
|
167
|
+
shift = true,
|
|
168
|
+
arrow = false,
|
|
169
|
+
strategy = "absolute",
|
|
170
|
+
portal = true,
|
|
171
|
+
class: className,
|
|
172
|
+
onPlacementChange,
|
|
173
|
+
children
|
|
174
|
+
}: PopperProps = $props();
|
|
175
|
+
|
|
176
|
+
let panel = $state<HTMLDivElement | undefined>();
|
|
177
|
+
let top = $state(0);
|
|
178
|
+
let left = $state(0);
|
|
179
|
+
// Placement actually applied (may differ from the requested `placement`
|
|
180
|
+
// after a flip). Initialised lazily; defaults to the requested placement.
|
|
181
|
+
let flippedPlacement = $state<PopperPlacement | undefined>();
|
|
182
|
+
const resolvedPlacement = $derived(flippedPlacement ?? placement);
|
|
183
|
+
|
|
184
|
+
function reposition() {
|
|
185
|
+
if (typeof window === "undefined") return;
|
|
186
|
+
if (!open || !anchor || !panel) return;
|
|
187
|
+
|
|
188
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
189
|
+
const panelRect = panel.getBoundingClientRect();
|
|
190
|
+
|
|
191
|
+
const result = computePosition(
|
|
192
|
+
{
|
|
193
|
+
top: anchorRect.top,
|
|
194
|
+
left: anchorRect.left,
|
|
195
|
+
right: anchorRect.right,
|
|
196
|
+
bottom: anchorRect.bottom,
|
|
197
|
+
width: anchorRect.width,
|
|
198
|
+
height: anchorRect.height
|
|
199
|
+
},
|
|
200
|
+
panelRect.width,
|
|
201
|
+
panelRect.height,
|
|
202
|
+
{
|
|
203
|
+
placement,
|
|
204
|
+
offset,
|
|
205
|
+
flip,
|
|
206
|
+
shift,
|
|
207
|
+
viewportWidth: window.innerWidth,
|
|
208
|
+
viewportHeight: window.innerHeight
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// `absolute` is positioned relative to the document, so add scroll offsets.
|
|
213
|
+
// `fixed` is viewport-relative, so coordinates are used as-is.
|
|
214
|
+
if (strategy === "absolute") {
|
|
215
|
+
top = result.top + window.scrollY;
|
|
216
|
+
left = result.left + window.scrollX;
|
|
217
|
+
} else {
|
|
218
|
+
top = result.top;
|
|
219
|
+
left = result.left;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (result.placement !== resolvedPlacement) {
|
|
223
|
+
flippedPlacement = result.placement;
|
|
224
|
+
onPlacementChange?.(result.placement);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
$effect(() => {
|
|
229
|
+
// Client-only: register listeners and compute the position once mounted.
|
|
230
|
+
if (typeof window === "undefined") return;
|
|
231
|
+
if (!open || !anchor || !panel) return;
|
|
232
|
+
|
|
233
|
+
reposition();
|
|
234
|
+
|
|
235
|
+
const onScroll = () => reposition();
|
|
236
|
+
const onResize = () => reposition();
|
|
237
|
+
window.addEventListener("scroll", onScroll, true);
|
|
238
|
+
window.addEventListener("resize", onResize);
|
|
239
|
+
|
|
240
|
+
return () => {
|
|
241
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
242
|
+
window.removeEventListener("resize", onResize);
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const panelStyle = () =>
|
|
247
|
+
`position: ${strategy}; top: ${top}px; left: ${left}px;`;
|
|
248
|
+
const panelSide = () => splitPlacement(resolvedPlacement).side;
|
|
249
|
+
</script>
|
|
250
|
+
|
|
251
|
+
{#snippet floating()}
|
|
252
|
+
<div
|
|
253
|
+
bind:this={panel}
|
|
254
|
+
class={className ? `st-popper ${className}` : "st-popper"}
|
|
255
|
+
data-popper-placement={resolvedPlacement}
|
|
256
|
+
style={panelStyle()}
|
|
257
|
+
>
|
|
258
|
+
{@render children?.()}
|
|
259
|
+
{#if arrow}
|
|
260
|
+
<span
|
|
261
|
+
class="st-popper__arrow"
|
|
262
|
+
data-popper-side={panelSide()}
|
|
263
|
+
aria-hidden="true"
|
|
264
|
+
></span>
|
|
265
|
+
{/if}
|
|
266
|
+
</div>
|
|
267
|
+
{/snippet}
|
|
268
|
+
|
|
269
|
+
{#if open && anchor}
|
|
270
|
+
{#if portal}
|
|
271
|
+
<Portal target="body">
|
|
272
|
+
{@render floating()}
|
|
273
|
+
</Portal>
|
|
274
|
+
{:else}
|
|
275
|
+
{@render floating()}
|
|
276
|
+
{/if}
|
|
277
|
+
{/if}
|
|
278
|
+
|
|
279
|
+
<style>
|
|
280
|
+
.st-popper {
|
|
281
|
+
z-index: var(--st-component-popover-zIndex, 80);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.st-popper__arrow {
|
|
285
|
+
position: absolute;
|
|
286
|
+
width: 0.5rem;
|
|
287
|
+
height: 0.5rem;
|
|
288
|
+
background: inherit;
|
|
289
|
+
border: inherit;
|
|
290
|
+
transform: rotate(45deg);
|
|
291
|
+
pointer-events: none;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.st-popper__arrow[data-popper-side="bottom"] {
|
|
295
|
+
top: -0.25rem;
|
|
296
|
+
left: 50%;
|
|
297
|
+
margin-left: -0.25rem;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.st-popper__arrow[data-popper-side="top"] {
|
|
301
|
+
bottom: -0.25rem;
|
|
302
|
+
left: 50%;
|
|
303
|
+
margin-left: -0.25rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.st-popper__arrow[data-popper-side="right"] {
|
|
307
|
+
left: -0.25rem;
|
|
308
|
+
top: 50%;
|
|
309
|
+
margin-top: -0.25rem;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.st-popper__arrow[data-popper-side="left"] {
|
|
313
|
+
right: -0.25rem;
|
|
314
|
+
top: 50%;
|
|
315
|
+
margin-top: -0.25rem;
|
|
316
|
+
}
|
|
317
|
+
</style>
|