gclass-anims 1.0.0-beta.21 → 1.0.0-beta.22.1

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/AnimToggle.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import initListeners from './Listeners.js'
2
2
  import { defaults, animations } from './Config.js'
3
- import gsap from 'gsap'
3
+ import { gsap } from 'gsap'
4
4
 
5
5
  // localStorage key controlling whether the GSAP animation system is mounted.
6
6
  const STORAGE_KEY = 'gclass-animations-enabled'
package/Animations.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DrawSVGPlugin, Flip, MotionPathPlugin, ScrambleTextPlugin, SplitText, TextPlugin } from "gsap/all";
2
- import gsap from "gsap";
2
+ import { gsap } from "gsap";
3
3
  import { defaults } from './Config.js'
4
4
 
5
5
  gsap.registerPlugin(Flip)
@@ -9,6 +9,44 @@ gsap.registerPlugin(DrawSVGPlugin)
9
9
  gsap.registerPlugin(MotionPathPlugin)
10
10
  gsap.registerPlugin(ScrambleTextPlugin)
11
11
 
12
+ // --- Breakpoint helpers for modifiers (xs/s/m/l/xl) -------------------------
13
+ // Lazy to avoid circular init (Config.js <-> Animations.js)
14
+ const getBpDataMod = () => {
15
+ const entries = Object.entries(defaults.breakpoints || {}).sort((a,b)=>a[1]-b[1])
16
+ const names = entries.map(([k])=>k)
17
+ const map = Object.fromEntries(entries)
18
+ const re = names.length ? new RegExp(`^(${names.join('|')}):(.+)$`) : /^$^/
19
+ return { entries, names, map, re }
20
+ }
21
+ const isBreakpointActiveMod = (bp) => {
22
+ if (!bp) return true
23
+ const { map } = getBpDataMod()
24
+ const px = map[bp]
25
+ if (px == null) return true
26
+ if (typeof window === 'undefined' || !window.matchMedia) return true
27
+ return window.matchMedia(`(min-width: ${px}px)`).matches
28
+ }
29
+ const hasGClassMod = (el, name) => {
30
+ if (el.classList.contains(name)) return true
31
+ const { names } = getBpDataMod()
32
+ for (const bp of names) if (el.classList.contains(`${bp}:${name}`) && isBreakpointActiveMod(bp)) return true
33
+ return false
34
+ }
35
+ const getActivePrefixedClassMod = (el, prefix) => {
36
+ const { names, map, re } = getBpDataMod()
37
+ let best = null, bestPx = -2
38
+ for (const c of el.classList) {
39
+ let bp = null, core = c
40
+ const m = c.match(re)
41
+ if (m) { bp = m[1]; core = m[2] }
42
+ if (!core.startsWith(prefix)) continue
43
+ if (bp && !isBreakpointActiveMod(bp)) continue
44
+ const px = bp ? map[bp] : -1
45
+ if (px > bestPx) { best = c; bestPx = px }
46
+ }
47
+ return best
48
+ }
49
+
12
50
  // A tasteful fallback whenever a call site omits an ease, so the animation
13
51
  // never lapses into the raw "none" look. Callers still override this freely.
14
52
  const DEFAULT_EASE = "power3.out";
@@ -210,9 +248,9 @@ export function countTargetVars (target){
210
248
  const end = match ? parseFloat(match[0]) : 0
211
249
  const decimals = match?.[0].includes(".") ? (match[0].split(".")[1] || "").length : 0
212
250
  // Count FROM `.spawn-num-N` (N = the starting number) up to `end`. When the
213
- // class is absent, fall back to 0.
214
- const startCls = [...target.classList].find(c => c.startsWith("spawn-num-"))
215
- const start = startCls ? parseFloat(startCls.slice("spawn-num-".length)) : 0
251
+ // class is absent, fall back to 0. Supports `m:spawn-num-10` etc. (mobile-first)
252
+ const startCls = getActivePrefixedClassMod(target, "spawn-num-")
253
+ const start = startCls ? parseFloat(startCls.slice(startCls.indexOf("spawn-num-") + "spawn-num-".length)) : 0
216
254
  return target._countTarget = { start, end, decimals }
217
255
  }
218
256
 
@@ -231,12 +269,12 @@ export function countUp (target , delay , dur, ease){
231
269
  // the interior fills. `fill-time-N` / `fill-ease-NAME` override the fill
232
270
  // phase; otherwise the fill takes half of `dur` and reuses the draw ease.
233
271
  const fillTimeOf = (el , fallback) => {
234
- const m = [...el.classList].find(c => c.startsWith("fill-time-"))
235
- return m ? Number(m.slice("fill-time-".length)) : fallback * 0.5
272
+ const m = getActivePrefixedClassMod(el, "fill-time-")
273
+ return m ? Number(m.slice(m.indexOf("fill-time-") + "fill-time-".length)) : fallback * 0.5
236
274
  }
237
275
  const fillEaseOf = (el , fallbackEase) => {
238
- const m = [...el.classList].find(c => c.startsWith("fill-ease-"))
239
- return m ? m.slice("fill-ease-".length) : fallbackEase
276
+ const m = getActivePrefixedClassMod(el, "fill-ease-")
277
+ return m ? m.slice(m.indexOf("fill-ease-") + "fill-ease-".length) : fallbackEase
240
278
  }
241
279
 
242
280
  // Stroke-draw reveal (strokes only - filled SVGs are deliberately out of
@@ -248,13 +286,13 @@ const fillEaseOf = (el , fallbackEase) => {
248
286
  export function drawsvg (target , delay , dur , ease){
249
287
  const e = easeOf(ease)
250
288
  const first = gsap.utils.toArray(target)[0]
251
- const hasFill = !!first?.classList?.contains("fill-svg")
289
+ const hasFill = !!first && hasGClassMod(first, "fill-svg")
252
290
  if (!hasFill) {
253
291
  return gsap.fromTo(target , {drawSVG:"0%"} , {ease:e , duration:dur , delay:delay , drawSVG:"100%"})
254
292
  }
255
293
  const fillDur = fillTimeOf(first , dur)
256
294
  const fillEase = fillEaseOf(first , ease)
257
- const fillTargets = gsap.utils.toArray(target).filter(el => el.classList.contains("fill-svg"))
295
+ const fillTargets = gsap.utils.toArray(target).filter(el => hasGClassMod(el, "fill-svg"))
258
296
  const tl = gsap.timeline({ delay })
259
297
  // Keep fill invisible while the stroke draws
260
298
  if (fillTargets.length) gsap.set(fillTargets , { fillOpacity: 0 })
@@ -332,7 +370,7 @@ export function splitPaths (paths){
332
370
  export function drawsvgSplit (target , delay , dur , ease){
333
371
  const e = easeOf(ease)
334
372
  const first = gsap.utils.toArray(target)[0]
335
- const hasFill = !!first?.classList?.contains("fill-svg")
373
+ const hasFill = !!first && hasGClassMod(first, "fill-svg")
336
374
  const fillDur = hasFill ? fillTimeOf(first , dur) : 0
337
375
  const fillEase = hasFill ? fillEaseOf(first , ease) : ease
338
376
  const tl = gsap.timeline({ delay })
@@ -397,13 +435,29 @@ export const scrambleSegments = (target) => {
397
435
  // .reveal-delay-N -> revealDelay in seconds (default defaults.revealDelay)
398
436
  // .chars-[...] -> character pool taken verbatim from inside the brackets
399
437
  // (default defaults.characterlist)
438
+ // Supports `m:amount-N`, `l:chars-[...]` etc. (largest active wins)
400
439
  export function scrambleVars (target){
401
440
  const num = (prefix , fallback) => {
402
- const match = [...target.classList].find(c => c.startsWith(prefix))
403
- return match ? Number(match.slice(prefix.length)) : fallback
441
+ const c = getActivePrefixedClassMod(target, prefix)
442
+ if (!c) return fallback
443
+ const idx = c.indexOf(prefix)
444
+ const n = Number(c.slice(idx + prefix.length))
445
+ return Number.isNaN(n) ? fallback : n
404
446
  }
405
447
  // Greedy up to the LAST "]" so pools containing "]" survive intact.
406
- const charsCls = [...target.classList].find(c => /^chars-\[(.*)\]$/.test(c))
448
+ // Check for `m:chars-[...]` then fallback to `chars-[...]`
449
+ let charsCls = null
450
+ let bestPx = -2
451
+ const { re: reChars, map: mapChars } = getBpDataMod()
452
+ for (const c of target.classList) {
453
+ let bp = null, core = c
454
+ const m = c.match(reChars)
455
+ if (m) { bp = m[1]; core = m[2] }
456
+ if (!/^chars-\[(.*)\]$/.test(core)) continue
457
+ if (bp && !isBreakpointActiveMod(bp)) continue
458
+ const px = bp ? mapChars[bp] : -1
459
+ if (px > bestPx) { charsCls = core; bestPx = px }
460
+ }
407
461
  return {
408
462
  segs: scrambleSegments(target) ,
409
463
  chars: charsCls ? charsCls.slice("chars-[".length , -1) : defaults.characterlist ,
@@ -411,7 +465,7 @@ export function scrambleVars (target){
411
465
  revealDelay: num("reveal-delay-" , defaults.revealDelay) ,
412
466
  // .scramble-rtl flips the reveal direction (ScrambleTextPlugin's
413
467
  // rightToLeft) so the sweep travels right -> left.
414
- rtl: target.classList.contains("scramble-rtl") ,
468
+ rtl: hasGClassMod(target, "scramble-rtl") ,
415
469
  }
416
470
  }
417
471
 
@@ -430,9 +484,9 @@ export function scrambleVars (target){
430
484
  // ScrambleTextPlugin resolve).
431
485
  // .scramble-rtl - reveal travels right -> left.
432
486
  export function scramble (target , delay , dur , ease){
433
- const e = [...target.classList].some(c => c.startsWith("ease-")) ? easeOf(ease) : "none"
487
+ const e = getActivePrefixedClassMod(target, "ease-") ? easeOf(ease) : "none"
434
488
  const { segs , chars , speed , revealDelay , rtl } = scrambleVars(target)
435
- const all = target.classList.contains("scramble-all")
489
+ const all = hasGClassMod(target, "scramble-all")
436
490
  const tl = gsap.timeline({ delay })
437
491
  segs.forEach(({ t , text }) => {
438
492
  if (all) {
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to `gclass-anims` will be documented in this file.
4
4
 
5
+ ## [1.0.0-beta.22.1] - 2026-09-06
6
+ - Fixed `.order` stagger grouping bug — `Listeners.js:76` `wrapQAll` now preserves DOM order via `body *` filter + `elementMatchesSel` (was `Set([...spawn-up], [...spawn-down])` grouping by type, now top-to-bottom as `qAll` does). Fixes `doc` `spawn` and landing `order` appearing all over.
7
+ - No API change from `beta.22`.
8
+
9
+ ## [1.0.0-beta.22] - 2026-09-06
10
+ - Added responsive breakpoints — `Config.js:75` `defaults.breakpoints {xs:475,s:640,m:768,l:1024,xl:1280}` (single-letter `s/m/l` avoids Tailwind `sm/md/lg` collision). Usage `m:spawn-up`, `l:float`, `xs:spawn-up`. Gating is live via `gsap.matchMedia` (`Listeners.js:26,202`).
11
+ - Added breakpoint-aware modifiers — `m:amount-20`, `m:time-2`, `m:ease-bounce`, `m:priority-3`, `m:chars-[...]`, `m:spawn-num-10` etc. Mobile-first largest active wins (`Listeners.js:237` `getActivePrefixedClass`, `Animations.js:13` lazy helpers). Covers `amount-`, `time-`, `priority-`, `edelay-`, `etime-`, `stagger-`, `fill-time-`, `reveal-delay-`, `spawn-num-`, `progress-start-`, etc.
12
+ - Fixed runtime `customAnims` re-normalization — `Listeners.js:190` now calls `normalize(customAnims)` inside `initListeners()` so `customAnims.push()` before next `init` is picked up.
13
+ - Added ESM + CJS dual build — `vite.lib.config.js` (Vite lib) builds `dist/gclass.esm.js` + `dist/gclass.cjs` (`gsap` external); `package.json:4` bumped to `beta.22`, `main/module` point to `dist/`, `exports: {import, require}`, `sideEffects:false`, `prepublishOnly: build`.
14
+ - Fixed CJS `gsap` interop — `Animations.js:2`/`Listeners.js:1`/`AnimToggle.js:3`/`CustomAnims.js:1` now `import {gsap} from 'gsap'` (named import) for correct `require('gsap').gsap` interop.
15
+ - Added `dev-react-strict` test harness — `vitest` + `jsdom` + `src/__tests__/breakpoints.test.jsx` (14 tests: gating, modifiers, Tailwind coexistence, StrictMode) + `BreakpointHarness.jsx` visual; `vite.config.js` test config.
16
+ - Documented breakpoints — `doc/src/app/documentation/responsive-design/page.js` (was duplicate `optimization`).
17
+
5
18
  ## [1.0.0-beta.21] - 2026-9-3
6
19
  - Added a `gclassOpts()` function that controls the animation fps and observer throttling
7
20
 
package/Config.js CHANGED
@@ -72,6 +72,7 @@ export const defaults = {
72
72
  revealDelay:0,
73
73
  characterlist:"AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz",
74
74
  bootTime: 5,
75
+ breakpoints : {xs : 475 , s:640 , m:768 , l:1024 , xl:1280}
75
76
  }
76
77
 
77
78
  export const animations = [
package/CustomAnims.js CHANGED
@@ -1,4 +1,4 @@
1
- import gsap from "gsap";
1
+ import { gsap } from "gsap";
2
2
 
3
3
  // A custom animation is just an entry with the SAME shape as one in
4
4
  // Listeners.js' `spawnConfigs`. Give it: