use-scroll-animate 5.3.0 → 5.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/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [5.4.0] - 2026-10-08
11
+
12
+ ### Added
13
+ - **`<usa-story template="…">`** (`defineStory()` from `use-scroll-animate/components/effects`; `defineEffectElements()` defines every element of the entry) — six scroll-storytelling templates:
14
+ - `pin` — a sticky `[data-stage]` while `[data-step]` sections scroll past; the active step gets `data-active`, the stage `data-active-step="<i>"`.
15
+ - `gallery` — a horizontal `[data-track]` slides sideways as you scroll down.
16
+ - `zoom` — zoom-through: the stage scales up to `zoom="6"` and fades.
17
+ - `compare` — before / after wipe driven by scroll, with a draggable, keyboard-accessible handle (`role="slider"`, ←/→, Shift for ×5, Home/End).
18
+ - `counter` — `[data-count="12,480"]` numbers count up on entering view (separators, decimals, prefix / suffix kept).
19
+ - `highlight` — the paragraph (or `[data-step]`) crossing the viewport center is highlighted.
20
+ - Every template sets `--usa-story-progress` (0–1), exposes `progress` / `step` / `update()` and fires `usa-story-step`. Helpers: `storyProgress(el)`, `formatCount(target, t)`, `STORY_TEMPLATES`.
21
+ - Showcase: new **[Scroll stories](./showcase/story.html)** page with all six templates full-page; gallery cards **Story: before / after**, **Story: data counters**, **Story: step highlight**. Gallery code tabs import from the card’s own entry.
22
+
23
+ ### Accessibility
24
+ - Under reduced motion nothing slides or zooms (the gallery stacks vertically), counters show their final values immediately; counted numbers carry the final value as `aria-label`.
25
+
10
26
  ## [5.3.0] - 2026-10-08
11
27
 
12
28
  ### Added
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var components_fx = require('./fx.cjs');
4
- require('../chunks/base-B5i8qQPR.cjs');
4
+ var base = require('../chunks/base-B5i8qQPR.cjs');
5
5
  require('../chunks/core-BDcszY4L.cjs');
6
6
  require('./tokens.cjs');
7
7
  require('../chunks/fx-B8hk1Fby.cjs');
@@ -619,6 +619,216 @@ const PAGE_FX = [
619
619
  },
620
620
  ];
621
621
 
622
+ /**
623
+ * 5.4 — `<usa-story template="…">` scroll-storytelling templates.
624
+ *
625
+ * - `pin` — a sticky `[data-stage]` while `[data-step]` sections scroll past; the
626
+ * step in view gets `data-active`, the stage gets `data-active-step="<index>"`.
627
+ * - `gallery` — a horizontal `[data-track]` slides sideways as you scroll down.
628
+ * - `zoom` — the `[data-stage]` zooms toward the viewer (`zoom="6"`) and fades.
629
+ * - `compare` — before / after (`[data-before]`, `[data-after]`) wipe driven by
630
+ * scroll, plus a draggable, keyboard-accessible handle (`role="slider"`).
631
+ * - `counter` — `[data-count="1234"]` numbers count up when they enter view.
632
+ * - `highlight` — paragraphs dim except the one crossing the viewport center.
633
+ *
634
+ * Every template sets `--usa-story-progress` (0–1) on the host and dispatches
635
+ * `usa-story-step` (`detail: { index }`). Reduced motion: no sliding / zooming
636
+ * (the gallery stacks vertically), counters show final values, the rest is
637
+ * class changes only.
638
+ */
639
+ const STORY_TEMPLATES = ['pin', 'gallery', 'zoom', 'compare', 'counter', 'highlight'];
640
+ const CSS = `usa-story{display:block;position:relative}
641
+ usa-story[template=pin] [data-stage],usa-story[template=gallery] [data-sticky],usa-story[template=zoom] [data-stage],usa-story[template=compare] [data-sticky]{position:sticky;top:0}
642
+ usa-story[template=pin] [data-stage]{align-self:start}
643
+ usa-story[template=pin] [data-step]{min-height:80vh;opacity:.35;transition:opacity .3s}
644
+ usa-story[template=pin] [data-step][data-active]{opacity:1}
645
+ usa-story[template=gallery] [data-sticky]{height:100vh;overflow:hidden;display:flex;align-items:center}
646
+ usa-story[template=gallery] [data-track]{display:flex;gap:24px;will-change:transform}
647
+ usa-story[template=gallery][data-static] [data-sticky]{position:static;height:auto;overflow:visible}
648
+ usa-story[template=gallery][data-static] [data-track]{flex-direction:column;transform:none!important}
649
+ usa-story[template=zoom] [data-stage]{height:100vh;overflow:hidden;display:grid;place-items:center}
650
+ usa-story[template=compare] [data-sticky]{height:var(--usa-story-h,70vh);overflow:hidden}
651
+ usa-story[template=compare] [data-before],usa-story[template=compare] [data-after]{position:absolute;inset:0}
652
+ usa-story[template=compare] [data-after]{clip-path:inset(0 0 0 var(--usa-split,50%))}
653
+ usa-story [data-handle]{position:absolute;top:0;bottom:0;left:var(--usa-split,50%);width:4px;margin-left:-2px;background:#fff;box-shadow:0 0 0 1px #0003;cursor:ew-resize;touch-action:none}
654
+ usa-story [data-handle]:focus-visible{outline:3px solid #7c5cff;outline-offset:2px}
655
+ usa-story[template=highlight] p,usa-story[template=highlight] [data-step]{opacity:.3;transition:opacity .25s}
656
+ usa-story[template=highlight] [data-active]{opacity:1}`;
657
+ const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
658
+ /** Progress of `el` through the viewport: 0 when its top hits the viewport top, 1 when its bottom hits the viewport bottom. */
659
+ function storyProgress(el, vh = typeof innerHeight === 'number' ? innerHeight : 800) {
660
+ const r = el.getBoundingClientRect();
661
+ const span = r.height - vh;
662
+ return span > 0 ? clamp(-r.top / span) : clamp((vh - r.top) / (vh + r.height));
663
+ }
664
+ /** Format a counted value like the target (`1,234`, `12.5`, prefix / suffix kept). */
665
+ function formatCount(target, t) {
666
+ const m = target.match(/^(\D*)([\d,]*\.?\d+)(.*)$/);
667
+ if (!m)
668
+ return target;
669
+ const [, pre, num, post] = m;
670
+ const n = parseFloat(num.replace(/,/g, ''));
671
+ const dec = (num.split('.')[1] || '').length;
672
+ let s = (n * t).toFixed(dec);
673
+ if (num.includes(','))
674
+ s = Number(s).toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec });
675
+ return pre + s + post;
676
+ }
677
+ function defineStory(tag = 'usa-story') {
678
+ return base.defineElement(tag, (Base) => class UsaStory extends Base {
679
+ constructor() {
680
+ super(...arguments);
681
+ this.progress = 0;
682
+ this.step = -1;
683
+ this.frame = 0;
684
+ }
685
+ static get observedAttributes() {
686
+ return ['template', 'zoom'];
687
+ }
688
+ get template() {
689
+ const t = this.str('template', 'pin');
690
+ return STORY_TEMPLATES.includes(t) ? t : 'pin';
691
+ }
692
+ steps() {
693
+ const own = Array.from(this.querySelectorAll('[data-step]:not([data-stage])'));
694
+ return own.length || this.template !== 'highlight' ? own : Array.from(this.querySelectorAll('p'));
695
+ }
696
+ setStep(i) {
697
+ if (i === this.step)
698
+ return;
699
+ this.step = i;
700
+ this.steps().forEach((s, k) => s.toggleAttribute('data-active', k === i));
701
+ const stage = this.querySelector('[data-stage]');
702
+ if (stage)
703
+ stage.dataset.activeStep = String(i);
704
+ this.dispatchEvent(new CustomEvent('usa-story-step', { detail: { index: i }, bubbles: true }));
705
+ }
706
+ /** Recompute from the current scroll position (called on scroll / resize). */
707
+ update() {
708
+ this.frame = 0;
709
+ const p = (this.progress = storyProgress(this));
710
+ this.style.setProperty('--usa-story-progress', p.toFixed(4));
711
+ const t = this.template;
712
+ const vh = innerHeight || 800;
713
+ if (t === 'pin' || t === 'highlight') {
714
+ const mid = vh / 2;
715
+ let best = -1;
716
+ let bestD = Infinity;
717
+ this.steps().forEach((s, i) => {
718
+ const r = s.getBoundingClientRect();
719
+ const d = r.top <= mid && r.bottom >= mid ? 0 : Math.min(Math.abs(r.top - mid), Math.abs(r.bottom - mid));
720
+ if (d < bestD)
721
+ (bestD = d), (best = i);
722
+ });
723
+ this.setStep(best);
724
+ }
725
+ else if (t === 'gallery' && !this.reduced) {
726
+ const track = this.querySelector('[data-track]');
727
+ const box = this.querySelector('[data-sticky]');
728
+ if (track && box)
729
+ track.style.transform = `translateX(${(-p * Math.max(0, track.scrollWidth - box.clientWidth)).toFixed(1)}px)`;
730
+ }
731
+ else if (t === 'zoom' && !this.reduced) {
732
+ const inner = this.querySelector('[data-stage] > *');
733
+ if (inner) {
734
+ inner.style.transform = `scale(${(1 + p * (this.num('zoom', 6) - 1)).toFixed(3)})`;
735
+ inner.style.opacity = String(clamp(1.4 - p * 1.4).toFixed(3));
736
+ }
737
+ }
738
+ else if (t === 'compare' && !this.hasAttribute('data-dragged')) {
739
+ this.setSplit(p * 100);
740
+ }
741
+ else if (t === 'counter') {
742
+ this.querySelectorAll('[data-count]:not([data-counted])').forEach((el) => {
743
+ const r = el.getBoundingClientRect();
744
+ if (r.top < vh * 0.9 && r.bottom > 0)
745
+ this.count(el);
746
+ });
747
+ }
748
+ }
749
+ setSplit(pct) {
750
+ const v = clamp(pct, 0, 100);
751
+ this.style.setProperty('--usa-split', `${v.toFixed(2)}%`);
752
+ const h = this.querySelector('[data-handle]');
753
+ if (h)
754
+ h.setAttribute('aria-valuenow', String(Math.round(v)));
755
+ }
756
+ count(el) {
757
+ el.dataset.counted = '';
758
+ const target = el.dataset.count || el.textContent || '0';
759
+ el.setAttribute('aria-label', target);
760
+ if (this.reduced) {
761
+ el.textContent = target;
762
+ return;
763
+ }
764
+ const dur = Number(el.dataset.duration) || 1600;
765
+ const t0 = performance.now();
766
+ const tick = (now) => {
767
+ const k = clamp((now - t0) / dur);
768
+ el.textContent = formatCount(target, 1 - (1 - k) ** 3);
769
+ if (k < 1)
770
+ requestAnimationFrame(tick);
771
+ };
772
+ requestAnimationFrame(tick);
773
+ }
774
+ mount() {
775
+ this.toggleAttribute('data-static', this.reduced);
776
+ if (this.template === 'compare')
777
+ this.mountCompare();
778
+ const kick = () => {
779
+ if (!this.frame)
780
+ this.frame = requestAnimationFrame(() => this.update());
781
+ };
782
+ this.listen(window, 'scroll', kick, { passive: true });
783
+ this.listen(window, 'resize', kick);
784
+ this.onCleanup(() => cancelAnimationFrame(this.frame));
785
+ this.update();
786
+ }
787
+ mountCompare() {
788
+ const box = this.querySelector('[data-sticky]') || this;
789
+ let h = this.querySelector('[data-handle]');
790
+ if (!h) {
791
+ h = document.createElement('div');
792
+ h.setAttribute('data-handle', '');
793
+ box.appendChild(h);
794
+ this.onCleanup(() => h.remove());
795
+ }
796
+ h.tabIndex = 0;
797
+ h.setAttribute('role', 'slider');
798
+ h.setAttribute('aria-label', this.str('label', 'Before / after'));
799
+ h.setAttribute('aria-valuemin', '0');
800
+ h.setAttribute('aria-valuemax', '100');
801
+ const at = (e) => {
802
+ const r = box.getBoundingClientRect();
803
+ this.dataset.dragged = '';
804
+ this.setSplit(((e.clientX - r.left) / (r.width || 1)) * 100);
805
+ };
806
+ let drag = false;
807
+ this.listen(h, 'pointerdown', (e) => {
808
+ drag = true;
809
+ h.setPointerCapture?.(e.pointerId);
810
+ });
811
+ this.listen(h, 'pointermove', (e) => drag && at(e));
812
+ this.listen(h, 'pointerup', () => (drag = false));
813
+ this.listen(h, 'keydown', (e) => {
814
+ const cur = parseFloat(h.getAttribute('aria-valuenow') || '50');
815
+ const step = e.shiftKey ? 10 : 2;
816
+ const next = e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? cur - step : e.key === 'ArrowRight' || e.key === 'ArrowUp' ? cur + step : e.key === 'Home' ? 0 : e.key === 'End' ? 100 : null;
817
+ if (next == null)
818
+ return;
819
+ e.preventDefault();
820
+ this.dataset.dragged = '';
821
+ this.setSplit(next);
822
+ });
823
+ this.setSplit(50);
824
+ }
825
+ unmount() {
826
+ this.step = -1;
827
+ delete this.dataset.dragged;
828
+ }
829
+ }, { id: 'usa-story', text: CSS });
830
+ }
831
+
622
832
  /**
623
833
  * use-scroll-animate/components/effects — the 5.x effect packs, all
624
834
  * registered through `registerEffect()` (5.0) and playable with
@@ -649,6 +859,10 @@ function registerPhysicsEffects() {
649
859
  function registerPageEffects() {
650
860
  components_fx.registerEffects(EFFECT_PACKS.page);
651
861
  }
862
+ /** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
863
+ function defineEffectElements() {
864
+ defineStory();
865
+ }
652
866
  /** Register the built-ins and every pack (idempotent). */
653
867
  function registerAllEffects() {
654
868
  components_fx.registerBuiltinEffects();
@@ -661,7 +875,11 @@ exports.CLICK_FX = CLICK_FX;
661
875
  exports.EFFECT_PACKS = EFFECT_PACKS;
662
876
  exports.PAGE_FX = PAGE_FX;
663
877
  exports.PHYSICS_FX = PHYSICS_FX;
878
+ exports.STORY_TEMPLATES = STORY_TEMPLATES;
664
879
  exports.bounceKeyframes = bounceKeyframes;
880
+ exports.defineEffectElements = defineEffectElements;
881
+ exports.defineStory = defineStory;
882
+ exports.formatCount = formatCount;
665
883
  exports.fxLayer = fxLayer;
666
884
  exports.registerAllEffects = registerAllEffects;
667
885
  exports.registerCardClickEffects = registerCardClickEffects;
@@ -669,4 +887,5 @@ exports.registerPageEffects = registerPageEffects;
669
887
  exports.registerPhysicsEffects = registerPhysicsEffects;
670
888
  exports.solveSpring = solveSpring;
671
889
  exports.springKeyframes = springKeyframes;
890
+ exports.storyProgress = storyProgress;
672
891
  //# sourceMappingURL=effects.cjs.map