@brandocms/jupiter 4.0.0-beta.1 → 4.0.0-beta.2

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.
Files changed (50) hide show
  1. package/README.md +191 -2
  2. package/package.json +20 -18
  3. package/src/index.js +10 -10
  4. package/src/modules/Application/index.js +203 -157
  5. package/src/modules/Cookies/index.js +34 -55
  6. package/src/modules/CoverOverlay/index.js +20 -13
  7. package/src/modules/Dataloader/index.js +71 -24
  8. package/src/modules/Dataloader/url-sync.js +238 -0
  9. package/src/modules/Dom/index.js +18 -0
  10. package/src/modules/DoubleHeader/index.js +571 -0
  11. package/src/modules/Dropdown/index.js +101 -75
  12. package/src/modules/EqualHeightElements/index.js +5 -7
  13. package/src/modules/EqualHeightImages/index.js +7 -2
  14. package/src/modules/FixedHeader/index.js +60 -30
  15. package/src/modules/FooterReveal/index.js +3 -3
  16. package/src/modules/HeroSlider/index.js +207 -91
  17. package/src/modules/HeroVideo/index.js +15 -27
  18. package/src/modules/Lazyload/index.js +101 -80
  19. package/src/modules/Lightbox/index.js +17 -55
  20. package/src/modules/Links/index.js +54 -49
  21. package/src/modules/Looper/index.js +1737 -0
  22. package/src/modules/Marquee/index.js +106 -37
  23. package/src/modules/MobileMenu/index.js +70 -124
  24. package/src/modules/Moonwalk/index.js +349 -150
  25. package/src/modules/Popover/index.js +186 -28
  26. package/src/modules/Popup/index.js +27 -34
  27. package/src/modules/StackedBoxes/index.js +3 -3
  28. package/src/modules/StickyHeader/index.js +364 -155
  29. package/src/modules/Toggler/index.js +184 -27
  30. package/src/utils/motion-helpers.js +330 -0
  31. package/types/index.d.ts +1 -30
  32. package/types/modules/Application/index.d.ts +6 -6
  33. package/types/modules/Breakpoints/index.d.ts +2 -0
  34. package/types/modules/Dataloader/index.d.ts +5 -2
  35. package/types/modules/Dataloader/url-sync.d.ts +36 -0
  36. package/types/modules/Dom/index.d.ts +7 -0
  37. package/types/modules/DoubleHeader/index.d.ts +63 -0
  38. package/types/modules/Dropdown/index.d.ts +7 -30
  39. package/types/modules/EqualHeightImages/index.d.ts +1 -1
  40. package/types/modules/FixedHeader/index.d.ts +1 -1
  41. package/types/modules/Lazyload/index.d.ts +9 -9
  42. package/types/modules/Lightbox/index.d.ts +0 -5
  43. package/types/modules/Looper/index.d.ts +127 -0
  44. package/types/modules/Moonwalk/index.d.ts +6 -15
  45. package/types/modules/Parallax/index.d.ts +10 -32
  46. package/types/modules/Popover/index.d.ts +12 -0
  47. package/types/modules/Popup/index.d.ts +6 -19
  48. package/types/modules/ScrollSpy/index.d.ts +1 -1
  49. package/types/modules/StickyHeader/index.d.ts +171 -14
  50. package/types/modules/Toggler/index.d.ts +24 -2
@@ -9,13 +9,12 @@
9
9
  *
10
10
  */
11
11
 
12
- import { gsap, CSSPlugin } from 'gsap/all'
12
+ import { animate } from 'motion'
13
13
  import _defaultsDeep from 'lodash.defaultsdeep'
14
14
  import prefersReducedMotion from '../../utils/prefersReducedMotion'
15
15
  import * as Events from '../../events'
16
16
  import imageIsLoaded from '../../utils/imageIsLoaded'
17
-
18
- gsap.registerPlugin(CSSPlugin)
17
+ import { set, delayedCall } from '../../utils/motion-helpers'
19
18
 
20
19
  const DEFAULT_OPTIONS = {
21
20
  el: '[data-hero-slider]',
@@ -47,19 +46,20 @@ const DEFAULT_OPTIONS = {
47
46
  onInitialize: (/* hs */) => {},
48
47
 
49
48
  onFadeIn: (hs, callback) => {
50
- if (hs.slides.length > 1) {
51
- gsap.to(hs.el, {
52
- duration: 0.25,
53
- opacity: 1,
54
- onComplete: () => {
55
- callback()
56
- },
57
- })
58
- } else {
59
- gsap.to(hs.el, {
60
- duration: 0.25,
61
- opacity: 1,
62
- })
49
+ // Get the first slide's image to start zooming during reveal
50
+ const firstSlideImg = hs.slides[hs._currentSlideIdx].querySelector('.hero-slide-img')
51
+
52
+ // Fade in the container
53
+ animate(hs.el, { opacity: 1 }, { duration: 0.25 })
54
+
55
+ // Start zoom immediately as container fades in
56
+ if (firstSlideImg && hs.slides.length > 1) {
57
+ // Animate with linear easing - MUST specify type: "tween"!
58
+ animate(
59
+ firstSlideImg,
60
+ { scale: [1, hs.opts.transition.scale] },
61
+ { type: "tween", duration: hs.opts.interval, ease: "linear" }
62
+ ).finished.then(callback)
63
63
  }
64
64
  },
65
65
  }
@@ -84,8 +84,9 @@ export default class HeroSlider {
84
84
 
85
85
  initialize() {
86
86
  this._addResizeHandler()
87
+ this._addVisibilityHandler()
87
88
  // style the container
88
- gsap.set(this.el, {
89
+ set(this.el, {
89
90
  position: 'absolute',
90
91
  top: 0,
91
92
  left: 0,
@@ -101,25 +102,30 @@ export default class HeroSlider {
101
102
  this._currentSlideIdx = this.opts.initialSlideNumber
102
103
 
103
104
  // style the slides
104
- Array.from(this.slides).forEach((s) => {
105
- gsap.set(s, {
105
+ Array.from(this.slides).forEach((s, index) => {
106
+ set(s, {
106
107
  zIndex: this.opts.zIndex.regular,
107
108
  position: 'absolute',
108
109
  top: 0,
109
110
  left: 0,
110
111
  width: '100%',
111
112
  height: '100%',
113
+ opacity: index === 0 ? 1 : 0, // Only first slide visible initially
112
114
  })
113
115
 
114
116
  const img = s.querySelector('.hero-slide-img')
115
117
 
116
118
  if (img) {
117
- gsap.set(img, {
119
+ set(img, {
118
120
  width: document.body.clientWidth,
119
121
  height: '100%',
120
122
  top: 0,
121
123
  left: 0,
122
124
  position: 'absolute',
125
+ transition: 'none',
126
+ transformOrigin: 'center center',
127
+ willChange: 'transform',
128
+ scale: 1, // Initialize scale
123
129
  })
124
130
  } else {
125
131
  console.error(
@@ -128,6 +134,7 @@ export default class HeroSlider {
128
134
  }
129
135
  })
130
136
 
137
+ // Set proper z-indexes for first two slides
131
138
  this.slides[0].style.zIndex = this.opts.zIndex.visible
132
139
  if (this.slides[1]) {
133
140
  this.slides[1].style.zIndex = this.opts.zIndex.next
@@ -181,87 +188,141 @@ export default class HeroSlider {
181
188
  * Switches between slides
182
189
  */
183
190
  slide(type) {
184
- const timeline = gsap.timeline()
185
-
186
191
  switch (type) {
187
192
  case 'fade':
188
- timeline
189
- .set(this._currentSlide, {
193
+ {
194
+ // Setup: set current slide invisible at correct z-index
195
+ set(this._currentSlide, {
190
196
  opacity: 0,
191
- scale: 1,
192
197
  zIndex: this.opts.zIndex.visible,
193
198
  })
194
- .set(this._nextSlide, {
195
- opacity: 0,
196
- })
197
- .to(this._previousSlide, {
198
- duration: this.opts.interval,
199
- scale: this.opts.transition.scale,
200
- })
201
- .to(this._currentSlide, {
202
- duration: this.opts.transition.duration,
203
- opacity: 1,
204
- delay: this.opts.interval - this.opts.transition.duration,
205
- force3D: true,
206
- ease: 'sine.inOut',
207
- })
208
- .set(this._previousSlide, {
209
- opacity: 0,
199
+ set(this._nextSlide, { opacity: 0 })
200
+
201
+ // Get the images to animate
202
+ const previousSlideImg = this._previousSlide.querySelector('.hero-slide-img')
203
+ const currentSlideImg = this._currentSlide.querySelector('.hero-slide-img')
204
+
205
+ // Explicitly set starting scale for current slide
206
+ set(currentSlideImg, { scale: 1 })
207
+
208
+ // Build animation sequence
209
+ // Previous slide continues zooming at same rate during fade out
210
+ // Zoom rate: (scale - 1) / interval, so in transition.duration we zoom by that rate * duration
211
+ const zoomRate = (this.opts.transition.scale - 1) / this.opts.interval
212
+ const continueZoomAmount = zoomRate * this.opts.transition.duration
213
+ const continueZoomTarget = this.opts.transition.scale + continueZoomAmount
214
+
215
+ const sequence = [
216
+ // Previous slide continues zooming during fade at same rate
217
+ [
218
+ previousSlideImg,
219
+ { scale: [this.opts.transition.scale, continueZoomTarget] },
220
+ { type: "tween", duration: this.opts.transition.duration, ease: "linear", at: 0 },
221
+ ],
222
+ // Current slide fade-in (starts immediately)
223
+ [
224
+ this._currentSlide,
225
+ { opacity: 1 },
226
+ {
227
+ type: "tween",
228
+ duration: this.opts.transition.duration,
229
+ ease: [0.45, 0, 0.55, 1], // sine.inOut bezier
230
+ at: 0,
231
+ },
232
+ ],
233
+ // Current slide image zoom (starts immediately as it fades in)
234
+ [
235
+ currentSlideImg,
236
+ { scale: [1, this.opts.transition.scale] },
237
+ { type: "tween", duration: this.opts.interval, ease: "linear", at: 0 },
238
+ ],
239
+ ]
240
+
241
+ this._currentAnimation = animate(sequence)
242
+
243
+ this._currentAnimation.finished.then(() => {
244
+ // Cleanup after animation completes
245
+ set(this._previousSlide, { opacity: 0 })
246
+ set(this._currentSlide, { opacity: 1 })
247
+ set(previousSlideImg, { scale: 1 })
248
+ this._nextSlide.style.zIndex = this.opts.zIndex.visible
249
+ this._currentSlide.style.zIndex = this.opts.zIndex.regular
250
+ this._previousSlide.style.zIndex = this.opts.zIndex.regular
251
+ this.next()
210
252
  })
211
- .call(
212
- () => {
213
- this._nextSlide.style.zIndex = this.opts.zIndex.visible
214
- this._currentSlide.style.zIndex = this.opts.zIndex.regular
215
- this._previousSlide.style.zIndex = this.opts.zIndex.regular
216
- this.next()
217
- },
218
- null,
219
- this
220
- )
221
-
253
+ }
222
254
  break
223
255
 
224
256
  case 'parallax':
225
- timeline
226
- .set(this._currentSlide, {
227
- zIndex: this.opts.zIndex.next,
228
- scale: 1.0,
229
- width: '100%',
230
- })
231
- .fromTo(
232
- this._previousSlide,
233
- {
234
- duration: this.opts.interval,
235
- overflow: 'hidden',
236
- },
237
- {
238
- duration: this.opts.interval,
239
- scale: this.opts.transition.scale,
240
- }
241
- )
242
- .to(this._previousSlide, {
243
- duration: this.opts.transition.duration,
244
- width: 0,
245
- ease: 'power3.in',
246
- autoRound: true,
247
- overwrite: 'preexisting',
248
- })
249
- .set(this._nextSlide, {
257
+ {
258
+ // Setup: current slide behind previous slide
259
+ set(this._currentSlide, {
250
260
  zIndex: this.opts.zIndex.next,
251
- })
252
- .set(this._currentSlide, {
253
- zIndex: this.opts.zIndex.visible,
254
261
  width: '100%',
262
+ opacity: 1, // Make sure it's visible underneath
255
263
  })
256
- .set(this._previousSlide, {
257
- zIndex: this.opts.zIndex.regular,
258
- scale: 1.0,
259
- width: '100%',
260
- })
261
- .call(() => {
264
+ set(this._previousSlide, { overflow: 'hidden' })
265
+
266
+ // Get the current slide's image to animate
267
+ const previousSlideImg = this._previousSlide.querySelector('.hero-slide-img')
268
+ const currentSlideImg = this._currentSlide.querySelector('.hero-slide-img')
269
+
270
+ // Explicitly set starting scale for current slide
271
+ set(currentSlideImg, { scale: 1 })
272
+
273
+ // Build animation sequence
274
+ // Previous slide continues zooming at same rate during collapse
275
+ // Zoom rate: (scale - 1) / interval, so in transition.duration we zoom by that rate * duration
276
+ const zoomRate = (this.opts.transition.scale - 1) / this.opts.interval
277
+ const continueZoomAmount = zoomRate * this.opts.transition.duration
278
+ const continueZoomTarget = this.opts.transition.scale + continueZoomAmount
279
+
280
+ const sequence = [
281
+ // Previous slide continues zooming during collapse at same rate
282
+ [
283
+ previousSlideImg,
284
+ { scale: [this.opts.transition.scale, continueZoomTarget] },
285
+ { type: "tween", duration: this.opts.transition.duration, ease: "linear", at: 0 },
286
+ ],
287
+ // Previous slide width collapse (starts immediately)
288
+ [
289
+ this._previousSlide,
290
+ { width: 0 },
291
+ {
292
+ type: "tween",
293
+ duration: this.opts.transition.duration,
294
+ ease: [0.895, 0.03, 0.685, 0.22], // power3.in bezier
295
+ at: 0,
296
+ },
297
+ ],
298
+ // Current slide image zoom (starts immediately as it's revealed)
299
+ [
300
+ currentSlideImg,
301
+ { scale: [1, this.opts.transition.scale] },
302
+ { type: "tween", duration: this.opts.interval, ease: "linear", at: 0 },
303
+ ],
304
+ ]
305
+
306
+ this._currentAnimation = animate(sequence)
307
+
308
+ this._currentAnimation.finished.then(() => {
309
+ // Cleanup and shuffle z-indexes
310
+ set(this._nextSlide, { zIndex: this.opts.zIndex.next, opacity: 1 })
311
+ set(this._currentSlide, {
312
+ zIndex: this.opts.zIndex.visible,
313
+ width: '100%',
314
+ opacity: 1,
315
+ })
316
+ set(this._previousSlide, {
317
+ zIndex: this.opts.zIndex.regular,
318
+ width: '100%',
319
+ opacity: 0, // Hide previous slide
320
+ })
321
+ // Reset previous slide image scale for next time
322
+ set(previousSlideImg, { scale: 1.0 })
262
323
  this.next()
263
324
  })
264
-
325
+ }
265
326
  break
266
327
 
267
328
  default:
@@ -295,10 +356,65 @@ export default class HeroSlider {
295
356
  }
296
357
 
297
358
  _resizeSlides() {
298
- gsap.to(this.images, {
299
- duration: 0.15,
300
- width: document.body.clientWidth,
301
- overwrite: 'all',
359
+ // Stop any running resize animations
360
+ if (this.resizeAnimation) {
361
+ this.resizeAnimation.stop()
362
+ }
363
+
364
+ this.resizeAnimation = animate(
365
+ this.images,
366
+ { width: document.body.clientWidth },
367
+ { duration: 0.15 }
368
+ )
369
+ }
370
+
371
+ /**
372
+ * Add a visibility change handler to restart animations when tab becomes visible
373
+ */
374
+ _addVisibilityHandler() {
375
+ document.addEventListener('visibilitychange', () => {
376
+ if (document.hidden) {
377
+ // Cancel running animation when tab becomes hidden
378
+ if (this._currentAnimation) {
379
+ this._currentAnimation.cancel()
380
+ this._currentAnimation = null
381
+ }
382
+ } else {
383
+ // Reset and restart when tab becomes visible
384
+ this._resetAndRestart()
385
+ }
386
+ })
387
+ }
388
+
389
+ /**
390
+ * Reset slide states and restart the animation cycle
391
+ */
392
+ _resetAndRestart() {
393
+ // Reset all slides to initial state
394
+ Array.from(this.slides).forEach((s, index) => {
395
+ const nextSlideIdx = (this._currentSlideIdx + 1) % this.slides.length
396
+ set(s, {
397
+ width: '100%',
398
+ opacity: index === this._currentSlideIdx ? 1 : 0,
399
+ zIndex: index === this._currentSlideIdx ? this.opts.zIndex.visible :
400
+ index === nextSlideIdx ? this.opts.zIndex.next :
401
+ this.opts.zIndex.regular
402
+ })
403
+ const img = s.querySelector('.hero-slide-img')
404
+ if (img) {
405
+ set(img, { scale: 1 })
406
+ }
302
407
  })
408
+
409
+ // Restart the zoom animation on current slide, then continue cycle
410
+ const currentSlideImg = this.slides[this._currentSlideIdx].querySelector('.hero-slide-img')
411
+ if (currentSlideImg && this.slides.length > 1) {
412
+ this._currentAnimation = animate(
413
+ currentSlideImg,
414
+ { scale: [1, this.opts.transition.scale] },
415
+ { type: "tween", duration: this.opts.interval, ease: "linear" }
416
+ )
417
+ this._currentAnimation.finished.then(() => this.next())
418
+ }
303
419
  }
304
420
  }
@@ -9,14 +9,13 @@
9
9
  * const heroVideo = new HeroVideo(app, opts)
10
10
  */
11
11
 
12
- import { gsap, CSSPlugin } from 'gsap/all'
12
+ import { animate } from 'motion'
13
13
  import _defaultsDeep from 'lodash.defaultsdeep'
14
14
  import * as Events from '../../events'
15
15
  import prefersReducedMotion from '../../utils/prefersReducedMotion'
16
16
  import imageIsLoaded from '../../utils/imageIsLoaded'
17
17
  import Dom from '../Dom'
18
-
19
- gsap.registerPlugin(CSSPlugin)
18
+ import { set, animateAutoAlpha } from '../../utils/motion-helpers'
20
19
 
21
20
  /**
22
21
  * @typedef {Object} HeroVideoElementGenerators
@@ -41,24 +40,15 @@ gsap.registerPlugin(CSSPlugin)
41
40
  const DEFAULT_OPTIONS = {
42
41
  el: '[data-hero-video]',
43
42
  onFadeIn: (hero) => {
44
- gsap.to(hero.videoDiv, {
45
- duration: 1,
46
- autoAlpha: 1,
47
- })
43
+ animateAutoAlpha(hero.videoDiv, 1, { duration: 1 })
48
44
  },
49
45
 
50
46
  onFadeInCover: (hero) => {
51
- gsap.to(hero.cover, {
52
- duration: 0.35,
53
- autoAlpha: 1,
54
- })
47
+ animateAutoAlpha(hero.cover, 1, { duration: 0.35 })
55
48
  },
56
49
 
57
50
  onFadeOutCover: (hero) => {
58
- gsap.set(hero.cover, {
59
- duration: 0.35,
60
- autoAlpha: 0,
61
- })
51
+ animateAutoAlpha(hero.cover, 0, { duration: 0.35 })
62
52
  },
63
53
 
64
54
  onPlayReady: () => {},
@@ -114,7 +104,7 @@ export default class HeroVideo {
114
104
  initialize() {
115
105
  this._addResizeHandler()
116
106
  // style the container
117
- gsap.set(this.el, {
107
+ set(this.el, {
118
108
  position: 'absolute',
119
109
  top: 0,
120
110
  left: 0,
@@ -125,7 +115,7 @@ export default class HeroVideo {
125
115
 
126
116
  this.cover = Dom.find(this.el, '[data-cover]')
127
117
  if (this.cover) {
128
- gsap.set(this.cover, { autoAlpha: 0 })
118
+ animateAutoAlpha(this.cover, 0, { duration: 0 })
129
119
  }
130
120
 
131
121
  const pauseParent = document.querySelector(this.opts.pauseParent)
@@ -146,7 +136,7 @@ export default class HeroVideo {
146
136
  this.addEvents()
147
137
  this.setSrc()
148
138
 
149
- gsap.set(this.videoDiv, {
139
+ set(this.videoDiv, {
150
140
  position: 'absolute',
151
141
  top: 0,
152
142
  left: 0,
@@ -163,12 +153,13 @@ export default class HeroVideo {
163
153
  }
164
154
  this.video.muted = true
165
155
 
166
- gsap.set(this.video, {
167
- width: document.body.clientWidth,
156
+ set(this.video, {
157
+ width: '100%',
168
158
  height: '100%',
159
+ objectFit: 'cover',
160
+ position: 'absolute',
169
161
  top: 0,
170
162
  left: 0,
171
- position: 'absolute',
172
163
  })
173
164
 
174
165
  if (this.cover) {
@@ -222,7 +213,7 @@ export default class HeroVideo {
222
213
  this.fadeIn()
223
214
  this.booting = false
224
215
  } else {
225
- gsap.set(this.videoDiv, { opacity: 1 })
216
+ set(this.videoDiv, { opacity: 1 })
226
217
  }
227
218
  }
228
219
  })
@@ -309,10 +300,7 @@ export default class HeroVideo {
309
300
  }
310
301
 
311
302
  _resize() {
312
- gsap.to(this.video, {
313
- duration: 0.15,
314
- width: document.body.clientWidth,
315
- overwrite: 'all',
316
- })
303
+ // Video uses object-fit: cover, so no manual sizing needed
304
+ // This method is kept for compatibility but does nothing
317
305
  }
318
306
  }