use-scroll-animate 5.7.0 → 5.8.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,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [5.8.0] - 2026-10-08
11
+
12
+ ### Added
13
+ - **Theme packs** (`use-scroll-animate/components/effects`): `neon`, `paper`, `glass`, `retro`, `brutalist` — each = design tokens (`--usa-theme-bg|fg|accent|accent-2|surface|border|radius|shadow|font`), motion tokens merged over the motion scale, and effect presets per role (`enter`, `hover`, `click`, `attention`, `background`). API: `THEMES`, `applyTheme(name, root?)` (on `<html>` also activates the motion tokens; on an element scopes them; returns an undo), `themeVars()`, `themeCss(name, selector?)` for static / SSR CSS, `themePreset(name, role)`, `playThemeEffect(el, role)`. Helper classes `.usa-surface`, `.usa-accent`.
14
+ - `<usa-theme name="…">` — a themed subtree; children with `data-theme-fx="click | hover | enter | attention"` get that role's preset.
15
+ - Theme effects: `neon-flicker` (dims, never blacks out; < 3 flashes / s), `paper-fold`, `glass-shine`, `retro-scanlines` (static under reduced motion), `brutal-shift`.
16
+ - **23 micro-interactions** (`MICRO_FX`, `registerMicroEffects()`), each doing the UI work as well as the motion: `copy-success` (clipboard + "Copied ✓"), `toggle-morph`, `password-reveal`, `favorite-star`, `like-heart`, `bookmark-flip` (all `aria-pressed`), `download-progress` / `submit-loading` (`aria-busy`, `usa-done`), `send-plane`, `add-to-cart`, `counter-bump`, `upvote`, `clap`, `emoji-react`, `refresh-spin`, `trash-shake` (`remove: true`), `check-toggle` (`aria-checked` on `role=checkbox`), `input-shake` (`aria-invalid`), `error-flash`, `success-check`, `nudge-hint`, `focus-pulse`, `notify-badge`. Helpers `togglePressed()`, `swapLabel()`, `bumpCount()`.
17
+ - Showcase: **Micro-interactions** and **Theme packs** cards.
18
+
19
+ ### Accessibility
20
+ - State changes (pressed, busy, invalid, counts, labels via a polite live region) happen with or without motion; under reduced motion only the animation is dropped.
21
+
10
22
  ## [5.7.0] - 2026-10-08
11
23
 
12
24
  ### Added
@@ -2,8 +2,8 @@
2
2
 
3
3
  var components_fx = require('./fx.cjs');
4
4
  var base = require('../chunks/base-B5i8qQPR.cjs');
5
+ var components_tokens = require('./tokens.cjs');
5
6
  require('../chunks/core-BDcszY4L.cjs');
6
- require('./tokens.cjs');
7
7
  require('../chunks/fx-B8hk1Fby.cjs');
8
8
 
9
9
  let layer = null;
@@ -1823,6 +1823,345 @@ function defineGestureFx(tag = 'usa-gesture-fx') {
1823
1823
  }, { id: 'usa-gesture-fx', text: 'usa-gesture-fx{display:inline-block;touch-action:none;user-select:none}' });
1824
1824
  }
1825
1825
 
1826
+ const saved = new WeakMap();
1827
+ /** Toggle `aria-pressed` (or set it) and return the new state. */
1828
+ function togglePressed(el, force) {
1829
+ const on = force ?? el.getAttribute('aria-pressed') !== 'true';
1830
+ el.setAttribute('aria-pressed', String(on));
1831
+ return on;
1832
+ }
1833
+ /** Swap `el`'s label for `ms` (polite live region), then restore it. */
1834
+ function swapLabel(el, text, ms) {
1835
+ if (!saved.has(el))
1836
+ saved.set(el, el.innerHTML);
1837
+ el.setAttribute('aria-live', 'polite');
1838
+ el.textContent = text;
1839
+ return new Promise((r) => setTimeout(() => {
1840
+ el.innerHTML = saved.get(el);
1841
+ saved.delete(el);
1842
+ el.removeAttribute('aria-live');
1843
+ r();
1844
+ }, ms));
1845
+ }
1846
+ /** Add `delta` to the number in `[data-count]` (or `el`), keeping it in `data-count`. Returns the new value. */
1847
+ function bumpCount(el, delta, ctx) {
1848
+ const t = el.querySelector('[data-count]') || el;
1849
+ const n = (Number(t.dataset.count ?? (t.textContent || '').replace(/[^\d.-]/g, '')) || 0) + delta;
1850
+ t.dataset.count = String(n);
1851
+ t.textContent = String(n);
1852
+ ctx?.animate(t, [{ transform: `translateY(${delta > 0 ? 60 : -60}%)`, opacity: 0 }, { transform: 'none', opacity: 1 }], { duration: 260, easing: 'cubic-bezier(.2,1.4,.4,1)' });
1853
+ return n;
1854
+ }
1855
+ const pop = (el, ctx, s = 1.25, d = 380) => ctx.animate(el, [{ transform: 'scale(1)' }, { transform: `scale(${s})`, offset: 0.4 }, { transform: 'scale(1)' }], { duration: d, easing: 'cubic-bezier(.2,1.4,.4,1)' });
1856
+ /** A few glyphs flying out of the pointer / centre. */
1857
+ function burst(el, ctx, glyph, color, n = 6, rise = false) {
1858
+ const { x, y } = origin(el, ctx);
1859
+ return all(Array.from({ length: n }, (_, i) => {
1860
+ const a = rise ? -Math.PI / 2 + rand(-0.6, 0.6) : (i / n) * Math.PI * 2;
1861
+ const d = rand(28, 56);
1862
+ return spawn(x - 7, y - 7, `font-size:14px;line-height:1;color:${color}`, ctx, [{ transform: 'translate(0,0) scale(.4)', opacity: 1 }, { transform: `translate(${Math.cos(a) * d}px,${Math.sin(a) * d}px) scale(1)`, opacity: 0 }], { duration: rand(500, 750), easing: 'cubic-bezier(.2,.8,.3,1)' }, glyph);
1863
+ }));
1864
+ }
1865
+ const click = (name, description, defaults, run) => ({ name, kind: 'click', description, defaults, run });
1866
+ const attn = (name, description, defaults, run) => ({ name, kind: 'attention', description, defaults, run });
1867
+ const MICRO_FX = [
1868
+ click('copy-success', 'Copies `text` (or `data-copy` / the target of `for`) to the clipboard and swaps the label to "Copied ✓".', { text: '', label: 'Copied ✓', ms: 1500 }, (el, o, ctx) => {
1869
+ const src = el.getAttribute('for') ? document.getElementById(el.getAttribute('for')) : null;
1870
+ const text = o.text || el.dataset.copy || src?.value || src?.textContent || '';
1871
+ navigator.clipboard?.writeText(text).catch(() => undefined);
1872
+ pop(el, ctx, 1.08);
1873
+ return swapLabel(el, o.label, o.ms);
1874
+ }),
1875
+ click('toggle-morph', 'Toggles `aria-pressed` with a squash-and-stretch morph.', {}, (el, _o, ctx) => {
1876
+ const on = togglePressed(el);
1877
+ return ctx.animate(el, [{ transform: 'scale(1,1)' }, { transform: `scale(${on ? 1.15 : 0.85},${on ? 0.85 : 1.15})`, offset: 0.35 }, { transform: 'scale(1,1)' }], { duration: 360, easing: 'ease-out' });
1878
+ }),
1879
+ click('password-reveal', 'Shows / hides the password input (`for` id, or the previous input) with an eye blink.', { show: 'Hide password', hide: 'Show password' }, (el, o, ctx) => {
1880
+ const input = (el.getAttribute('for') ? document.getElementById(el.getAttribute('for')) : el.previousElementSibling);
1881
+ if (!input || !('type' in input))
1882
+ return;
1883
+ const show = input.type === 'password';
1884
+ input.type = show ? 'text' : 'password';
1885
+ togglePressed(el, show);
1886
+ el.setAttribute('aria-label', show ? o.show : o.hide);
1887
+ return ctx.animate(el, [{ transform: 'scaleY(1)' }, { transform: 'scaleY(.1)', offset: 0.5 }, { transform: 'scaleY(1)' }], { duration: 240, easing: 'ease-in-out' });
1888
+ }),
1889
+ click('favorite-star', 'Toggles a favourite: the star pops and throws little stars (`color`).', { color: '#facc15' }, (el, o, ctx) => {
1890
+ const on = togglePressed(el);
1891
+ return Promise.all([pop(el, ctx, on ? 1.35 : 0.85)?.finished, on ? burst(el, ctx, '★', o.color) : null]);
1892
+ }),
1893
+ click('like-heart', 'Toggles a like: the heart beats and hearts float up (`color`).', { color: '#ff5c8a' }, (el, o, ctx) => {
1894
+ const on = togglePressed(el);
1895
+ return Promise.all([pop(el, ctx, on ? 1.3 : 0.9)?.finished, on ? burst(el, ctx, '♥', o.color, 5, true) : null]);
1896
+ }),
1897
+ click('bookmark-flip', 'Toggles a bookmark with a 3D flip.', {}, (el, _o, ctx) => {
1898
+ togglePressed(el);
1899
+ return ctx.animate(el, [{ transform: 'perspective(400px) rotateY(0)' }, { transform: 'perspective(400px) rotateY(180deg)' }, { transform: 'perspective(400px) rotateY(360deg)' }], { duration: 520, easing: 'ease-in-out' });
1900
+ }),
1901
+ click('download-progress', 'Fills a progress bar over `duration` ms, then shows "Done ✓" (`aria-busy` while running; fires `usa-done`).', { duration: 1600, label: 'Done ✓', color: '#34d399' }, (el, o, ctx) => {
1902
+ if (el.getAttribute('aria-busy') === 'true')
1903
+ return;
1904
+ el.setAttribute('aria-busy', 'true');
1905
+ const [bar, remove] = overlay(el, `background:${o.color}55;transform-origin:left;transform:scaleX(0)`);
1906
+ const a = ctx.animate(bar, [{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }], { duration: o.duration, easing: 'linear', fill: 'forwards' });
1907
+ const done = a ? a.finished.catch(() => undefined) : Promise.resolve();
1908
+ return done.then(() => {
1909
+ remove();
1910
+ el.removeAttribute('aria-busy');
1911
+ el.dispatchEvent(new CustomEvent('usa-done', { bubbles: true }));
1912
+ return swapLabel(el, o.label, 1400);
1913
+ });
1914
+ }),
1915
+ click('submit-loading', 'Shows "Sending…" with pulsing dots for `duration` ms, then "Sent ✓" (`aria-busy`, `usa-done`).', { duration: 1200, loading: 'Sending…', label: 'Sent ✓' }, (el, o, ctx) => {
1916
+ if (el.getAttribute('aria-busy') === 'true')
1917
+ return;
1918
+ el.setAttribute('aria-busy', 'true');
1919
+ const t = swapLabel(el, o.loading, o.duration);
1920
+ const a = ctx.animate(el, [{ opacity: 1 }, { opacity: 0.55 }, { opacity: 1 }], { duration: 600, iterations: Math.max(1, Math.round(o.duration / 600)) });
1921
+ return t.then(() => {
1922
+ a?.cancel();
1923
+ el.removeAttribute('aria-busy');
1924
+ el.dispatchEvent(new CustomEvent('usa-done', { bubbles: true }));
1925
+ return swapLabel(el, o.label, 1400);
1926
+ });
1927
+ }),
1928
+ click('send-plane', 'A paper plane ✈ takes off from the button.', { color: '#22d3ee' }, (el, o, ctx) => {
1929
+ const r = el.getBoundingClientRect();
1930
+ pop(el, ctx, 0.92, 200);
1931
+ return spawn(r.left + r.width / 2 - 8, r.top + r.height / 2 - 8, `font-size:16px;color:${o.color}`, ctx, [{ transform: 'translate(0,0) rotate(0)', opacity: 1 }, { transform: 'translate(40px,-10px) rotate(-10deg)', opacity: 1, offset: 0.4 }, { transform: 'translate(160px,-90px) rotate(-25deg)', opacity: 0 }], { duration: 800, easing: 'ease-in' }, '✈')?.finished;
1932
+ }),
1933
+ click('add-to-cart', 'Bumps `[data-count]` by one and floats a "+1" (`text`).', { text: '+1', color: '#34d399' }, (el, o, ctx) => {
1934
+ bumpCount(el, 1, ctx);
1935
+ const { x, y } = origin(el, ctx);
1936
+ return spawn(x - 10, y - 10, `font:700 14px system-ui;color:${o.color}`, ctx, [{ transform: 'translateY(0)', opacity: 1 }, { transform: 'translateY(-40px)', opacity: 0 }], { duration: 700, easing: 'ease-out' }, o.text)?.finished;
1937
+ }),
1938
+ click('counter-bump', 'Adds `step` to `[data-count]` with a rolling number.', { step: 1 }, (el, o, ctx) => void bumpCount(el, Number(o.step) || 1, ctx)),
1939
+ click('upvote', 'Toggles an upvote: the arrow nudges up and `[data-count]` ±1.', {}, (el, _o, ctx) => {
1940
+ const on = togglePressed(el);
1941
+ bumpCount(el, on ? 1 : -1, ctx);
1942
+ return ctx.animate(el, [{ transform: 'translateY(0)' }, { transform: `translateY(${on ? -6 : 4}px)`, offset: 0.4 }, { transform: 'translateY(0)' }], { duration: 320, easing: 'cubic-bezier(.2,1.4,.4,1)' });
1943
+ }),
1944
+ click('clap', 'Counts claps in `[data-count]` with a 👏 burst on every press.', {}, (el, _o, ctx) => {
1945
+ bumpCount(el, 1, ctx);
1946
+ return Promise.all([pop(el, ctx, 1.15, 240)?.finished, burst(el, ctx, '👏', 'inherit', 3, true)]);
1947
+ }),
1948
+ click('emoji-react', 'Pops one `emoji` up from the pointer (reaction button).', { emoji: '🎉' }, (el, o, ctx) => burst(el, ctx, o.emoji, 'inherit', 1, true)),
1949
+ click('refresh-spin', 'Spins the icon one turn (`turns`).', { turns: 1 }, (el, o, ctx) => ctx.animate(el, [{ transform: 'rotate(0)' }, { transform: `rotate(${360 * o.turns}deg)` }], { duration: 600 * o.turns, easing: 'cubic-bezier(.4,0,.2,1)' })),
1950
+ click('trash-shake', 'Shakes, then drops and fades (`remove: true` removes the element afterwards).', { remove: false }, (el, o, ctx) => {
1951
+ const a = ctx.animate(el, [{ transform: 'none', opacity: 1 }, { transform: 'rotate(-6deg)', offset: 0.15 }, { transform: 'rotate(6deg)', offset: 0.3 }, { transform: 'rotate(0)', offset: 0.45, opacity: 1 }, { transform: 'translateY(30px) scale(.8)', opacity: 0 }], { duration: 650, easing: 'ease-in', fill: 'forwards' });
1952
+ const end = () => (o.remove ? el.remove() : a?.cancel());
1953
+ return a ? a.finished.then(end, end) : void end();
1954
+ }),
1955
+ click('check-toggle', 'Toggles a check mark (`aria-checked` for role=checkbox, else `aria-pressed`) that scales in.', {}, (el, _o, ctx) => {
1956
+ const attr = el.getAttribute('role') === 'checkbox' ? 'aria-checked' : 'aria-pressed';
1957
+ const on = el.getAttribute(attr) !== 'true';
1958
+ el.setAttribute(attr, String(on));
1959
+ return ctx.animate(el, on ? [{ transform: 'scale(.6)' }, { transform: 'scale(1.15)', offset: 0.6 }, { transform: 'scale(1)' }] : [{ transform: 'scale(1)' }, { transform: 'scale(.85)' }, { transform: 'scale(1)' }], { duration: 280, easing: 'ease-out' });
1960
+ }),
1961
+ attn('input-shake', 'Shakes an invalid field side to side and sets `aria-invalid` (`color` outline flash).', { color: '#ff5c8a' }, (el, o, ctx) => {
1962
+ el.setAttribute('aria-invalid', 'true');
1963
+ const prev = el.style.outline;
1964
+ el.style.outline = `2px solid ${o.color}`;
1965
+ setTimeout(() => (el.style.outline = prev), 900);
1966
+ return ctx.animate(el, [0, -8, 8, -6, 6, -3, 0].map((x) => ({ transform: `translateX(${x}px)` })), { duration: 420, easing: 'ease-out' });
1967
+ }),
1968
+ attn('error-flash', 'Flashes the element red once (no more than one flash per call).', { color: '#ff5c8a' }, (el, o, ctx) => ctx.animate(el, [{ boxShadow: `0 0 0 0 ${o.color}00` }, { boxShadow: `0 0 0 4px ${o.color}`, offset: 0.3 }, { boxShadow: `0 0 0 0 ${o.color}00` }], { duration: 700 })),
1969
+ attn('success-check', 'A green ✓ badge pops over the element and fades.', { color: '#34d399' }, (el, o, ctx) => {
1970
+ const [b, remove] = overlay(el, `display:grid;place-items:center;font:700 22px system-ui;color:#fff;background:${o.color}d0`);
1971
+ b.textContent = '✓';
1972
+ const a = ctx.animate(b, [{ opacity: 0, transform: 'scale(.6)' }, { opacity: 1, transform: 'scale(1)', offset: 0.3 }, { opacity: 1, offset: 0.75 }, { opacity: 0 }], { duration: 1100, easing: 'ease-out' });
1973
+ return a ? a.finished.then(remove, remove) : void setTimeout(remove, 900);
1974
+ }),
1975
+ attn('nudge-hint', 'A small sideways nudge that says "try me" (`distance`).', { distance: 6 }, (el, o, ctx) => ctx.animate(el, [0, o.distance, 0, o.distance / 2, 0].map((x) => ({ transform: `translateX(${x}px)` })), { duration: 700, easing: 'ease-in-out' })),
1976
+ attn('focus-pulse', 'A ring pulses around the element to draw focus to it (`color`).', { color: '#7c5cff' }, (el, o, ctx) => ctx.animate(el, [{ boxShadow: `0 0 0 0 ${o.color}aa` }, { boxShadow: `0 0 0 12px ${o.color}00` }], { duration: 900, iterations: 2, easing: 'ease-out' })),
1977
+ attn('notify-badge', 'Bumps the badge count (`[data-count]`, `step`) with a pop — for new notifications.', { step: 1 }, (el, o, ctx) => {
1978
+ bumpCount(el, Number(o.step) || 1, ctx);
1979
+ return pop(el, ctx, 1.3, 320);
1980
+ }),
1981
+ ];
1982
+
1983
+ const THEME_ROLES = ['enter', 'hover', 'click', 'attention', 'background'];
1984
+ const P = (enter, hover, click, attention, background) => ({
1985
+ enter: { effect: enter },
1986
+ hover: { effect: hover },
1987
+ click: { effect: click },
1988
+ attention: { effect: attention },
1989
+ background: { effect: background },
1990
+ });
1991
+ const THEMES = {
1992
+ neon: {
1993
+ name: 'neon',
1994
+ vars: { bg: '#07070c', fg: '#e8e8ff', accent: '#22d3ee', 'accent-2': '#ff2bd6', surface: '#11111c', border: '1px solid #22d3ee', radius: '10px', shadow: '0 0 18px #22d3ee66, inset 0 0 12px #ff2bd622', font: 'ui-monospace, SFMono-Regular, Menlo, monospace' },
1995
+ motion: { duration: { fast: 120, normal: 220 }, easing: { standard: 'cubic-bezier(0.2, 0, 0, 1)' } },
1996
+ presets: P('fade-up', 'neon-flicker', 'shockwave', 'neon-flicker', 'starfield'),
1997
+ },
1998
+ paper: {
1999
+ name: 'paper',
2000
+ vars: { bg: '#f6f1e7', fg: '#2b2721', accent: '#c2410c', 'accent-2': '#0f766e', surface: '#fffdf8', border: '1px solid #e3dccd', radius: '4px', shadow: '0 1px 0 #0000000d, 0 10px 24px -14px #00000040', font: 'Georgia, "Times New Roman", serif' },
2001
+ motion: { duration: { fast: 200, normal: 380, slow: 700 }, easing: { standard: 'cubic-bezier(0, 0, 0, 1)' } },
2002
+ presets: P('paper-fold', 'wiggle', 'ink-splash', 'nudge-hint', 'contours'),
2003
+ },
2004
+ glass: {
2005
+ name: 'glass',
2006
+ vars: { bg: 'linear-gradient(135deg, #1e1b4b, #0e7490)', fg: '#f8fafc', accent: '#a5f3fc', 'accent-2': '#c4b5fd', surface: '#ffffff1f', border: '1px solid #ffffff40', radius: '18px', shadow: '0 8px 32px #0000003d', font: 'system-ui, -apple-system, "Segoe UI", sans-serif' },
2007
+ motion: { duration: { normal: 320 }, easing: { standard: 'cubic-bezier(0.22, 1, 0.36, 1)' } },
2008
+ presets: P('blur', 'glass-shine', 'ripple', 'focus-pulse', 'mesh-gradient'),
2009
+ },
2010
+ retro: {
2011
+ name: 'retro',
2012
+ vars: { bg: '#1d1135', fg: '#ffe9a8', accent: '#ff6b35', 'accent-2': '#2ec4b6', surface: '#2a1b4d', border: '3px solid #ffe9a8', radius: '0', shadow: '4px 4px 0 #ff6b35', font: '"Courier New", ui-monospace, monospace' },
2013
+ motion: { duration: { normal: 300 }, easing: { standard: 'steps(6, end)' } },
2014
+ presets: P('clip-up', 'rubber-band', 'star-burst', 'tada', 'retro-scanlines'),
2015
+ },
2016
+ brutalist: {
2017
+ name: 'brutalist',
2018
+ vars: { bg: '#ffffff', fg: '#000000', accent: '#ff3b00', 'accent-2': '#0047ff', surface: '#fff200', border: '3px solid #000000', radius: '0', shadow: '6px 6px 0 #000000', font: '"Arial Black", Arial, sans-serif' },
2019
+ motion: { duration: { fast: 80, normal: 160 }, easing: { standard: 'linear' } },
2020
+ presets: P('drop-bounce', 'jelly', 'brutal-shift', 'shake', 'voronoi'),
2021
+ },
2022
+ };
2023
+ const THEME_NAMES = Object.keys(THEMES);
2024
+ const BASE_CSS = `[data-usa-theme]{background:var(--usa-theme-bg);color:var(--usa-theme-fg);font-family:var(--usa-theme-font)}
2025
+ [data-usa-theme] .usa-surface{background:var(--usa-theme-surface);border:var(--usa-theme-border);border-radius:var(--usa-theme-radius);box-shadow:var(--usa-theme-shadow)}
2026
+ [data-usa-theme] .usa-accent{color:var(--usa-theme-accent)}
2027
+ [data-usa-theme=glass] .usa-surface{-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}`;
2028
+ const pack = (t) => {
2029
+ const p = typeof t === 'string' ? THEMES[t] : t;
2030
+ if (!p)
2031
+ throw new Error(`[use-scroll-animate] unknown theme "${String(t)}" — ${THEME_NAMES.join(', ')}`);
2032
+ return p;
2033
+ };
2034
+ /** The CSS custom properties of a theme (design + motion tokens). */
2035
+ function themeVars(t) {
2036
+ const p = pack(t);
2037
+ const out = {};
2038
+ for (const [k, v] of Object.entries(p.vars))
2039
+ out[`--usa-theme-${k}`] = v;
2040
+ return { ...out, ...components_tokens.motionTokensToVars(components_tokens.mergeMotionTokens(p.motion)) };
2041
+ }
2042
+ /** A theme as a CSS rule (`selector` default `[data-usa-theme=<name>]`) — for SSR / static CSS. */
2043
+ function themeCss(t, selector) {
2044
+ const p = pack(t);
2045
+ return `${selector || `[data-usa-theme=${p.name}]`}{${Object.entries(themeVars(p)).map(([k, v]) => `${k}:${v}`).join(';')}}`;
2046
+ }
2047
+ /** Apply a theme to `root` (default `<html>`). Returns an undo. */
2048
+ function applyTheme(t, root) {
2049
+ const p = pack(t);
2050
+ const el = root || document.documentElement;
2051
+ base.adoptStyles('usa-theme', BASE_CSS);
2052
+ const prevAttr = el.getAttribute('data-usa-theme');
2053
+ el.setAttribute('data-usa-theme', p.name);
2054
+ const vars = Object.entries(p.vars).map(([k, v]) => [`--usa-theme-${k}`, v]);
2055
+ const old = vars.map(([k]) => [k, el.style.getPropertyValue(k)]);
2056
+ for (const [k, v] of vars)
2057
+ el.style.setProperty(k, v);
2058
+ let undoMotion;
2059
+ if (el === document.documentElement)
2060
+ undoMotion = components_tokens.applyMotionTokens(p.motion, el);
2061
+ else {
2062
+ const mv = Object.entries(components_tokens.motionTokensToVars(components_tokens.mergeMotionTokens(p.motion)));
2063
+ const mold = mv.map(([k]) => [k, el.style.getPropertyValue(k)]);
2064
+ for (const [k, v] of mv)
2065
+ el.style.setProperty(k, v);
2066
+ undoMotion = () => mold.forEach(([k, v]) => (v ? el.style.setProperty(k, v) : el.style.removeProperty(k)));
2067
+ }
2068
+ return () => {
2069
+ undoMotion();
2070
+ old.forEach(([k, v]) => (v ? el.style.setProperty(k, v) : el.style.removeProperty(k)));
2071
+ if (prevAttr === null)
2072
+ el.removeAttribute('data-usa-theme');
2073
+ else
2074
+ el.setAttribute('data-usa-theme', prevAttr);
2075
+ };
2076
+ }
2077
+ /** The effect preset of a theme for a role. */
2078
+ function themePreset(t, role) {
2079
+ return pack(t).presets[role];
2080
+ }
2081
+ /** Play the preset for `role` of the theme on the closest `[data-usa-theme]` (or `theme`). */
2082
+ function playThemeEffect(el, role, theme) {
2083
+ const name = theme || el.closest('[data-usa-theme]')?.getAttribute('data-usa-theme') || 'neon';
2084
+ const pr = themePreset(name, role);
2085
+ return components_fx.playEffect(el, pr.effect, pr.options);
2086
+ }
2087
+ const THEME_FX = [
2088
+ {
2089
+ name: 'neon-flicker',
2090
+ kind: 'attention',
2091
+ description: 'A neon tube flickering on — dims (never blacks out) twice, well under 3 flashes per second.',
2092
+ defaults: { color: '#22d3ee' },
2093
+ run: (el, o, ctx) => ctx.animate(el, [{ opacity: 1, filter: 'none' }, { opacity: 0.55, offset: 0.2 }, { opacity: 1, filter: `drop-shadow(0 0 6px ${o.color})`, offset: 0.35 }, { opacity: 0.7, offset: 0.6 }, { opacity: 1, filter: `drop-shadow(0 0 10px ${o.color})` }], { duration: 900, easing: 'linear' }),
2094
+ },
2095
+ {
2096
+ name: 'paper-fold',
2097
+ kind: 'enter',
2098
+ description: 'Unfolds like a sheet of paper hinged at the top.',
2099
+ defaults: { duration: 600 },
2100
+ run: (el, o, ctx) => ctx.animate(el, [{ transform: 'perspective(800px) rotateX(-85deg)', transformOrigin: 'top', opacity: 0 }, { transform: 'perspective(800px) rotateX(12deg)', transformOrigin: 'top', opacity: 1, offset: 0.7 }, { transform: 'perspective(800px) rotateX(0)', transformOrigin: 'top', opacity: 1 }], { duration: o.duration, easing: 'ease-out', fill: 'backwards' }),
2101
+ },
2102
+ {
2103
+ name: 'glass-shine',
2104
+ kind: 'hover',
2105
+ description: 'A bright diagonal shine sweeps across frosted glass.',
2106
+ defaults: { duration: 700 },
2107
+ run: (el, o, ctx) => {
2108
+ const [s, remove] = overlay(el, 'overflow:hidden;background:linear-gradient(105deg,transparent 35%,#ffffff8c 50%,transparent 65%);background-size:250% 100%;background-position:120% 0');
2109
+ const a = ctx.animate(s, [{ backgroundPosition: '120% 0' }, { backgroundPosition: '-20% 0' }], { duration: o.duration, easing: 'ease-in-out' });
2110
+ return a ? a.finished.then(remove, remove) : void remove();
2111
+ },
2112
+ },
2113
+ {
2114
+ name: 'retro-scanlines',
2115
+ kind: 'background',
2116
+ description: 'CRT scanlines with a slow roll (static lines under reduced motion; persistent).',
2117
+ reduced: 'run',
2118
+ defaults: { opacity: 0.18 },
2119
+ run: (el, o, ctx) => {
2120
+ const [s, remove] = overlay(el, `opacity:${o.opacity};background:repeating-linear-gradient(0deg,#000 0 1px,transparent 1px 3px);mix-blend-mode:multiply`);
2121
+ const a = ctx.reduced ? null : ctx.animate(s, [{ backgroundPosition: '0 0' }, { backgroundPosition: '0 30px' }], { duration: 2400, iterations: Infinity, easing: 'linear' });
2122
+ return () => {
2123
+ a?.cancel();
2124
+ remove();
2125
+ };
2126
+ },
2127
+ },
2128
+ {
2129
+ name: 'brutal-shift',
2130
+ kind: 'click',
2131
+ description: 'Presses into its hard drop shadow and springs back (brutalist buttons).',
2132
+ defaults: { offset: 6 },
2133
+ run: (el, o, ctx) => ctx.animate(el, [{ transform: 'translate(0,0)' }, { transform: `translate(${o.offset}px,${o.offset}px)`, boxShadow: '0 0 0 #000', offset: 0.3 }, { transform: 'translate(0,0)' }], { duration: 260, easing: 'ease-out' }),
2134
+ },
2135
+ ];
2136
+ /** `<usa-theme name="neon | paper | glass | retro | brutalist">` — a themed subtree. */
2137
+ function defineTheme(tag = 'usa-theme') {
2138
+ return base.defineElement(tag, (Base) => class UsaTheme extends Base {
2139
+ static get observedAttributes() {
2140
+ return ['name'];
2141
+ }
2142
+ get theme() {
2143
+ const n = this.str('name', 'neon');
2144
+ return THEMES[n] ? n : 'neon';
2145
+ }
2146
+ mount() {
2147
+ this.onCleanup(applyTheme(this.theme, this));
2148
+ this.querySelectorAll('[data-theme-fx]').forEach((el) => {
2149
+ const role = (el.dataset.themeFx || 'click');
2150
+ if (!THEME_ROLES.includes(role))
2151
+ return;
2152
+ const pr = themePreset(this.theme, role);
2153
+ const trigger = role === 'attention' ? 'click' : role === 'background' ? 'load' : role;
2154
+ try {
2155
+ this.onCleanup(components_fx.bindEffect(el, pr.effect, { ...(pr.options || {}), trigger }));
2156
+ }
2157
+ catch {
2158
+ /* effect not registered: call registerAllEffects() */
2159
+ }
2160
+ });
2161
+ }
2162
+ }, { id: 'usa-theme-el', text: 'usa-theme{display:block}' });
2163
+ }
2164
+
1826
2165
  /**
1827
2166
  * use-scroll-animate/components/effects — the 5.x effect packs, all
1828
2167
  * registered through `registerEffect()` (5.0) and playable with
@@ -1843,6 +2182,8 @@ const EFFECT_PACKS = {
1843
2182
  generative: GENERATIVE_FX,
1844
2183
  audio: AUDIO_FX,
1845
2184
  cursor: CURSOR_FX,
2185
+ micro: MICRO_FX,
2186
+ themes: THEME_FX,
1846
2187
  };
1847
2188
  /** 5.1: card & click effects 2.0. */
1848
2189
  function registerCardClickEffects() {
@@ -1868,11 +2209,17 @@ function registerAudioEffects() {
1868
2209
  function registerCursorEffects() {
1869
2210
  components_fx.registerEffects(EFFECT_PACKS.cursor);
1870
2211
  }
2212
+ /** 5.8: micro-interactions + theme-pack effects. */
2213
+ function registerMicroEffects() {
2214
+ components_fx.registerEffects(EFFECT_PACKS.micro);
2215
+ components_fx.registerEffects(EFFECT_PACKS.themes);
2216
+ }
1871
2217
  /** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
1872
2218
  function defineEffectElements() {
1873
2219
  defineStory();
1874
2220
  defineAudio();
1875
2221
  defineGestureFx();
2222
+ defineTheme();
1876
2223
  }
1877
2224
  /** Register the built-ins and every pack (idempotent). */
1878
2225
  function registerAllEffects() {
@@ -1888,19 +2235,27 @@ exports.CURSOR_FX = CURSOR_FX;
1888
2235
  exports.EFFECT_PACKS = EFFECT_PACKS;
1889
2236
  exports.GENERATIVE_FX = GENERATIVE_FX;
1890
2237
  exports.GESTURES = GESTURES;
2238
+ exports.MICRO_FX = MICRO_FX;
1891
2239
  exports.PAGE_FX = PAGE_FX;
1892
2240
  exports.PHYSICS_FX = PHYSICS_FX;
1893
2241
  exports.STORY_TEMPLATES = STORY_TEMPLATES;
2242
+ exports.THEMES = THEMES;
2243
+ exports.THEME_FX = THEME_FX;
2244
+ exports.THEME_NAMES = THEME_NAMES;
2245
+ exports.THEME_ROLES = THEME_ROLES;
1894
2246
  exports.angleDelta = angleDelta;
2247
+ exports.applyTheme = applyTheme;
1895
2248
  exports.bindBeat = bindBeat;
1896
2249
  exports.bindGesture = bindGesture;
1897
2250
  exports.bounceKeyframes = bounceKeyframes;
2251
+ exports.bumpCount = bumpCount;
1898
2252
  exports.canvasBackground = canvasBackground;
1899
2253
  exports.createBeatDetector = createBeatDetector;
1900
2254
  exports.defineAudio = defineAudio;
1901
2255
  exports.defineEffectElements = defineEffectElements;
1902
2256
  exports.defineGestureFx = defineGestureFx;
1903
2257
  exports.defineStory = defineStory;
2258
+ exports.defineTheme = defineTheme;
1904
2259
  exports.disableAudio = disableAudio;
1905
2260
  exports.enableAudio = enableAudio;
1906
2261
  exports.flingVelocity = flingVelocity;
@@ -1910,14 +2265,21 @@ exports.getAudio = getAudio;
1910
2265
  exports.hexRgb = hexRgb;
1911
2266
  exports.noise2 = noise2;
1912
2267
  exports.onBeat = onBeat;
2268
+ exports.playThemeEffect = playThemeEffect;
1913
2269
  exports.registerAllEffects = registerAllEffects;
1914
2270
  exports.registerAudioEffects = registerAudioEffects;
1915
2271
  exports.registerCardClickEffects = registerCardClickEffects;
1916
2272
  exports.registerCursorEffects = registerCursorEffects;
1917
2273
  exports.registerGenerativeEffects = registerGenerativeEffects;
2274
+ exports.registerMicroEffects = registerMicroEffects;
1918
2275
  exports.registerPageEffects = registerPageEffects;
1919
2276
  exports.registerPhysicsEffects = registerPhysicsEffects;
1920
2277
  exports.solveSpring = solveSpring;
1921
2278
  exports.springKeyframes = springKeyframes;
1922
2279
  exports.storyProgress = storyProgress;
2280
+ exports.swapLabel = swapLabel;
2281
+ exports.themeCss = themeCss;
2282
+ exports.themePreset = themePreset;
2283
+ exports.themeVars = themeVars;
2284
+ exports.togglePressed = togglePressed;
1923
2285
  //# sourceMappingURL=effects.cjs.map