scroll-arrows 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,8 @@ Framework-agnostic core + a thin React wrapper. Arrows live in a click-through
20
20
  overlay `<svg>`, auto-track their endpoints with `ResizeObserver`, and draw on
21
21
  scroll progress.
22
22
 
23
+ **[▶ Live demo](https://dancj.github.io/scroll-arrows/)**
24
+
23
25
  ```bash
24
26
  npm install scroll-arrows
25
27
  ```
@@ -89,9 +91,11 @@ arrow.destroy();
89
91
  rendering fully drawn and static (no scroll animation) while still tracking
90
92
  layout. Opt out with `respectReducedMotion: false` to keep the animation.
91
93
  Works the same for `scrollArrowGroup`.
92
- - **Labels** — `label` rides along the line at `labelAt` (0..1, default mid)
93
- and can sit off the line via `labelOffset` (perpendicular px; + = left of the
94
- draw direction, = right). Fades in as the pen draws through it.
94
+ - **Labels** — `label` rides along the line at `labelAt` a keyword
95
+ (`'start'` / `'middle'` / `'end'`), a `0..1` fraction, or a percentage string
96
+ like `'25%'` (default `'middle'`) and can sit off the line via `labelOffset`
97
+ (perpendicular px; + = left of the draw direction, − = right). Fades in as the
98
+ pen draws through it.
95
99
  `labelBackground` masks a gap in the line behind the text (the excalidraw
96
100
  look); style via `labelColor` / `font`.
97
101
  - **Staggered groups** — `scrollArrowGroup` owns N arrows and reveals them in
@@ -301,6 +301,25 @@ function clamp012(t) {
301
301
  }
302
302
 
303
303
  // src/draw.ts
304
+ var LABEL_KEYWORDS = {
305
+ start: 0,
306
+ middle: 0.5,
307
+ end: 1
308
+ };
309
+ function resolveLabelAt(value, fallback = 0.5) {
310
+ if (value == null) return fallback;
311
+ if (typeof value === "number") {
312
+ return Number.isFinite(value) ? clamp01(value) : fallback;
313
+ }
314
+ const key = value.trim().toLowerCase();
315
+ const keyword = LABEL_KEYWORDS[key];
316
+ if (keyword !== void 0) return keyword;
317
+ if (key.endsWith("%")) {
318
+ const n = Number.parseFloat(key.slice(0, -1));
319
+ return Number.isFinite(n) ? clamp01(n / 100) : fallback;
320
+ }
321
+ return fallback;
322
+ }
304
323
  function lengths(segs) {
305
324
  let lineLen = 0;
306
325
  let headLen = 0;
@@ -330,7 +349,8 @@ function lineProgress(segs, eased) {
330
349
  return lineLen > 0 ? clamp01(drawn / lineLen) : 1;
331
350
  }
332
351
  function labelOpacity(lineProg, labelAt, fade = 0.08) {
333
- return clamp01((lineProg - clamp01(labelAt)) / (fade || 1));
352
+ const start = Math.min(clamp01(labelAt), 1 - fade);
353
+ return clamp01((lineProg - start) / (fade || 1));
334
354
  }
335
355
 
336
356
  // src/scroll-arrow.ts
@@ -344,8 +364,17 @@ var ScrollArrow = class {
344
364
  this.segments = [];
345
365
  /** Representative line stroke + label nodes, when a label is set. */
346
366
  this.lineEl = null;
367
+ /**
368
+ * The smooth ideal path `d` (pre-roughjs). Label placement measures against
369
+ * this, not `lineEl`: rough.js bakes its double stroke into one path with two
370
+ * subpaths, so `lineEl.getTotalLength()` is ~2x the visible curve and would
371
+ * put `labelAt` at twice its intended fraction.
372
+ */
373
+ this.lineD = "";
347
374
  this.labelEl = null;
348
375
  this.labelBgEl = null;
376
+ /** `opts.labelAt` resolved to a 0..1 fraction; cached by renderLabel for the draw loop. */
377
+ this.resolvedLabelAt = 0.5;
349
378
  this.rafId = 0;
350
379
  this.destroyed = false;
351
380
  this.onScroll = () => {
@@ -501,6 +530,7 @@ var ScrollArrow = class {
501
530
  const belly = { x: clear.x * BOW, y: clear.y * BOW };
502
531
  d = buildPath(local, curvature, belly);
503
532
  }
533
+ this.lineD = d;
504
534
  this.appendDrawable(this.rc.path(d, roughOpts), "line");
505
535
  const head = this.opts.head;
506
536
  const size = this.opts.headSize;
@@ -537,25 +567,26 @@ var ScrollArrow = class {
537
567
  this.labelEl = null;
538
568
  this.labelBgEl = null;
539
569
  const text = this.opts.label;
540
- if (!text || !this.lineEl) return;
541
- const total = this.lineEl.getTotalLength();
542
- const at = clampAt(this.opts.labelAt ?? 0.5);
543
- const pt = this.lineEl.getPointAtLength(at * total);
570
+ if (!text || !this.lineEl || !this.lineD) return;
571
+ const at = resolveLabelAt(this.opts.labelAt);
572
+ this.resolvedLabelAt = at;
573
+ const measure = createSvgEl("path");
574
+ measure.setAttribute("d", this.lineD);
575
+ this.group.appendChild(measure);
576
+ const total = measure.getTotalLength();
577
+ const pt = measure.getPointAtLength(at * total);
544
578
  const offset = this.opts.labelOffset ?? 0;
545
579
  let x = pt.x;
546
580
  let y = pt.y;
547
581
  if (offset && total > 0) {
548
582
  const eps = Math.min(1, total / 2);
549
- const before = this.lineEl.getPointAtLength(
550
- Math.max(0, at * total - eps)
551
- );
552
- const after = this.lineEl.getPointAtLength(
553
- Math.min(total, at * total + eps)
554
- );
583
+ const before = measure.getPointAtLength(Math.max(0, at * total - eps));
584
+ const after = measure.getPointAtLength(Math.min(total, at * total + eps));
555
585
  const n = unitNormal(before, after);
556
586
  x += n.x * offset;
557
587
  y += n.y * offset;
558
588
  }
589
+ this.group.removeChild(measure);
559
590
  const label = createSvgEl("text");
560
591
  label.textContent = text;
561
592
  label.setAttribute("x", String(x));
@@ -605,7 +636,7 @@ var ScrollArrow = class {
605
636
  if (this.labelEl) {
606
637
  const op = labelOpacity(
607
638
  lineProgress(this.segments, eased),
608
- this.opts.labelAt ?? 0.5
639
+ this.resolvedLabelAt
609
640
  );
610
641
  this.labelEl.style.opacity = String(op);
611
642
  if (this.labelBgEl) this.labelBgEl.style.opacity = String(op);
@@ -659,9 +690,6 @@ function resolve(ref) {
659
690
  function refKey(ref) {
660
691
  return typeof ref === "string" ? ref : ref.tagName + (ref.id ? "#" + ref.id : "");
661
692
  }
662
- function clampAt(t) {
663
- return t < 0 ? 0 : t > 1 ? 1 : t;
664
- }
665
693
 
666
694
  // src/group.ts
667
695
  var ScrollArrowGroup = class {
@@ -798,5 +826,5 @@ function resolve2(ref) {
798
826
  }
799
827
 
800
828
  export { ScrollArrow, ScrollArrowGroup, easeInOutCubic };
801
- //# sourceMappingURL=chunk-LIT577GH.js.map
802
- //# sourceMappingURL=chunk-LIT577GH.js.map
829
+ //# sourceMappingURL=chunk-GX722UDR.js.map
830
+ //# sourceMappingURL=chunk-GX722UDR.js.map
package/dist/index.cjs CHANGED
@@ -309,6 +309,25 @@ function clamp012(t) {
309
309
  }
310
310
 
311
311
  // src/draw.ts
312
+ var LABEL_KEYWORDS = {
313
+ start: 0,
314
+ middle: 0.5,
315
+ end: 1
316
+ };
317
+ function resolveLabelAt(value, fallback = 0.5) {
318
+ if (value == null) return fallback;
319
+ if (typeof value === "number") {
320
+ return Number.isFinite(value) ? clamp01(value) : fallback;
321
+ }
322
+ const key = value.trim().toLowerCase();
323
+ const keyword = LABEL_KEYWORDS[key];
324
+ if (keyword !== void 0) return keyword;
325
+ if (key.endsWith("%")) {
326
+ const n = Number.parseFloat(key.slice(0, -1));
327
+ return Number.isFinite(n) ? clamp01(n / 100) : fallback;
328
+ }
329
+ return fallback;
330
+ }
312
331
  function lengths(segs) {
313
332
  let lineLen = 0;
314
333
  let headLen = 0;
@@ -338,7 +357,8 @@ function lineProgress(segs, eased) {
338
357
  return lineLen > 0 ? clamp01(drawn / lineLen) : 1;
339
358
  }
340
359
  function labelOpacity(lineProg, labelAt, fade = 0.08) {
341
- return clamp01((lineProg - clamp01(labelAt)) / (fade || 1));
360
+ const start = Math.min(clamp01(labelAt), 1 - fade);
361
+ return clamp01((lineProg - start) / (fade || 1));
342
362
  }
343
363
 
344
364
  // src/scroll-arrow.ts
@@ -352,8 +372,17 @@ var ScrollArrow = class {
352
372
  this.segments = [];
353
373
  /** Representative line stroke + label nodes, when a label is set. */
354
374
  this.lineEl = null;
375
+ /**
376
+ * The smooth ideal path `d` (pre-roughjs). Label placement measures against
377
+ * this, not `lineEl`: rough.js bakes its double stroke into one path with two
378
+ * subpaths, so `lineEl.getTotalLength()` is ~2x the visible curve and would
379
+ * put `labelAt` at twice its intended fraction.
380
+ */
381
+ this.lineD = "";
355
382
  this.labelEl = null;
356
383
  this.labelBgEl = null;
384
+ /** `opts.labelAt` resolved to a 0..1 fraction; cached by renderLabel for the draw loop. */
385
+ this.resolvedLabelAt = 0.5;
357
386
  this.rafId = 0;
358
387
  this.destroyed = false;
359
388
  this.onScroll = () => {
@@ -509,6 +538,7 @@ var ScrollArrow = class {
509
538
  const belly = { x: clear.x * BOW, y: clear.y * BOW };
510
539
  d = buildPath(local, curvature, belly);
511
540
  }
541
+ this.lineD = d;
512
542
  this.appendDrawable(this.rc.path(d, roughOpts), "line");
513
543
  const head = this.opts.head;
514
544
  const size = this.opts.headSize;
@@ -545,25 +575,26 @@ var ScrollArrow = class {
545
575
  this.labelEl = null;
546
576
  this.labelBgEl = null;
547
577
  const text = this.opts.label;
548
- if (!text || !this.lineEl) return;
549
- const total = this.lineEl.getTotalLength();
550
- const at = clampAt(this.opts.labelAt ?? 0.5);
551
- const pt = this.lineEl.getPointAtLength(at * total);
578
+ if (!text || !this.lineEl || !this.lineD) return;
579
+ const at = resolveLabelAt(this.opts.labelAt);
580
+ this.resolvedLabelAt = at;
581
+ const measure = createSvgEl("path");
582
+ measure.setAttribute("d", this.lineD);
583
+ this.group.appendChild(measure);
584
+ const total = measure.getTotalLength();
585
+ const pt = measure.getPointAtLength(at * total);
552
586
  const offset = this.opts.labelOffset ?? 0;
553
587
  let x = pt.x;
554
588
  let y = pt.y;
555
589
  if (offset && total > 0) {
556
590
  const eps = Math.min(1, total / 2);
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
- );
591
+ const before = measure.getPointAtLength(Math.max(0, at * total - eps));
592
+ const after = measure.getPointAtLength(Math.min(total, at * total + eps));
563
593
  const n = unitNormal(before, after);
564
594
  x += n.x * offset;
565
595
  y += n.y * offset;
566
596
  }
597
+ this.group.removeChild(measure);
567
598
  const label = createSvgEl("text");
568
599
  label.textContent = text;
569
600
  label.setAttribute("x", String(x));
@@ -613,7 +644,7 @@ var ScrollArrow = class {
613
644
  if (this.labelEl) {
614
645
  const op = labelOpacity(
615
646
  lineProgress(this.segments, eased),
616
- this.opts.labelAt ?? 0.5
647
+ this.resolvedLabelAt
617
648
  );
618
649
  this.labelEl.style.opacity = String(op);
619
650
  if (this.labelBgEl) this.labelBgEl.style.opacity = String(op);
@@ -667,9 +698,6 @@ function resolve(ref) {
667
698
  function refKey(ref) {
668
699
  return typeof ref === "string" ? ref : ref.tagName + (ref.id ? "#" + ref.id : "");
669
700
  }
670
- function clampAt(t) {
671
- return t < 0 ? 0 : t > 1 ? 1 : t;
672
- }
673
701
 
674
702
  // src/group.ts
675
703
  var ScrollArrowGroup = class {
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
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';
1
+ import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-CgHPWybd.cjs';
2
+ export { A as ArrowHead, E as ElementRef, L as LabelPosition, P as Point, b as ScrollOptions, c as Socket } from './types-CgHPWybd.cjs';
3
3
 
4
4
  /** A single hand-drawn arrow that draws itself between two elements on scroll. */
5
5
  declare class ScrollArrow {
@@ -18,8 +18,17 @@ declare class ScrollArrow {
18
18
  private segments;
19
19
  /** Representative line stroke + label nodes, when a label is set. */
20
20
  private lineEl;
21
+ /**
22
+ * The smooth ideal path `d` (pre-roughjs). Label placement measures against
23
+ * this, not `lineEl`: rough.js bakes its double stroke into one path with two
24
+ * subpaths, so `lineEl.getTotalLength()` is ~2x the visible curve and would
25
+ * put `labelAt` at twice its intended fraction.
26
+ */
27
+ private lineD;
21
28
  private labelEl;
22
29
  private labelBgEl;
30
+ /** `opts.labelAt` resolved to a 0..1 fraction; cached by renderLabel for the draw loop. */
31
+ private resolvedLabelAt;
23
32
  private progress;
24
33
  private ro?;
25
34
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
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';
1
+ import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-CgHPWybd.js';
2
+ export { A as ArrowHead, E as ElementRef, L as LabelPosition, P as Point, b as ScrollOptions, c as Socket } from './types-CgHPWybd.js';
3
3
 
4
4
  /** A single hand-drawn arrow that draws itself between two elements on scroll. */
5
5
  declare class ScrollArrow {
@@ -18,8 +18,17 @@ declare class ScrollArrow {
18
18
  private segments;
19
19
  /** Representative line stroke + label nodes, when a label is set. */
20
20
  private lineEl;
21
+ /**
22
+ * The smooth ideal path `d` (pre-roughjs). Label placement measures against
23
+ * this, not `lineEl`: rough.js bakes its double stroke into one path with two
24
+ * subpaths, so `lineEl.getTotalLength()` is ~2x the visible curve and would
25
+ * put `labelAt` at twice its intended fraction.
26
+ */
27
+ private lineD;
21
28
  private labelEl;
22
29
  private labelBgEl;
30
+ /** `opts.labelAt` resolved to a 0..1 fraction; cached by renderLabel for the draw loop. */
31
+ private resolvedLabelAt;
23
32
  private progress;
24
33
  private ro?;
25
34
  /**
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ScrollArrow, ScrollArrowGroup } from './chunk-LIT577GH.js';
2
- export { ScrollArrow, ScrollArrowGroup, easeInOutCubic } from './chunk-LIT577GH.js';
1
+ import { ScrollArrow, ScrollArrowGroup } from './chunk-GX722UDR.js';
2
+ export { ScrollArrow, ScrollArrowGroup, easeInOutCubic } from './chunk-GX722UDR.js';
3
3
 
4
4
  // src/index.ts
5
5
  function scrollArrow(options) {
package/dist/react.cjs CHANGED
@@ -310,6 +310,25 @@ function clamp012(t) {
310
310
  }
311
311
 
312
312
  // src/draw.ts
313
+ var LABEL_KEYWORDS = {
314
+ start: 0,
315
+ middle: 0.5,
316
+ end: 1
317
+ };
318
+ function resolveLabelAt(value, fallback = 0.5) {
319
+ if (value == null) return fallback;
320
+ if (typeof value === "number") {
321
+ return Number.isFinite(value) ? clamp01(value) : fallback;
322
+ }
323
+ const key = value.trim().toLowerCase();
324
+ const keyword = LABEL_KEYWORDS[key];
325
+ if (keyword !== void 0) return keyword;
326
+ if (key.endsWith("%")) {
327
+ const n = Number.parseFloat(key.slice(0, -1));
328
+ return Number.isFinite(n) ? clamp01(n / 100) : fallback;
329
+ }
330
+ return fallback;
331
+ }
313
332
  function lengths(segs) {
314
333
  let lineLen = 0;
315
334
  let headLen = 0;
@@ -339,7 +358,8 @@ function lineProgress(segs, eased) {
339
358
  return lineLen > 0 ? clamp01(drawn / lineLen) : 1;
340
359
  }
341
360
  function labelOpacity(lineProg, labelAt, fade = 0.08) {
342
- return clamp01((lineProg - clamp01(labelAt)) / (fade || 1));
361
+ const start = Math.min(clamp01(labelAt), 1 - fade);
362
+ return clamp01((lineProg - start) / (fade || 1));
343
363
  }
344
364
 
345
365
  // src/scroll-arrow.ts
@@ -353,8 +373,17 @@ var ScrollArrow = class {
353
373
  this.segments = [];
354
374
  /** Representative line stroke + label nodes, when a label is set. */
355
375
  this.lineEl = null;
376
+ /**
377
+ * The smooth ideal path `d` (pre-roughjs). Label placement measures against
378
+ * this, not `lineEl`: rough.js bakes its double stroke into one path with two
379
+ * subpaths, so `lineEl.getTotalLength()` is ~2x the visible curve and would
380
+ * put `labelAt` at twice its intended fraction.
381
+ */
382
+ this.lineD = "";
356
383
  this.labelEl = null;
357
384
  this.labelBgEl = null;
385
+ /** `opts.labelAt` resolved to a 0..1 fraction; cached by renderLabel for the draw loop. */
386
+ this.resolvedLabelAt = 0.5;
358
387
  this.rafId = 0;
359
388
  this.destroyed = false;
360
389
  this.onScroll = () => {
@@ -510,6 +539,7 @@ var ScrollArrow = class {
510
539
  const belly = { x: clear.x * BOW, y: clear.y * BOW };
511
540
  d = buildPath(local, curvature, belly);
512
541
  }
542
+ this.lineD = d;
513
543
  this.appendDrawable(this.rc.path(d, roughOpts), "line");
514
544
  const head = this.opts.head;
515
545
  const size = this.opts.headSize;
@@ -546,25 +576,26 @@ var ScrollArrow = class {
546
576
  this.labelEl = null;
547
577
  this.labelBgEl = null;
548
578
  const text = this.opts.label;
549
- if (!text || !this.lineEl) return;
550
- const total = this.lineEl.getTotalLength();
551
- const at = clampAt(this.opts.labelAt ?? 0.5);
552
- const pt = this.lineEl.getPointAtLength(at * total);
579
+ if (!text || !this.lineEl || !this.lineD) return;
580
+ const at = resolveLabelAt(this.opts.labelAt);
581
+ this.resolvedLabelAt = at;
582
+ const measure = createSvgEl("path");
583
+ measure.setAttribute("d", this.lineD);
584
+ this.group.appendChild(measure);
585
+ const total = measure.getTotalLength();
586
+ const pt = measure.getPointAtLength(at * total);
553
587
  const offset = this.opts.labelOffset ?? 0;
554
588
  let x = pt.x;
555
589
  let y = pt.y;
556
590
  if (offset && total > 0) {
557
591
  const eps = Math.min(1, total / 2);
558
- const before = this.lineEl.getPointAtLength(
559
- Math.max(0, at * total - eps)
560
- );
561
- const after = this.lineEl.getPointAtLength(
562
- Math.min(total, at * total + eps)
563
- );
592
+ const before = measure.getPointAtLength(Math.max(0, at * total - eps));
593
+ const after = measure.getPointAtLength(Math.min(total, at * total + eps));
564
594
  const n = unitNormal(before, after);
565
595
  x += n.x * offset;
566
596
  y += n.y * offset;
567
597
  }
598
+ this.group.removeChild(measure);
568
599
  const label = createSvgEl("text");
569
600
  label.textContent = text;
570
601
  label.setAttribute("x", String(x));
@@ -614,7 +645,7 @@ var ScrollArrow = class {
614
645
  if (this.labelEl) {
615
646
  const op = labelOpacity(
616
647
  lineProgress(this.segments, eased),
617
- this.opts.labelAt ?? 0.5
648
+ this.resolvedLabelAt
618
649
  );
619
650
  this.labelEl.style.opacity = String(op);
620
651
  if (this.labelBgEl) this.labelBgEl.style.opacity = String(op);
@@ -668,9 +699,6 @@ function resolve(ref) {
668
699
  function refKey(ref) {
669
700
  return typeof ref === "string" ? ref : ref.tagName + (ref.id ? "#" + ref.id : "");
670
701
  }
671
- function clampAt(t) {
672
- return t < 0 ? 0 : t > 1 ? 1 : t;
673
- }
674
702
 
675
703
  // src/group.ts
676
704
  var ScrollArrowGroup = class {
package/dist/react.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RefObject } from 'react';
2
- import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-Cpvz9wtr.cjs';
2
+ import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-CgHPWybd.cjs';
3
3
 
4
4
  type Anchor = RefObject<Element | null> | Element | string;
5
5
  interface UseScrollArrowOptions extends Omit<ScrollArrowOptions, 'start' | 'end'> {
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RefObject } from 'react';
2
- import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-Cpvz9wtr.js';
2
+ import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-CgHPWybd.js';
3
3
 
4
4
  type Anchor = RefObject<Element | null> | Element | string;
5
5
  interface UseScrollArrowOptions extends Omit<ScrollArrowOptions, 'start' | 'end'> {
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ScrollArrow, ScrollArrowGroup } from './chunk-LIT577GH.js';
1
+ import { ScrollArrow, ScrollArrowGroup } from './chunk-GX722UDR.js';
2
2
  import { useRef, useEffect } from 'react';
3
3
 
4
4
  function read(a) {
@@ -13,6 +13,13 @@ type ArrowHead = 'start' | 'end' | 'both' | 'none';
13
13
  type Route = 'curved' | 'elbow';
14
14
  /** Anything we can resolve to a live DOM element. */
15
15
  type ElementRef = Element | string;
16
+ /**
17
+ * Where a label sits along the line: a keyword, a `0..1` fraction, or a
18
+ * percentage string like `'25%'`. A dynamically built percentage string is
19
+ * typed as `string`, so cast it (`pct as LabelPosition`) — any unrecognized
20
+ * value falls back to the default at runtime.
21
+ */
22
+ type LabelPosition = 'start' | 'middle' | 'end' | number | `${number}%`;
16
23
  interface ScrollOptions {
17
24
  /**
18
25
  * The element whose travel through the viewport drives the draw.
@@ -91,11 +98,18 @@ interface ScrollArrowOptions {
91
98
  headSize?: number;
92
99
  /** Text to place along the line. Omit for no label. */
93
100
  label?: string;
94
- /** Position of the label along the line, 0 (start) .. 1 (end). Default 0.5. */
95
- labelAt?: number;
96
101
  /**
97
- * Perpendicular offset from the line in px. Positive sits to the left of the
98
- * draw direction, negative to the right; 0 sits on the line. Default 0.
102
+ * Where the label sits *along* the line. Accepts:
103
+ * - a keyword: `'start'`, `'middle'`, or `'end'`;
104
+ * - a `0..1` fraction (`0` = start, `1` = end); or
105
+ * - a percentage string like `'25%'`.
106
+ * Default `'middle'`. For sideways placement see `labelOffset`.
107
+ */
108
+ labelAt?: LabelPosition;
109
+ /**
110
+ * How far the label sits *perpendicular* to the line, in px (sideways, not
111
+ * along it). Positive sits to the left of the draw direction, negative to the
112
+ * right; 0 sits on the line. Default 0.
99
113
  */
100
114
  labelOffset?: number;
101
115
  /** Label text color. Default: stroke color. */
@@ -179,4 +193,4 @@ interface ScrollArrowGroupOptions {
179
193
  enabled?: boolean;
180
194
  }
181
195
 
182
- export type { ArrowHead as A, ElementRef as E, Point as P, ScrollArrowOptions as S, ScrollArrowGroupOptions as a, ScrollOptions as b, Socket as c };
196
+ export type { ArrowHead as A, ElementRef as E, LabelPosition as L, Point as P, ScrollArrowOptions as S, ScrollArrowGroupOptions as a, ScrollOptions as b, Socket as c };
@@ -13,6 +13,13 @@ type ArrowHead = 'start' | 'end' | 'both' | 'none';
13
13
  type Route = 'curved' | 'elbow';
14
14
  /** Anything we can resolve to a live DOM element. */
15
15
  type ElementRef = Element | string;
16
+ /**
17
+ * Where a label sits along the line: a keyword, a `0..1` fraction, or a
18
+ * percentage string like `'25%'`. A dynamically built percentage string is
19
+ * typed as `string`, so cast it (`pct as LabelPosition`) — any unrecognized
20
+ * value falls back to the default at runtime.
21
+ */
22
+ type LabelPosition = 'start' | 'middle' | 'end' | number | `${number}%`;
16
23
  interface ScrollOptions {
17
24
  /**
18
25
  * The element whose travel through the viewport drives the draw.
@@ -91,11 +98,18 @@ interface ScrollArrowOptions {
91
98
  headSize?: number;
92
99
  /** Text to place along the line. Omit for no label. */
93
100
  label?: string;
94
- /** Position of the label along the line, 0 (start) .. 1 (end). Default 0.5. */
95
- labelAt?: number;
96
101
  /**
97
- * Perpendicular offset from the line in px. Positive sits to the left of the
98
- * draw direction, negative to the right; 0 sits on the line. Default 0.
102
+ * Where the label sits *along* the line. Accepts:
103
+ * - a keyword: `'start'`, `'middle'`, or `'end'`;
104
+ * - a `0..1` fraction (`0` = start, `1` = end); or
105
+ * - a percentage string like `'25%'`.
106
+ * Default `'middle'`. For sideways placement see `labelOffset`.
107
+ */
108
+ labelAt?: LabelPosition;
109
+ /**
110
+ * How far the label sits *perpendicular* to the line, in px (sideways, not
111
+ * along it). Positive sits to the left of the draw direction, negative to the
112
+ * right; 0 sits on the line. Default 0.
99
113
  */
100
114
  labelOffset?: number;
101
115
  /** Label text color. Default: stroke color. */
@@ -179,4 +193,4 @@ interface ScrollArrowGroupOptions {
179
193
  enabled?: boolean;
180
194
  }
181
195
 
182
- export type { ArrowHead as A, ElementRef as E, Point as P, ScrollArrowOptions as S, ScrollArrowGroupOptions as a, ScrollOptions as b, Socket as c };
196
+ export type { ArrowHead as A, ElementRef as E, LabelPosition as L, Point as P, ScrollArrowOptions as S, ScrollArrowGroupOptions as a, ScrollOptions as b, Socket as c };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scroll-arrows",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Hand-drawn arrows that draw themselves between two elements as you scroll. Roughness goes from clean straight lines to scratchy and curvy.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -54,6 +54,7 @@
54
54
  "build": "tsup",
55
55
  "dev": "tsup --watch",
56
56
  "demo": "vite demo",
57
+ "demo:build": "vite build demo --base=/scroll-arrows/ --outDir dist-demo --emptyOutDir",
57
58
  "gen:hero": "node scripts/genHeroSvg.mjs",
58
59
  "typecheck": "tsc --noEmit",
59
60
  "lint": "eslint .",