gclass-anims 1.0.0-beta.14 → 1.0.0-beta.15

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,10 +1,10 @@
1
1
  import initListeners from './Listeners'
2
2
 
3
3
  // localStorage key controlling whether the GSAP animation system is mounted.
4
- const STORAGE_KEY = 'funbyte-animations-enabled'
4
+ const STORAGE_KEY = 'gclass-animations-enabled'
5
5
  // localStorage key for a forced reduced-motion override (see
6
6
  // enableReducedMotion / disableReducedMotion).
7
- const REDUCED_KEY = 'funbyte-reduced-motion'
7
+ const REDUCED_KEY = 'gclass-reduced-motion'
8
8
 
9
9
  const reducedMotionQuery = typeof window !== 'undefined'
10
10
  ? window.matchMedia('(prefers-reduced-motion: reduce)')
package/Animations.js CHANGED
@@ -587,27 +587,43 @@ export function radiate (delay , target , amount , dur , ease , zIndex){
587
587
  tick = true
588
588
  requestAnimationFrame(() => { tick = false; applyRect() })
589
589
  }
590
+ // Don't animate detached targets — return inert tween
591
+ if (!target.isConnected) {
592
+ return gsap.fromTo(clone, {}, { duration: 0 })
593
+ }
590
594
  document.body.appendChild(clone)
591
595
  applyRect()
592
596
  window.addEventListener("scroll", schedule, { passive: true })
593
597
  window.addEventListener("resize", schedule, { passive: true })
594
598
  // Killing the tween (teardown, hover/click rebuilds) must clean up exactly
595
599
  // like natural completion - otherwise clones + listeners leak.
600
+ let tween = null
596
601
  const cleanup = () => {
597
602
  clone.remove()
598
603
  window.removeEventListener("scroll", schedule)
599
604
  window.removeEventListener("resize", schedule)
605
+ if (observer) observer.disconnect()
600
606
  }
607
+ // If target is removed from DOM (React unmount, .remove(), SPA navigation),
608
+ // kill the tween and remove the clone — mirrors React useEffect cleanup
609
+ const observer = new MutationObserver(() => {
610
+ if (!target.isConnected) {
611
+ if (tween) tween.kill()
612
+ cleanup()
613
+ }
614
+ })
615
+ observer.observe(document.body, { childList: true, subtree: true })
601
616
 
602
- return gsap.fromTo(clone , {scale:1 , opacity:1} , {
617
+ tween = gsap.fromTo(clone , {scale:1 , opacity:1} , {
603
618
  scale:amount / 10 ,
604
619
  opacity:0 ,
605
620
  duration:dur ,
606
621
  delay:delay ,
607
622
  ease:easeOf(ease) ,
608
- onComplete: cleanup ,
609
- onInterrupt: cleanup ,
623
+ onComplete: () => { observer.disconnect(); cleanup(); },
624
+ onInterrupt: () => { observer.disconnect(); cleanup(); },
610
625
  })
626
+ return tween
611
627
  }
612
628
 
613
629
 
package/Boot.html ADDED
@@ -0,0 +1,18 @@
1
+ <!-- Boot.html — GClass boot overlay template -->
2
+ <!-- 1. Put your boot HTML here. This file is scanned by Tailwind (v4 auto-detects *.html, v3 add "./Boot.html" to content[]) -->
3
+ <!-- so any Tailwind utility + gclass-anims class (e.g. .spawn-*, .time-*) placed here is preserved in the CSS build. -->
4
+ <!-- 2. Boot.js will fetch and inject this file if no #boot-overlay exists in the DOM. -->
5
+ <!-- 3. Keep the outer id="boot-overlay" and .boot-time-N (N=seconds visible) — Boot.js reads it via readBootTime(). -->
6
+
7
+ <div id="boot-overlay" class="boot-time-2 fixed inset-0 z-[9999] grid place-items-center bg-slate-950" style="position:fixed;inset:0;z-index:9999;display:grid;place-items:center;background:#020617;">
8
+ <div class="flex flex-col items-center gap-4 p-8 text-center">
9
+ <!-- gclass-anims classes work here — Config.js:76 wiring runs inside the overlay -->
10
+ <h1 class="spawn-up time-1 text-4xl font-bold tracking-tight text-cyan-200 radiate radiate-z-9999 compatibility">GClass</h1>
11
+ <p class="spawn-fade time-1 text-sm text-slate-400">GSAP utilities</p>
12
+
13
+ <!-- Tailwind / other CSS lib classes also work — they are detected because this *.html is in tailwind content -->
14
+ <div class="mt-2 h-1 w-32 overflow-hidden rounded-full bg-slate-800">
15
+
16
+ </div>
17
+ </div>
18
+ </div>
package/Boot.js ADDED
@@ -0,0 +1,145 @@
1
+ import { initAnimations } from './AnimToggle.js'
2
+
3
+ // Raw JS boot overlay — framework-agnostic, no React.
4
+ // Shows #boot-overlay once per session, delays main DOM wiring until finished,
5
+ // and keeps Config.js animations usable inside the overlay.
6
+ // Works with any framework (vanilla, React, Vue, Svelte, Next, Nuxt, Angular) and with/without Tailwind.
7
+
8
+ let hasBooted = false
9
+ const BOOT_KEY = 'gclass-boot-done' // kept for optional persistence, not used for gate
10
+
11
+ const readBootTime = (el, fallback) => {
12
+ const cls = [...el.classList].find(c => c.startsWith('boot-time-'))
13
+ return cls ? Number(cls.slice('boot-time-'.length)) : fallback
14
+ }
15
+
16
+ export async function initBoot({ enabled = false, time = 2, id = 'boot-overlay', onDone } = {}) {
17
+ if (typeof window === 'undefined') return () => {}
18
+ if (!enabled) {
19
+ initAnimations()
20
+ return () => {}
21
+ }
22
+ if (hasBooted) {
23
+ initAnimations()
24
+ return () => {}
25
+ }
26
+
27
+ let overlay = document.getElementById(id)
28
+ if (!overlay) {
29
+ const tryFetch = async (url) => {
30
+ try {
31
+ const res = await fetch(url)
32
+ if (res.ok) return await res.text()
33
+ } catch {}
34
+ return null
35
+ }
36
+ let html = await tryFetch(new URL('./Boot.html', import.meta.url).href)
37
+ if (!html) html = await tryFetch('/Boot.html')
38
+ if (!html) html = await tryFetch('./Boot.html')
39
+ if (html) {
40
+ const tpl = document.createElement('template')
41
+ tpl.innerHTML = html.trim()
42
+ const node = tpl.content.firstElementChild
43
+ if (node) {
44
+ document.body.prepend(node)
45
+ overlay = document.getElementById(id) ?? node
46
+ }
47
+ }
48
+ }
49
+ if (!overlay) {
50
+ initAnimations()
51
+ return () => {}
52
+ }
53
+
54
+ hasBooted = true
55
+ overlay.style.position = 'fixed'
56
+ overlay.style.inset = '0'
57
+ overlay.style.zIndex = '9999'
58
+ overlay.style.display = 'grid'
59
+ overlay.style.placeItems = 'center'
60
+ if (!overlay.style.background && !overlay.style.backgroundColor) {
61
+ overlay.style.background = '#020617'
62
+ }
63
+
64
+ // Isolate: detach all body content except overlay so first initAnimations() only wires overlay
65
+ // This works for vanilla <main>, React #root, Vue #app, Next #__next, Nuxt, Svelte, Angular
66
+ // Listeners.js scans body * regardless of display:none, so must physically detach
67
+ const toIsolate = [...document.body.children].filter(el => {
68
+ if (el.id === id) return false
69
+ if (el.tagName === 'SCRIPT' || el.tagName === 'STYLE' || el.tagName === 'TEMPLATE' || el.tagName === 'LINK') return false
70
+ if (el.id === 'next-route-announcer' || el.hasAttribute('data-nextjs')) return false
71
+ return true
72
+ })
73
+ const placements = toIsolate.map(el => ({ el, parent: el.parentNode, next: el.nextSibling }))
74
+ placements.forEach(({ el }) => el.remove())
75
+ const prevOverflow = document.body.style.overflow
76
+ document.body.style.overflow = 'hidden'
77
+
78
+ initAnimations()
79
+ placements.forEach(({ parent, next, el }) => {
80
+ if (!parent || el.isConnected) return
81
+ if (next && next.isConnected && next.parentNode === parent) {
82
+ parent.insertBefore(el, next)
83
+ } else {
84
+ parent.appendChild(el)
85
+ }
86
+ })
87
+
88
+ const duration = readBootTime(overlay, time)
89
+
90
+ const timer = setTimeout(() => {
91
+ overlay.style.transition = 'opacity 0.4s ease'
92
+ overlay.style.opacity = '0'
93
+ setTimeout(() => {
94
+ overlay.remove()
95
+ document.body.style.removeProperty('overflow')
96
+ if (prevOverflow) document.body.style.overflow = prevOverflow
97
+ initAnimations()
98
+ onDone?.()
99
+ }, 400)
100
+ }, duration * 1000)
101
+
102
+ return () => {
103
+ clearTimeout(timer)
104
+ document.body.style.removeProperty('overflow')
105
+ if (prevOverflow) document.body.style.overflow = prevOverflow
106
+ placements.forEach(({ el, parent, next }) => {
107
+ if (!el.isConnected && parent) {
108
+ if (next && next.isConnected && next.parentNode === parent) parent.insertBefore(el, next)
109
+ else parent.appendChild(el)
110
+ }
111
+ })
112
+ }
113
+ }
114
+
115
+ export async function createBootOverlay(html, opts = {}) {
116
+ if (document.getElementById(opts.id ?? 'boot-overlay')) return initBoot(opts)
117
+ if (!html) {
118
+ const tryFetch = async (url) => {
119
+ try { const r = await fetch(url); if (r.ok) return await r.text() } catch {}
120
+ return null
121
+ }
122
+ html = await tryFetch(new URL('./Boot.html', import.meta.url).href)
123
+ if (!html) html = await tryFetch('/Boot.html')
124
+ }
125
+ const div = document.createElement('div')
126
+ div.id = opts.id ?? 'boot-overlay'
127
+ if (!html) {
128
+ div.className = `boot-time-${opts.time ?? 2}`
129
+ div.style.cssText = 'position:fixed;inset:0;z-index:9999;display:grid;place-items:center;background:#020617;'
130
+ div.innerHTML = '<h1 class="spawn-up time-1">Loading...</h1>'
131
+ } else {
132
+ const tpl = document.createElement('template')
133
+ tpl.innerHTML = html.trim()
134
+ const node = tpl.content.firstElementChild
135
+ if (node) {
136
+ document.body.prepend(node)
137
+ return initBoot(opts)
138
+ }
139
+ div.innerHTML = html
140
+ }
141
+ document.body.prepend(div)
142
+ return initBoot(opts)
143
+ }
144
+
145
+ export default initBoot
package/index.d.ts CHANGED
@@ -219,4 +219,16 @@ export function radiate(delay: number, target: TweenTarget, amount: number, dur:
219
219
  export function hover(delay: number, target: TweenTarget, amount: number, dur: number, ease: string): any
220
220
  export function marquee(target: TweenTarget, dir: string, duration: number, xOffset?: number, yOffset?: number, noRepeat?: boolean): any
221
221
  export function flip(state: any, ease: string, dur: number): any
222
- export function animatecss(target: TweenTarget, dur: number, delay: number, ease: string, propertyS: any, propertySValue: any, propertyE: any, propertyEValue: any): any
222
+ export function animatecss(target: TweenTarget, dur: number, delay: number, ease: string, propertyS: any, propertySValue: any, propertyE: any, propertyEValue: any): any
223
+ // --- Boot ----------------------------------------------------------------
224
+
225
+ export interface BootOptions {
226
+ enabled?: boolean
227
+ time?: number
228
+ id?: string
229
+ onDone?: () => void
230
+ }
231
+ export function initBoot(opts?: BootOptions): Promise<() => void>
232
+ export function createBootOverlay(html?: string, opts?: BootOptions): Promise<() => void>
233
+ export default function Boot(opts?: BootOptions): Promise<() => void>
234
+
package/index.js CHANGED
@@ -2,4 +2,5 @@ export { initAnimations, toggleAnimations, enableReducedMotion, disableReducedMo
2
2
  export { default as initListeners, registerComplete } from './Listeners.js'
3
3
  export { customAnims } from './CustomAnims.js'
4
4
  export { defaults, animations, normalize } from './Config.js'
5
+ export { default as Boot, initBoot, createBootOverlay } from './Boot.js'
5
6
  export * from './Animations.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gclass-anims",
3
- "version": "1.0.0-beta.14",
3
+ "version": "1.0.0-beta.15",
4
4
  "description": "A Tailwind-style utility layer on top of GSAP. Framework-agnostic - works in vanilla JS, React, Vue, Svelte, or any bundler.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -21,6 +21,8 @@
21
21
  "Animations.js",
22
22
  "Config.js",
23
23
  "CustomAnims.js",
24
+ "Boot.js",
25
+ "Boot.html",
24
26
  "README.md",
25
27
  "LICENSE",
26
28
  "CHANGELOG.md"