scroll-arrows 0.1.0 → 0.2.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/index.cjs CHANGED
@@ -24,20 +24,24 @@ function docRect(el) {
24
24
  height: r2.height
25
25
  };
26
26
  }
27
+ function isDegenerateRect(r2) {
28
+ return r2.width <= 0 || r2.height <= 0;
29
+ }
27
30
  function center(r2) {
28
31
  return { x: r2.left + r2.width / 2, y: r2.top + r2.height / 2 };
29
32
  }
30
- function socketPoint(r2, side) {
33
+ function socketPoint(r2, side, offset = 0) {
31
34
  const c = center(r2);
35
+ const o = offset < -0.5 ? -0.5 : offset > 0.5 ? 0.5 : offset;
32
36
  switch (side) {
33
37
  case "top":
34
- return { x: c.x, y: r2.top };
38
+ return { x: c.x + o * r2.width, y: r2.top };
35
39
  case "bottom":
36
- return { x: c.x, y: r2.top + r2.height };
40
+ return { x: c.x + o * r2.width, y: r2.top + r2.height };
37
41
  case "left":
38
- return { x: r2.left, y: c.y };
42
+ return { x: r2.left, y: c.y + o * r2.height };
39
43
  case "right":
40
- return { x: r2.left + r2.width, y: c.y };
44
+ return { x: r2.left + r2.width, y: c.y + o * r2.height };
41
45
  default:
42
46
  return c;
43
47
  }
@@ -70,12 +74,12 @@ function autoSide(self, other) {
70
74
  }
71
75
  return best;
72
76
  }
73
- function resolveEndpoints(startRect, endRect, startSocket, endSocket) {
77
+ function resolveEndpoints(startRect, endRect, startSocket, endSocket, startOffset = 0, endOffset = 0) {
74
78
  const s = startSocket === "auto" ? autoSide(startRect, endRect) : startSocket;
75
79
  const e = endSocket === "auto" ? autoSide(endRect, startRect) : endSocket;
76
80
  return {
77
- start: socketPoint(startRect, s),
78
- end: socketPoint(endRect, e),
81
+ start: socketPoint(startRect, s, startOffset),
82
+ end: socketPoint(endRect, e, endOffset),
79
83
  startNormal: socketNormal(s),
80
84
  endNormal: socketNormal(e)
81
85
  };
@@ -88,10 +92,36 @@ function buildPath(ep, curvature, belly = { x: 0, y: 0 }) {
88
92
  const reach = dist * (0.3 + curvature * 0.4);
89
93
  const sn = startNormal.x || startNormal.y ? startNormal : unit(dx, dy);
90
94
  const en = endNormal.x || endNormal.y ? endNormal : unit(-dx, -dy);
91
- const c1 = { x: start.x + sn.x * reach + belly.x, y: start.y + sn.y * reach + belly.y };
92
- const c2 = { x: end.x + en.x * reach + belly.x, y: end.y + en.y * reach + belly.y };
95
+ const c1 = {
96
+ x: start.x + sn.x * reach + belly.x,
97
+ y: start.y + sn.y * reach + belly.y
98
+ };
99
+ const c2 = {
100
+ x: end.x + en.x * reach + belly.x,
101
+ y: end.y + en.y * reach + belly.y
102
+ };
93
103
  return `M ${r(start.x)} ${r(start.y)} C ${r(c1.x)} ${r(c1.y)} ${r(c2.x)} ${r(c2.y)} ${r(end.x)} ${r(end.y)}`;
94
104
  }
105
+ function buildElbowPath(ep) {
106
+ const { start: s, end: e, startNormal: sn, endNormal: en } = ep;
107
+ const dx = e.x - s.x;
108
+ const dy = e.y - s.y;
109
+ const startVertical = sn.y !== 0 || sn.x === 0 && Math.abs(dy) >= Math.abs(dx);
110
+ const endVertical = en.y !== 0 || en.x === 0 && Math.abs(dy) >= Math.abs(dx);
111
+ let pts;
112
+ if (startVertical && endVertical) {
113
+ const midY = (s.y + e.y) / 2;
114
+ pts = [s, { x: s.x, y: midY }, { x: e.x, y: midY }, e];
115
+ } else if (!startVertical && !endVertical) {
116
+ const midX = (s.x + e.x) / 2;
117
+ pts = [s, { x: midX, y: s.y }, { x: midX, y: e.y }, e];
118
+ } else if (startVertical) {
119
+ pts = [s, { x: s.x, y: e.y }, e];
120
+ } else {
121
+ pts = [s, { x: e.x, y: s.y }, e];
122
+ }
123
+ return `M ${r(pts[0].x)} ${r(pts[0].y)}` + pts.slice(1).map((p) => ` L ${r(p.x)} ${r(p.y)}`).join("");
124
+ }
95
125
  function routeOffset(start, end, obstacles, padding = 14) {
96
126
  const dx = end.x - start.x;
97
127
  const dy = end.y - start.y;
@@ -167,12 +197,29 @@ function easeInOutCubic(t) {
167
197
  function clamp01(t) {
168
198
  return t < 0 ? 0 : t > 1 ? 1 : t;
169
199
  }
200
+ function prefersReducedMotion() {
201
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
202
+ return false;
203
+ }
204
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
205
+ }
170
206
  function scrollProgress(targetRect, range) {
171
207
  const vh = window.innerHeight || 1;
172
208
  const topFrac = (targetRect.top - window.scrollY) / vh;
173
209
  const [enter, leave] = range;
174
210
  return clamp01((enter - topFrac) / (enter - leave || 1));
175
211
  }
212
+ function staggerWindows(n, stagger) {
213
+ if (n <= 0) return [];
214
+ const s = clamp01(stagger);
215
+ const span = 1 / (1 + (n - 1) * s);
216
+ const step = span * s;
217
+ return Array.from({ length: n }, (_, i) => ({ start: i * step, span }));
218
+ }
219
+ function windowProgress(p, w) {
220
+ if (w.span <= 0) return p > w.start ? 1 : 0;
221
+ return clamp01((p - w.start) / w.span);
222
+ }
176
223
  function midpointRect(a, b) {
177
224
  const ra = docRect(a);
178
225
  const rb = docRect(b);
@@ -209,7 +256,8 @@ function getOverlay(container) {
209
256
  (_a = document.body.style).position || (_a.position = "relative");
210
257
  } else {
211
258
  const pos = getComputedStyle(container).position;
212
- if (pos === "static") container.style.position = "relative";
259
+ if (pos === "static")
260
+ container.style.position = "relative";
213
261
  }
214
262
  container.appendChild(svg);
215
263
  overlays.set(container, svg);
@@ -329,14 +377,36 @@ var ScrollArrow = class {
329
377
  this.svg = getOverlay(this.container);
330
378
  this.rc = rough__default.default.svg(this.svg);
331
379
  this.svg.appendChild(this.group);
332
- this.progress = clamp01(this.opts.progress);
380
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
381
+ this.progress = this.reducedMotion ? 1 : clamp01(this.opts.progress);
333
382
  this.refs = this.resolveRefs();
334
383
  this.stroke = options.stroke ?? getComputedStyle(this.container).color ?? "#222";
335
384
  this.seed = options.seed ?? deriveSeed(refKey(options.start), refKey(options.end));
385
+ this.enabled = this.opts.enabled ?? true;
386
+ if (!this.enabled) this.group.style.display = "none";
336
387
  this.render();
337
388
  this.bind();
338
389
  this.update();
339
390
  }
391
+ /**
392
+ * Suspend or restore the arrow without tearing it down. Disabling hides it and
393
+ * stops all draw/scroll work; enabling shows it and recomputes geometry (so it
394
+ * reflects any layout change that happened while hidden). Idempotent. Wire it
395
+ * to `matchMedia` to switch arrows off below a breakpoint.
396
+ */
397
+ setEnabled(on) {
398
+ if (on === this.enabled || this.destroyed) return;
399
+ this.enabled = on;
400
+ if (on) {
401
+ this.group.style.display = "";
402
+ this.render();
403
+ this.update();
404
+ } else {
405
+ this.group.style.display = "none";
406
+ cancelAnimationFrame(this.rafId);
407
+ this.rafId = 0;
408
+ }
409
+ }
340
410
  /** Manually set draw progress (0..1). Only meaningful when scroll is false. */
341
411
  setProgress(p) {
342
412
  this.progress = clamp01(p);
@@ -350,6 +420,7 @@ var ScrollArrow = class {
350
420
  destroy() {
351
421
  this.destroyed = true;
352
422
  this.ro?.disconnect();
423
+ this.teardownReveal();
353
424
  window.removeEventListener("scroll", this.onScroll, true);
354
425
  window.removeEventListener("resize", this.onScroll);
355
426
  cancelAnimationFrame(this.rafId);
@@ -375,17 +446,30 @@ var ScrollArrow = class {
375
446
  const list = Array.isArray(a) ? a : [a];
376
447
  return list.map(resolve).filter((el) => el !== null);
377
448
  }
378
- computeEndpoints() {
379
- const sr = docRect(this.refs.start);
380
- const er = docRect(this.refs.end);
449
+ computeEndpoints(sr, er) {
381
450
  const ss = this.opts.startSocket ?? "auto";
382
451
  const es = this.opts.endSocket ?? "auto";
383
- return resolveEndpoints(sr, er, ss, es);
452
+ return resolveEndpoints(
453
+ sr,
454
+ er,
455
+ ss,
456
+ es,
457
+ this.opts.startSocketOffset ?? 0,
458
+ this.opts.endSocketOffset ?? 0
459
+ );
384
460
  }
385
461
  render() {
386
462
  while (this.group.firstChild) this.group.removeChild(this.group.firstChild);
387
463
  this.segments = [];
388
- const ep = this.computeEndpoints();
464
+ if (!this.enabled) return;
465
+ const sr = docRect(this.refs.start);
466
+ const er = docRect(this.refs.end);
467
+ if (isDegenerateRect(sr) || isDegenerateRect(er)) {
468
+ this.armReveal();
469
+ return;
470
+ }
471
+ this.teardownReveal();
472
+ const ep = this.computeEndpoints(sr, er);
389
473
  const origin = overlayOrigin(this.svg);
390
474
  const shift = (e) => ({
391
475
  start: { x: e.start.x - origin.x, y: e.start.y - origin.y },
@@ -402,24 +486,29 @@ var ScrollArrow = class {
402
486
  this.seed,
403
487
  this.opts.anchorEnds ?? true
404
488
  );
405
- const obstacles = this.resolveAvoid().map((el) => {
406
- const dr = docRect(el);
407
- return {
408
- left: dr.left - origin.x,
409
- top: dr.top - origin.y,
410
- width: dr.width,
411
- height: dr.height
412
- };
413
- });
414
- const clear = routeOffset(
415
- local.start,
416
- local.end,
417
- obstacles,
418
- this.opts.avoidPadding ?? 14
419
- );
420
- const BOW = 1.6;
421
- const belly = { x: clear.x * BOW, y: clear.y * BOW };
422
- const d = buildPath(local, curvature, belly);
489
+ let d;
490
+ if (this.opts.route === "elbow") {
491
+ d = buildElbowPath(local);
492
+ } else {
493
+ const obstacles = this.resolveAvoid().map((el) => {
494
+ const dr = docRect(el);
495
+ return {
496
+ left: dr.left - origin.x,
497
+ top: dr.top - origin.y,
498
+ width: dr.width,
499
+ height: dr.height
500
+ };
501
+ });
502
+ const clear = routeOffset(
503
+ local.start,
504
+ local.end,
505
+ obstacles,
506
+ this.opts.avoidPadding ?? 14
507
+ );
508
+ const BOW = 1.6;
509
+ const belly = { x: clear.x * BOW, y: clear.y * BOW };
510
+ d = buildPath(local, curvature, belly);
511
+ }
423
512
  this.appendDrawable(this.rc.path(d, roughOpts), "line");
424
513
  const head = this.opts.head;
425
514
  const size = this.opts.headSize;
@@ -465,8 +554,12 @@ var ScrollArrow = class {
465
554
  let y = pt.y;
466
555
  if (offset && total > 0) {
467
556
  const eps = Math.min(1, total / 2);
468
- const before = this.lineEl.getPointAtLength(Math.max(0, at * total - eps));
469
- const after = this.lineEl.getPointAtLength(Math.min(total, at * total + eps));
557
+ const before = this.lineEl.getPointAtLength(
558
+ Math.max(0, at * total - eps)
559
+ );
560
+ const after = this.lineEl.getPointAtLength(
561
+ Math.min(total, at * total + eps)
562
+ );
470
563
  const n = unitNormal(before, after);
471
564
  x += n.x * offset;
472
565
  y += n.y * offset;
@@ -526,19 +619,37 @@ var ScrollArrow = class {
526
619
  if (this.labelBgEl) this.labelBgEl.style.opacity = String(op);
527
620
  }
528
621
  }
622
+ /**
623
+ * Watch the anchors for the moment a hidden one becomes laid out, then redraw.
624
+ * IntersectionObserver fires when a previously `display:none` element gains a
625
+ * box — a transition ResizeObserver does not reliably report. Armed lazily and
626
+ * idempotently; torn down by the next successful render(). Guarded so it no-ops
627
+ * in environments without IntersectionObserver (older engines, some SSR/jsdom).
628
+ */
629
+ armReveal() {
630
+ if (this.revealObs || this.destroyed) return;
631
+ if (typeof IntersectionObserver === "undefined") return;
632
+ this.revealObs = new IntersectionObserver(() => this.render());
633
+ this.revealObs.observe(this.refs.start);
634
+ this.revealObs.observe(this.refs.end);
635
+ }
636
+ teardownReveal() {
637
+ this.revealObs?.disconnect();
638
+ this.revealObs = void 0;
639
+ }
529
640
  bind() {
530
641
  const targets = [this.refs.start, this.refs.end, ...this.resolveAvoid()];
531
642
  if (this.refs.target) targets.push(this.refs.target);
532
643
  this.ro = new ResizeObserver(() => this.render());
533
644
  targets.forEach((t) => this.ro.observe(t));
534
- if (this.opts.scroll !== false) {
645
+ if (this.opts.scroll !== false && !this.reducedMotion) {
535
646
  window.addEventListener("scroll", this.onScroll, true);
536
647
  window.addEventListener("resize", this.onScroll);
537
648
  }
538
649
  }
539
650
  update() {
540
- if (this.destroyed) return;
541
- if (this.opts.scroll === false) {
651
+ if (this.destroyed || !this.enabled) return;
652
+ if (this.opts.scroll === false || this.reducedMotion) {
542
653
  this.applyProgress();
543
654
  return;
544
655
  }
@@ -560,13 +671,152 @@ function clampAt(t) {
560
671
  return t < 0 ? 0 : t > 1 ? 1 : t;
561
672
  }
562
673
 
674
+ // src/group.ts
675
+ var ScrollArrowGroup = class {
676
+ constructor(options) {
677
+ this.elements = [];
678
+ this.target = null;
679
+ this.progress = 0;
680
+ this.rafId = 0;
681
+ this.destroyed = false;
682
+ this.onScroll = () => {
683
+ if (this.rafId) return;
684
+ this.rafId = requestAnimationFrame(() => {
685
+ this.rafId = 0;
686
+ this.update();
687
+ });
688
+ };
689
+ if (!options.arrows || options.arrows.length === 0) {
690
+ throw new Error("[scroll-arrows] group needs at least one arrow");
691
+ }
692
+ this.opts = {
693
+ stagger: 1,
694
+ speed: 1,
695
+ easing: (t) => t,
696
+ ...options
697
+ };
698
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
699
+ this.enabled = this.opts.enabled ?? true;
700
+ this.arrows = options.arrows.map(
701
+ (a) => new ScrollArrow({
702
+ ...a,
703
+ scroll: false,
704
+ progress: 0,
705
+ respectReducedMotion: false,
706
+ enabled: this.enabled
707
+ })
708
+ );
709
+ this.windows = staggerWindows(this.arrows.length, this.opts.stagger);
710
+ for (const a of options.arrows) {
711
+ const s = resolve2(a.start);
712
+ const e = resolve2(a.end);
713
+ if (s) this.elements.push(s);
714
+ if (e) this.elements.push(e);
715
+ }
716
+ const scroll = this.opts.scroll;
717
+ if (scroll !== false && scroll?.target) {
718
+ this.target = resolve2(scroll.target);
719
+ }
720
+ if (this.reducedMotion) this.progress = 1;
721
+ this.bind();
722
+ this.update();
723
+ }
724
+ /** Manually set group progress (0..1). Only meaningful when scroll is false. */
725
+ setProgress(p) {
726
+ this.progress = clamp01(p);
727
+ this.distribute();
728
+ }
729
+ /** Recompute geometry for every arrow (call after layout changes). */
730
+ refresh() {
731
+ this.arrows.forEach((a) => a.refresh());
732
+ this.update();
733
+ }
734
+ /**
735
+ * Suspend or restore the whole group without tearing it down. Hides every
736
+ * arrow and stops scroll work when off; restores and recomputes when on.
737
+ * Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
738
+ */
739
+ setEnabled(on) {
740
+ if (on === this.enabled || this.destroyed) return;
741
+ this.enabled = on;
742
+ this.arrows.forEach((a) => a.setEnabled(on));
743
+ if (on) {
744
+ this.bind();
745
+ this.update();
746
+ } else {
747
+ window.removeEventListener("scroll", this.onScroll, true);
748
+ window.removeEventListener("resize", this.onScroll);
749
+ cancelAnimationFrame(this.rafId);
750
+ this.rafId = 0;
751
+ }
752
+ }
753
+ destroy() {
754
+ this.destroyed = true;
755
+ window.removeEventListener("scroll", this.onScroll, true);
756
+ window.removeEventListener("resize", this.onScroll);
757
+ cancelAnimationFrame(this.rafId);
758
+ this.arrows.forEach((a) => a.destroy());
759
+ }
760
+ // --- internals ---------------------------------------------------------
761
+ bind() {
762
+ if (this.opts.scroll === false || this.reducedMotion || !this.enabled)
763
+ return;
764
+ window.addEventListener("scroll", this.onScroll, true);
765
+ window.addEventListener("resize", this.onScroll);
766
+ }
767
+ update() {
768
+ if (this.destroyed || !this.enabled) return;
769
+ if (this.opts.scroll === false || this.reducedMotion) {
770
+ this.distribute();
771
+ return;
772
+ }
773
+ const scroll = this.opts.scroll ?? {};
774
+ const range = scroll.range ?? [0.85, 0.35];
775
+ const rect = this.target ? docRect(this.target) : this.groupRect();
776
+ const raw = scrollProgress(rect, range);
777
+ this.progress = clamp01(raw * this.opts.speed);
778
+ this.distribute();
779
+ }
780
+ /** Push each arrow to its sliced local progress for the current group value. */
781
+ distribute() {
782
+ const eased = clamp01(this.opts.easing(this.progress));
783
+ this.arrows.forEach((arrow, i) => {
784
+ arrow.setProgress(windowProgress(eased, this.windows[i]));
785
+ });
786
+ }
787
+ /** Synthetic rect spanning every endpoint, used as the default trigger. */
788
+ groupRect() {
789
+ const rects = this.elements.map(docRect);
790
+ if (rects.length === 0) return { left: 0, top: 0, width: 0, height: 0 };
791
+ let left = Infinity;
792
+ let top = Infinity;
793
+ let right = -Infinity;
794
+ let bottom = -Infinity;
795
+ for (const r2 of rects) {
796
+ left = Math.min(left, r2.left);
797
+ top = Math.min(top, r2.top);
798
+ right = Math.max(right, r2.left + r2.width);
799
+ bottom = Math.max(bottom, r2.top + r2.height);
800
+ }
801
+ return { left, top, width: right - left, height: bottom - top };
802
+ }
803
+ };
804
+ function resolve2(ref) {
805
+ return typeof ref === "string" ? document.querySelector(ref) : ref;
806
+ }
807
+
563
808
  // src/index.ts
564
809
  function scrollArrow(options) {
565
810
  return new ScrollArrow(options);
566
811
  }
812
+ function scrollArrowGroup(options) {
813
+ return new ScrollArrowGroup(options);
814
+ }
567
815
 
568
816
  exports.ScrollArrow = ScrollArrow;
817
+ exports.ScrollArrowGroup = ScrollArrowGroup;
569
818
  exports.easeInOutCubic = easeInOutCubic;
570
819
  exports.scrollArrow = scrollArrow;
820
+ exports.scrollArrowGroup = scrollArrowGroup;
571
821
  //# sourceMappingURL=index.cjs.map
572
822
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as ScrollArrowOptions } from './types-DehQP2Hx.cjs';
2
- export { A as ArrowHead, E as ElementRef, P as Point, a as ScrollOptions, b as Socket } from './types-DehQP2Hx.cjs';
1
+ import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-Cpvz9wtr.cjs';
2
+ export { A as ArrowHead, E as ElementRef, P as Point, b as ScrollOptions, c as Socket } from './types-Cpvz9wtr.cjs';
3
3
 
4
4
  /** A single hand-drawn arrow that draws itself between two elements on scroll. */
5
5
  declare class ScrollArrow {
@@ -22,9 +22,25 @@ declare class ScrollArrow {
22
22
  private labelBgEl;
23
23
  private progress;
24
24
  private ro?;
25
+ /**
26
+ * Lazily armed only while an anchor is hidden/zero-size, so the common path
27
+ * (both anchors visible) wires no extra observer. Fires render() on reveal.
28
+ */
29
+ private revealObs?;
25
30
  private rafId;
26
31
  private destroyed;
32
+ /** Render static + fully drawn: user prefers reduced motion and we honor it. */
33
+ private reducedMotion;
34
+ /** When false the arrow is hidden and skips all draw/scroll work (no teardown). */
35
+ private enabled;
27
36
  constructor(options: ScrollArrowOptions);
37
+ /**
38
+ * Suspend or restore the arrow without tearing it down. Disabling hides it and
39
+ * stops all draw/scroll work; enabling shows it and recomputes geometry (so it
40
+ * reflects any layout change that happened while hidden). Idempotent. Wire it
41
+ * to `matchMedia` to switch arrows off below a breakpoint.
42
+ */
43
+ setEnabled(on: boolean): void;
28
44
  /** Manually set draw progress (0..1). Only meaningful when scroll is false. */
29
45
  setProgress(p: number): void;
30
46
  /** Recompute geometry (call after layout changes you control). */
@@ -44,14 +60,64 @@ declare class ScrollArrow {
44
60
  * once the line is complete.
45
61
  */
46
62
  private applyProgress;
63
+ /**
64
+ * Watch the anchors for the moment a hidden one becomes laid out, then redraw.
65
+ * IntersectionObserver fires when a previously `display:none` element gains a
66
+ * box — a transition ResizeObserver does not reliably report. Armed lazily and
67
+ * idempotently; torn down by the next successful render(). Guarded so it no-ops
68
+ * in environments without IntersectionObserver (older engines, some SSR/jsdom).
69
+ */
70
+ private armReveal;
71
+ private teardownReveal;
72
+ private bind;
73
+ private onScroll;
74
+ private update;
75
+ }
76
+
77
+ /**
78
+ * A coordinated set of arrows that draw in a staggered sequence off one shared
79
+ * scroll trigger (or manual `setProgress`). Owns its arrows; `destroy()` tears
80
+ * them all down.
81
+ */
82
+ declare class ScrollArrowGroup {
83
+ private opts;
84
+ private arrows;
85
+ private windows;
86
+ private elements;
87
+ private target;
88
+ private progress;
89
+ private rafId;
90
+ private destroyed;
91
+ /** Render every arrow static + fully drawn when reduced motion is honored. */
92
+ private reducedMotion;
93
+ /** When false every arrow is hidden and the group does no scroll work. */
94
+ private enabled;
95
+ constructor(options: ScrollArrowGroupOptions);
96
+ /** Manually set group progress (0..1). Only meaningful when scroll is false. */
97
+ setProgress(p: number): void;
98
+ /** Recompute geometry for every arrow (call after layout changes). */
99
+ refresh(): void;
100
+ /**
101
+ * Suspend or restore the whole group without tearing it down. Hides every
102
+ * arrow and stops scroll work when off; restores and recomputes when on.
103
+ * Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
104
+ */
105
+ setEnabled(on: boolean): void;
106
+ destroy(): void;
47
107
  private bind;
48
108
  private onScroll;
49
109
  private update;
110
+ /** Push each arrow to its sliced local progress for the current group value. */
111
+ private distribute;
112
+ /** Synthetic rect spanning every endpoint, used as the default trigger. */
113
+ private groupRect;
50
114
  }
51
115
 
52
116
  declare function easeInOutCubic(t: number): number;
53
117
 
54
118
  /** Convenience factory. `const a = scrollArrow({ start, end })`. */
55
119
  declare function scrollArrow(options: ScrollArrowOptions): ScrollArrow;
120
+ /** Convenience factory. `const g = scrollArrowGroup({ arrows: [...] })`. */
121
+ declare function scrollArrowGroup(options: ScrollArrowGroupOptions): ScrollArrowGroup;
56
122
 
57
- export { ScrollArrow, ScrollArrowOptions, easeInOutCubic, scrollArrow };
123
+ export { ScrollArrow, ScrollArrowGroup, ScrollArrowGroupOptions, ScrollArrowOptions, easeInOutCubic, scrollArrow, scrollArrowGroup };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as ScrollArrowOptions } from './types-DehQP2Hx.js';
2
- export { A as ArrowHead, E as ElementRef, P as Point, a as ScrollOptions, b as Socket } from './types-DehQP2Hx.js';
1
+ import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-Cpvz9wtr.js';
2
+ export { A as ArrowHead, E as ElementRef, P as Point, b as ScrollOptions, c as Socket } from './types-Cpvz9wtr.js';
3
3
 
4
4
  /** A single hand-drawn arrow that draws itself between two elements on scroll. */
5
5
  declare class ScrollArrow {
@@ -22,9 +22,25 @@ declare class ScrollArrow {
22
22
  private labelBgEl;
23
23
  private progress;
24
24
  private ro?;
25
+ /**
26
+ * Lazily armed only while an anchor is hidden/zero-size, so the common path
27
+ * (both anchors visible) wires no extra observer. Fires render() on reveal.
28
+ */
29
+ private revealObs?;
25
30
  private rafId;
26
31
  private destroyed;
32
+ /** Render static + fully drawn: user prefers reduced motion and we honor it. */
33
+ private reducedMotion;
34
+ /** When false the arrow is hidden and skips all draw/scroll work (no teardown). */
35
+ private enabled;
27
36
  constructor(options: ScrollArrowOptions);
37
+ /**
38
+ * Suspend or restore the arrow without tearing it down. Disabling hides it and
39
+ * stops all draw/scroll work; enabling shows it and recomputes geometry (so it
40
+ * reflects any layout change that happened while hidden). Idempotent. Wire it
41
+ * to `matchMedia` to switch arrows off below a breakpoint.
42
+ */
43
+ setEnabled(on: boolean): void;
28
44
  /** Manually set draw progress (0..1). Only meaningful when scroll is false. */
29
45
  setProgress(p: number): void;
30
46
  /** Recompute geometry (call after layout changes you control). */
@@ -44,14 +60,64 @@ declare class ScrollArrow {
44
60
  * once the line is complete.
45
61
  */
46
62
  private applyProgress;
63
+ /**
64
+ * Watch the anchors for the moment a hidden one becomes laid out, then redraw.
65
+ * IntersectionObserver fires when a previously `display:none` element gains a
66
+ * box — a transition ResizeObserver does not reliably report. Armed lazily and
67
+ * idempotently; torn down by the next successful render(). Guarded so it no-ops
68
+ * in environments without IntersectionObserver (older engines, some SSR/jsdom).
69
+ */
70
+ private armReveal;
71
+ private teardownReveal;
72
+ private bind;
73
+ private onScroll;
74
+ private update;
75
+ }
76
+
77
+ /**
78
+ * A coordinated set of arrows that draw in a staggered sequence off one shared
79
+ * scroll trigger (or manual `setProgress`). Owns its arrows; `destroy()` tears
80
+ * them all down.
81
+ */
82
+ declare class ScrollArrowGroup {
83
+ private opts;
84
+ private arrows;
85
+ private windows;
86
+ private elements;
87
+ private target;
88
+ private progress;
89
+ private rafId;
90
+ private destroyed;
91
+ /** Render every arrow static + fully drawn when reduced motion is honored. */
92
+ private reducedMotion;
93
+ /** When false every arrow is hidden and the group does no scroll work. */
94
+ private enabled;
95
+ constructor(options: ScrollArrowGroupOptions);
96
+ /** Manually set group progress (0..1). Only meaningful when scroll is false. */
97
+ setProgress(p: number): void;
98
+ /** Recompute geometry for every arrow (call after layout changes). */
99
+ refresh(): void;
100
+ /**
101
+ * Suspend or restore the whole group without tearing it down. Hides every
102
+ * arrow and stops scroll work when off; restores and recomputes when on.
103
+ * Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
104
+ */
105
+ setEnabled(on: boolean): void;
106
+ destroy(): void;
47
107
  private bind;
48
108
  private onScroll;
49
109
  private update;
110
+ /** Push each arrow to its sliced local progress for the current group value. */
111
+ private distribute;
112
+ /** Synthetic rect spanning every endpoint, used as the default trigger. */
113
+ private groupRect;
50
114
  }
51
115
 
52
116
  declare function easeInOutCubic(t: number): number;
53
117
 
54
118
  /** Convenience factory. `const a = scrollArrow({ start, end })`. */
55
119
  declare function scrollArrow(options: ScrollArrowOptions): ScrollArrow;
120
+ /** Convenience factory. `const g = scrollArrowGroup({ arrows: [...] })`. */
121
+ declare function scrollArrowGroup(options: ScrollArrowGroupOptions): ScrollArrowGroup;
56
122
 
57
- export { ScrollArrow, ScrollArrowOptions, easeInOutCubic, scrollArrow };
123
+ export { ScrollArrow, ScrollArrowGroup, ScrollArrowGroupOptions, ScrollArrowOptions, easeInOutCubic, scrollArrow, scrollArrowGroup };
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
- import { ScrollArrow } from './chunk-HLZXSGP5.js';
2
- export { ScrollArrow, easeInOutCubic } from './chunk-HLZXSGP5.js';
1
+ import { ScrollArrow, ScrollArrowGroup } from './chunk-LIT577GH.js';
2
+ export { ScrollArrow, ScrollArrowGroup, easeInOutCubic } from './chunk-LIT577GH.js';
3
3
 
4
4
  // src/index.ts
5
5
  function scrollArrow(options) {
6
6
  return new ScrollArrow(options);
7
7
  }
8
+ function scrollArrowGroup(options) {
9
+ return new ScrollArrowGroup(options);
10
+ }
8
11
 
9
- export { scrollArrow };
12
+ export { scrollArrow, scrollArrowGroup };
10
13
  //# sourceMappingURL=index.js.map
11
14
  //# sourceMappingURL=index.js.map