gclass-anims 1.0.0-beta.20 → 1.0.0-beta.22
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 +78 -6
- package/Animations.js +73 -19
- package/CHANGELOG.md +11 -0
- package/Config.js +1 -0
- package/CustomAnims.js +1 -1
- package/Listeners.js +381 -112
- package/dist/gclass.cjs +3268 -0
- package/dist/gclass.esm.js +3208 -0
- package/index.d.ts +27 -3
- package/index.js +1 -1
- package/package.json +12 -5
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'
|
|
@@ -118,6 +118,57 @@ let bootTimeout = null
|
|
|
118
118
|
let bootStyle = null
|
|
119
119
|
let hasBooted = false // true after first hard-load boot, skips boot on SPA path changes (remains false until first boot, resets on hard reload)
|
|
120
120
|
|
|
121
|
+
// Runtime config for gclassOpts — 0 = defaults (no throttle, default GSAP ticker)
|
|
122
|
+
let currentThrottle = 0
|
|
123
|
+
let currentFps = 0
|
|
124
|
+
const configSubscribers = new Set()
|
|
125
|
+
function getConfigSnapshot() { return { throttlePerFrame: currentThrottle, fps: currentFps } }
|
|
126
|
+
function emitConfig() { configSubscribers.forEach(fn => fn(getConfigSnapshot())) }
|
|
127
|
+
function applyFps(fps) {
|
|
128
|
+
const v = Number(fps) || 0
|
|
129
|
+
currentFps = v
|
|
130
|
+
if (v > 0) gsap.ticker.fps(v)
|
|
131
|
+
else gsap.ticker.fps(0) // 0 = remove cap, fallback to rAF (GSAP default)
|
|
132
|
+
emitConfig()
|
|
133
|
+
}
|
|
134
|
+
function normalizeGclassArgs(throttlePerFrame, fps) {
|
|
135
|
+
// support gclassOpts({throttlePerFrame, fps}) object overload
|
|
136
|
+
if (typeof throttlePerFrame === 'object' && throttlePerFrame !== null) {
|
|
137
|
+
fps = throttlePerFrame.fps
|
|
138
|
+
throttlePerFrame = throttlePerFrame.throttlePerFrame
|
|
139
|
+
}
|
|
140
|
+
const nextThrottle = throttlePerFrame == null ? 0 : Number(throttlePerFrame) || 0
|
|
141
|
+
const nextFps = fps == null ? 0 : Number(fps) || 0
|
|
142
|
+
return { nextThrottle, nextFps }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Change GClass runtime options on the fly without reload.
|
|
147
|
+
* gclassOpts(throttlePerFrame, fps) — both optional numbers.
|
|
148
|
+
* gclassOpts() or gclassOpts(undefined, undefined) resets to defaults (no throttle, default ticker).
|
|
149
|
+
* Also accepts gclassOpts({throttlePerFrame, fps}).
|
|
150
|
+
* Example low-end button: onClick={() => gclassOpts(1, 30)}
|
|
151
|
+
* Example reset: onClick={() => gclassOpts()} // 0, 60fps rAF
|
|
152
|
+
*/
|
|
153
|
+
export function gclassOpts(throttlePerFrame, fps) {
|
|
154
|
+
const { nextThrottle, nextFps } = normalizeGclassArgs(throttlePerFrame, fps)
|
|
155
|
+
currentThrottle = nextThrottle
|
|
156
|
+
applyFps(nextFps)
|
|
157
|
+
// re-wire observers with new throttle without full boot reload (if already running)
|
|
158
|
+
if (typeof window !== 'undefined' && cleanup && !bootTimeout) {
|
|
159
|
+
try { cleanup() } catch {}
|
|
160
|
+
cleanup = null
|
|
161
|
+
if (getEnabled()) cleanup = initListeners(document, currentThrottle)
|
|
162
|
+
}
|
|
163
|
+
// if boot is in progress we just store for next initAnimations; if not running, next initAnimations will use stored values
|
|
164
|
+
return getConfigSnapshot()
|
|
165
|
+
}
|
|
166
|
+
export function getGClassConfig() { return getConfigSnapshot() }
|
|
167
|
+
export function subscribeGClassConfig(cb) {
|
|
168
|
+
configSubscribers.add(cb)
|
|
169
|
+
return () => configSubscribers.delete(cb)
|
|
170
|
+
}
|
|
171
|
+
|
|
121
172
|
const readBootTime = (els, fallback) => {
|
|
122
173
|
let max = null
|
|
123
174
|
for (const el of els) {
|
|
@@ -136,7 +187,28 @@ const readBootTime = (els, fallback) => {
|
|
|
136
187
|
// Now also handles boot screen: any HTML/JSX with `.boot-up` anywhere is treated as the boot overlay.
|
|
137
188
|
// No separate initBoot needed - just call initAnimations().
|
|
138
189
|
// Boot stops all DOM rendering for defaults.bootTime (overwritten by boot-time-N class).
|
|
139
|
-
|
|
190
|
+
// throttlePerFrame / fps: forwarded to gclassOpts-equivalent runtime.
|
|
191
|
+
// initAnimations(throttlePerFrame, fps) — positional numbers, 0/undefined = defaults
|
|
192
|
+
// initAnimations({throttlePerFrame, fps}) — object overload
|
|
193
|
+
// initAnimations() — uses last gclassOpts values (defaults on first call)
|
|
194
|
+
export function initAnimations(throttlePerFrame, fps) {
|
|
195
|
+
// gclassOpts-style normalization: (throttle, fps) positional or {throttlePerFrame, fps} object
|
|
196
|
+
// no args -> fallback to last gclassOpts values (defaults 0 = no throttle, default ticker)
|
|
197
|
+
let effThrottle = currentThrottle
|
|
198
|
+
let effFps = currentFps
|
|
199
|
+
if (throttlePerFrame !== undefined || fps !== undefined) {
|
|
200
|
+
const { nextThrottle, nextFps } = normalizeGclassArgs(throttlePerFrame, fps)
|
|
201
|
+
effThrottle = nextThrottle
|
|
202
|
+
effFps = nextFps
|
|
203
|
+
currentThrottle = effThrottle
|
|
204
|
+
currentFps = effFps
|
|
205
|
+
if (effFps > 0) gsap.ticker.fps(effFps)
|
|
206
|
+
else gsap.ticker.fps(0)
|
|
207
|
+
emitConfig()
|
|
208
|
+
} else {
|
|
209
|
+
if (effFps > 0) gsap.ticker.fps(effFps)
|
|
210
|
+
else gsap.ticker.fps(0)
|
|
211
|
+
}
|
|
140
212
|
if (typeof window === 'undefined' || !getEnabled()) return
|
|
141
213
|
// boot already in progress (first mount in StrictMode) - ignore second mount
|
|
142
214
|
if (bootTimeout) {
|
|
@@ -194,7 +266,7 @@ export function initAnimations() {
|
|
|
194
266
|
bootEls.forEach(el => { el.style.visibility = 'visible' })
|
|
195
267
|
|
|
196
268
|
// animations inside boot screen must play while rest of DOM is hidden - init scoped to boot-up
|
|
197
|
-
bootCleanup = initListeners(bootEl)
|
|
269
|
+
bootCleanup = initListeners(bootEl, effThrottle)
|
|
198
270
|
|
|
199
271
|
bootTimeout = setTimeout(() => {
|
|
200
272
|
const bootEndCls = [...bootEl.classList].find(c => c.startsWith('boot-end-'))
|
|
@@ -205,7 +277,7 @@ export function initAnimations() {
|
|
|
205
277
|
bootStyle?.remove(); bootStyle = null
|
|
206
278
|
hideBootEls(bootEls)
|
|
207
279
|
bootTimeout = null
|
|
208
|
-
cleanup = initListeners()
|
|
280
|
+
cleanup = initListeners(document, effThrottle)
|
|
209
281
|
return
|
|
210
282
|
}
|
|
211
283
|
const name = bootEndCls.slice('boot-end-'.length) // e.g. spawn-blur
|
|
@@ -224,7 +296,7 @@ export function initAnimations() {
|
|
|
224
296
|
bootStyle?.remove(); bootStyle = null
|
|
225
297
|
hideBootEls(bootEls)
|
|
226
298
|
bootTimeout = null
|
|
227
|
-
cleanup = initListeners()
|
|
299
|
+
cleanup = initListeners(document, effThrottle)
|
|
228
300
|
}
|
|
229
301
|
|
|
230
302
|
if (!cfg || !from) {
|
|
@@ -238,5 +310,5 @@ export function initAnimations() {
|
|
|
238
310
|
return
|
|
239
311
|
}
|
|
240
312
|
|
|
241
|
-
cleanup = initListeners()
|
|
313
|
+
cleanup = initListeners(document, effThrottle)
|
|
242
314
|
}
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
403
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
487
|
+
const e = getActivePrefixedClassMod(target, "ease-") ? easeOf(ease) : "none"
|
|
434
488
|
const { segs , chars , speed , revealDelay , rtl } = scrambleVars(target)
|
|
435
|
-
const all = target
|
|
489
|
+
const all = hasGClassMod(target, "scramble-all")
|
|
436
490
|
const tl = gsap.timeline({ delay })
|
|
437
491
|
segs.forEach(({ t , text }) => {
|
|
438
492
|
if (all) {
|
|
@@ -591,7 +645,7 @@ export function radiate (delay , target , amount , dur , ease , zIndex){
|
|
|
591
645
|
tick = true
|
|
592
646
|
requestAnimationFrame(() => { tick = false; applyRect() })
|
|
593
647
|
}
|
|
594
|
-
// Don't animate detached targets
|
|
648
|
+
// Don't animate detached targets - return inert tween
|
|
595
649
|
if (!target.isConnected) {
|
|
596
650
|
return gsap.fromTo(clone, {}, { duration: 0 })
|
|
597
651
|
}
|
|
@@ -609,7 +663,7 @@ export function radiate (delay , target , amount , dur , ease , zIndex){
|
|
|
609
663
|
if (observer) observer.disconnect()
|
|
610
664
|
}
|
|
611
665
|
// If target is removed from DOM (React unmount, .remove(), SPA navigation),
|
|
612
|
-
// kill the tween and remove the clone
|
|
666
|
+
// kill the tween and remove the clone - mirrors React useEffect cleanup
|
|
613
667
|
const observer = new MutationObserver(() => {
|
|
614
668
|
if (!target.isConnected) {
|
|
615
669
|
if (tween) tween.kill()
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `gclass-anims` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.0.0-beta.22] - 2026-09-06
|
|
6
|
+
- 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`).
|
|
7
|
+
- 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.
|
|
8
|
+
- Fixed runtime `customAnims` re-normalization — `Listeners.js:190` now calls `normalize(customAnims)` inside `initListeners()` so `customAnims.push()` before next `init` is picked up.
|
|
9
|
+
- 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`.
|
|
10
|
+
- 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.
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
13
|
+
## [1.0.0-beta.21] - 2026-9-3
|
|
14
|
+
- Added a `gclassOpts()` function that controls the animation fps and observer throttling
|
|
15
|
+
|
|
5
16
|
## [1.0.0-beta.20] - 2026-9-2
|
|
6
17
|
- Fix ESM strict import: `AnimToggle.js` and `Listeners.js` now use `.js` extensions (`Remix`/`Qwik` Node ESM `Cannot find module` fix)
|
|
7
18
|
- Fix `Lit` shadow DOM `works.txt` `no` → light DOM default (`createRenderRoot(){return this}`) + `initListeners(shadowRoot)` docs
|
package/Config.js
CHANGED
package/CustomAnims.js
CHANGED