@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
@@ -1,6 +1,7 @@
1
- import { gsap } from 'gsap/all'
1
+ import { animate, stagger } from 'motion'
2
2
  import _defaultsDeep from 'lodash.defaultsdeep'
3
3
  import Dom from '../Dom'
4
+ import { set } from '../../utils/motion-helpers'
4
5
 
5
6
  /**
6
7
  * <ul data-dropdown>
@@ -50,10 +51,15 @@ export default class Dropdown {
50
51
  this.elements = {}
51
52
  this.open = false
52
53
  this.element = opts.el
53
- this.timeline = gsap.timeline({ paused: true, reversed: true })
54
54
 
55
- this.elements.trigger = Dom.find(this.element, this.opts.selectors.trigger)
56
- if (this.elements.trigger.hasAttribute('data-dropdown-target')) {
55
+ // Check if the element itself is the trigger, or find it inside
56
+ if (this.element.matches && this.element.matches(this.opts.selectors.trigger)) {
57
+ this.elements.trigger = this.element
58
+ } else {
59
+ this.elements.trigger = Dom.find(this.element, this.opts.selectors.trigger)
60
+ }
61
+
62
+ if (this.elements.trigger && this.elements.trigger.hasAttribute('data-dropdown-target')) {
57
63
  const dropdownTarget = this.elements.trigger.getAttribute(
58
64
  'data-dropdown-target'
59
65
  )
@@ -75,78 +81,62 @@ export default class Dropdown {
75
81
  }
76
82
 
77
83
  initialize() {
78
- this.timeline
79
- .set(this.elements.menu, { display: 'none', clearProps: 'height' })
80
- .set(this.elements.menu, { display: 'flex', opacity: 0 })
81
- .from(
82
- this.elements.menu,
83
- {
84
- className: `${this.elements.menu.className} zero-height`,
85
- duration: 0.05,
86
- },
87
- 'open'
88
- )
89
- .to(
90
- this.elements.menu,
91
- {
92
- height: 'auto',
93
- duration: 0.05,
94
- },
95
- 'open'
96
- )
97
- .call(() => {
98
- // Get current bounds and viewport dimensions
99
- const menuRect = this.elements.menu.getBoundingClientRect()
100
- const viewportHeight = window.innerHeight
101
- const viewportWidth = window.innerWidth
102
- const menuHeight = menuRect.height
103
- const menuTop = menuRect.top
104
-
105
- // Update CSS variable for height (if used in your styles)
106
- Dom.setCSSVar(
107
- 'dropdown-menu-height',
108
- `${menuHeight}px`,
109
- this.elements.menu
110
- )
111
-
112
- // Vertical placement: if the menu overflows the bottom, set placement to "top"
113
- if (menuHeight + menuTop > viewportHeight) {
114
- this.elements.menu.setAttribute('data-dropdown-placement', 'top')
115
- } else {
116
- this.elements.menu.setAttribute('data-dropdown-placement', 'bottom')
117
- }
118
-
119
- // Horizontal check: adjust left offset if the menu is offscreen
120
- const computedStyle = window.getComputedStyle(this.elements.menu)
121
- let currentLeft = parseFloat(computedStyle.left) || 0
84
+ if (!this.elements.menu) {
85
+ console.error('Dropdown menu element not found')
86
+ return
87
+ }
122
88
 
123
- if (menuRect.left < 0) {
124
- // Shift right by the amount it’s off the left edge
125
- this.elements.menu.style.left = `${currentLeft - menuRect.left}px`
126
- } else if (menuRect.right > viewportWidth) {
127
- // Shift left by the amount it’s off the right edge
128
- this.elements.menu.style.left = `${currentLeft - (menuRect.right - viewportWidth)}px`
129
- }
130
- })
131
- .to(this.elements.menu, {
132
- opacity: 1,
133
- duration: this.opts.menuOpenDuration,
134
- })
89
+ // Initial setup - menu hidden with height cleared
90
+ this.elements.menu.style.removeProperty('height')
91
+ set(this.elements.menu, { display: 'none', opacity: 0 })
135
92
 
136
- if (this.elements.menuItems.length) {
137
- this.timeline.from(
138
- this.elements.menuItems,
139
- this.opts.tweens.items,
140
- `open+=${this.opts.menuOpenDuration}`
141
- )
93
+ // Store initial menu items opacity
94
+ if (this.elements.menuItems && this.elements.menuItems.length) {
95
+ set(this.elements.menuItems, { opacity: 0 })
142
96
  }
143
97
 
144
98
  if (!this.elements.trigger) {
99
+ console.error('Dropdown trigger element not found')
145
100
  return
146
101
  }
147
102
  this.elements.trigger.addEventListener('click', this.onClick.bind(this))
148
103
  }
149
104
 
105
+ positionMenu() {
106
+ // Get current bounds and viewport dimensions
107
+ const menuRect = this.elements.menu.getBoundingClientRect()
108
+ const viewportHeight = window.innerHeight
109
+ const viewportWidth = window.innerWidth
110
+ const menuHeight = menuRect.height
111
+ const menuTop = menuRect.top
112
+
113
+ // Update CSS variable for height (if used in your styles)
114
+ Dom.setCSSVar(
115
+ 'dropdown-menu-height',
116
+ `${menuHeight}px`,
117
+ this.elements.menu
118
+ )
119
+
120
+ // Vertical placement: if the menu overflows the bottom, set placement to "top"
121
+ if (menuHeight + menuTop > viewportHeight) {
122
+ this.elements.menu.setAttribute('data-dropdown-placement', 'top')
123
+ } else {
124
+ this.elements.menu.setAttribute('data-dropdown-placement', 'bottom')
125
+ }
126
+
127
+ // Horizontal check: adjust left offset if the menu is offscreen
128
+ const computedStyle = window.getComputedStyle(this.elements.menu)
129
+ let currentLeft = parseFloat(computedStyle.left) || 0
130
+
131
+ if (menuRect.left < 0) {
132
+ // Shift right by the amount it's off the left edge
133
+ this.elements.menu.style.left = `${currentLeft - menuRect.left}px`
134
+ } else if (menuRect.right > viewportWidth) {
135
+ // Shift left by the amount it's off the right edge
136
+ this.elements.menu.style.left = `${currentLeft - (menuRect.right - viewportWidth)}px`
137
+ }
138
+ }
139
+
150
140
  async onClick(event) {
151
141
  event.preventDefault()
152
142
  event.stopPropagation()
@@ -179,10 +169,30 @@ export default class Dropdown {
179
169
  // Add document click listener when menu is open.
180
170
  document.addEventListener('click', this.handleDocumentClick)
181
171
 
182
- if (this.timeline.reversed()) {
183
- await this.timeline.play()
184
- } else {
185
- await this.timeline.reverse()
172
+ // Show menu (display: flex, still invisible)
173
+ set(this.elements.menu, { display: 'flex', opacity: 0 })
174
+
175
+ // Add zero-height class for animation
176
+ this.elements.menu.classList.add('zero-height')
177
+
178
+ // Brief delay to let browser calculate dimensions, then remove zero-height
179
+ await new Promise(resolve => setTimeout(resolve, 50))
180
+ this.elements.menu.classList.remove('zero-height')
181
+
182
+ // Position menu based on viewport
183
+ this.positionMenu()
184
+
185
+ // Fade in menu
186
+ await animate(this.elements.menu, { opacity: 1 }, {
187
+ duration: this.opts.menuOpenDuration
188
+ }).finished
189
+
190
+ // Animate menu items if present
191
+ if (this.elements.menuItems.length) {
192
+ await animate(this.elements.menuItems, { opacity: 1 }, {
193
+ duration: this.opts.tweens.items.duration,
194
+ delay: stagger(this.opts.tweens.items.stagger)
195
+ }).finished
186
196
  }
187
197
  }
188
198
 
@@ -194,11 +204,27 @@ export default class Dropdown {
194
204
  // Remove the document click listener when menu closes.
195
205
  document.removeEventListener('click', this.handleDocumentClick)
196
206
 
197
- if (this.timeline.reversed()) {
198
- await this.timeline.play()
199
- } else {
200
- await this.timeline.reverse()
207
+ // Animate menu items out first (reverse order, faster)
208
+ if (this.elements.menuItems.length) {
209
+ await animate(this.elements.menuItems, { opacity: 0 }, {
210
+ duration: this.opts.tweens.items.duration * 0.5,
211
+ }).finished
201
212
  }
213
+
214
+ // Fade out menu
215
+ await animate(this.elements.menu, { opacity: 0 }, {
216
+ duration: this.opts.menuOpenDuration
217
+ }).finished
218
+
219
+ // Add zero-height class back for collapse
220
+ this.elements.menu.classList.add('zero-height')
221
+
222
+ // Brief delay for height animation
223
+ await new Promise(resolve => setTimeout(resolve, 50))
224
+
225
+ // Finally hide completely
226
+ set(this.elements.menu, { display: 'none' })
227
+ this.elements.menu.classList.remove('zero-height')
202
228
  }
203
229
 
204
230
  // Handler that checks if a click was outside the dropdown element.
@@ -211,7 +237,7 @@ export default class Dropdown {
211
237
  }
212
238
 
213
239
  checkForInitialOpen() {
214
- if (this.elements.trigger.hasAttribute('data-dropdown-active')) {
240
+ if (this.elements.trigger && this.elements.trigger.hasAttribute('data-dropdown-active')) {
215
241
  this.openMenu()
216
242
  }
217
243
  }
@@ -1,4 +1,4 @@
1
- import { gsap } from 'gsap/all'
1
+ import { clearProps } from '../../utils/motion-helpers'
2
2
  import Dom from '../Dom'
3
3
  import _defaultsDeep from 'lodash.defaultsdeep'
4
4
  import * as Events from '../../events'
@@ -13,9 +13,7 @@ export default class EqualHeightElements {
13
13
  this.selector = selector
14
14
  this.initialize()
15
15
  window.addEventListener(Events.APPLICATION_RESIZE, () => {
16
- gsap.set('[data-eq-height-elements-adjusted]', {
17
- clearProps: 'minHeight',
18
- })
16
+ clearProps('[data-eq-height-elements-adjusted]', 'minHeight')
19
17
  this.initialize()
20
18
  })
21
19
  }
@@ -61,9 +59,9 @@ export default class EqualHeightElements {
61
59
 
62
60
  if (actionables.length) {
63
61
  actionables.forEach((a) => {
64
- gsap.set(a.elements, {
65
- minHeight: a.height,
66
- attr: { 'data-eq-height-elements-adjusted': true },
62
+ a.elements.forEach(el => {
63
+ el.style.minHeight = `${a.height}px`
64
+ el.setAttribute('data-eq-height-elements-adjusted', 'true')
67
65
  })
68
66
  })
69
67
  }
@@ -1,4 +1,3 @@
1
- import { gsap } from 'gsap/all'
2
1
  import Dom from '../Dom'
3
2
  import * as Events from '../../events'
4
3
  import imagesAreLoaded from '../../utils/imagesAreLoaded'
@@ -30,6 +29,10 @@ export default class EqualHeightImages {
30
29
  let height = 0
31
30
  const imgs = Dom.all(canvas, 'img')
32
31
 
32
+ if (imgs.length === 0) {
33
+ return
34
+ }
35
+
33
36
  imagesAreLoaded(imgs, false).then(() => {
34
37
  imgs.forEach((el) => {
35
38
  const rect = el.getBoundingClientRect()
@@ -63,7 +66,9 @@ export default class EqualHeightImages {
63
66
 
64
67
  if (actionables.length) {
65
68
  actionables.forEach((a) => {
66
- gsap.set(a.elements, { minHeight: a.height })
69
+ a.elements.forEach((el) => {
70
+ el.style.minHeight = `${a.height}px`
71
+ })
67
72
  })
68
73
  }
69
74
  })
@@ -23,10 +23,11 @@
23
23
  *
24
24
  */
25
25
 
26
- import { gsap } from 'gsap/all'
26
+ import { animate, stagger } from 'motion'
27
27
  import _defaultsDeep from 'lodash.defaultsdeep'
28
28
  import * as Events from '../../events'
29
29
  import Dom from '../Dom'
30
+ import { set } from '../../utils/motion-helpers'
30
31
 
31
32
  /**
32
33
  * @typedef {Object} FixedHeaderEvents
@@ -77,41 +78,42 @@ import Dom from '../Dom'
77
78
  /** @type {FixedHeaderEvents} */
78
79
  const DEFAULT_EVENTS = {
79
80
  onPin: (h) => {
80
- gsap.to(h.el, {
81
+ animate(h.el, {
82
+ yPercent: '0'
83
+ }, {
81
84
  duration: 0.35,
82
- yPercent: '0',
83
- ease: 'sine.out',
84
- autoRound: true,
85
+ ease: 'easeOut'
85
86
  })
86
87
  },
87
88
 
88
89
  onUnpin: (h) => {
89
90
  h._hiding = true
90
- gsap.to(h.el, {
91
+ animate(h.el, {
92
+ yPercent: '-100'
93
+ }, {
91
94
  duration: 0.25,
92
- yPercent: '-100',
93
- ease: 'sine.in',
94
- autoRound: true,
95
- onComplete: () => {
96
- h._hiding = false
97
- },
95
+ ease: 'easeIn'
96
+ }).finished.then(() => {
97
+ h._hiding = false
98
98
  })
99
99
  },
100
100
 
101
101
  onAltBg: (h) => {
102
102
  if (h.opts.altBgColor) {
103
- gsap.to(h.el, {
104
- duration: 0.2,
105
- backgroundColor: h.opts.altBgColor,
103
+ animate(h.el, {
104
+ backgroundColor: h.opts.altBgColor
105
+ }, {
106
+ duration: 0.2
106
107
  })
107
108
  }
108
109
  },
109
110
 
110
111
  onNotAltBg: (h) => {
111
112
  if (h.opts.regBgColor) {
112
- gsap.to(h.el, {
113
- duration: 0.4,
114
- backgroundColor: h.opts.regBgColor,
113
+ animate(h.el, {
114
+ backgroundColor: h.opts.regBgColor
115
+ }, {
116
+ duration: 0.4
115
117
  })
116
118
  }
117
119
  },
@@ -154,21 +156,28 @@ const DEFAULT_OPTIONS = {
154
156
  canvas: window,
155
157
  intersects: null,
156
158
  beforeEnter: (h) => {
157
- const timeline = gsap.timeline()
158
- timeline.set(h.el, { yPercent: -100 }).set(h.lis, { opacity: 0 })
159
+ set(h.el, { yPercent: -100 })
160
+ set(h.lis, { opacity: 0 })
159
161
  },
160
162
 
161
163
  enter: (h) => {
162
- const timeline = gsap.timeline()
163
- timeline
164
- .to(h.el, {
165
- duration: 1,
166
- yPercent: 0,
167
- delay: h.opts.enterDelay,
168
- ease: 'power3.out',
169
- autoRound: true,
170
- })
171
- .staggerTo(h.lis, 0.8, { opacity: 1, ease: 'sine.in' }, 0.1, '-=1')
164
+ // Header slides down
165
+ animate(h.el, {
166
+ yPercent: 0
167
+ }, {
168
+ duration: 1,
169
+ delay: h.opts.enterDelay,
170
+ ease: 'easeOut'
171
+ })
172
+
173
+ // Menu items fade in with stagger (starts at same time as header: '-=1' means 1s overlap)
174
+ animate(h.lis, {
175
+ opacity: 1
176
+ }, {
177
+ duration: 0.8,
178
+ delay: stagger(0.1, { startDelay: h.opts.enterDelay }),
179
+ ease: 'easeIn'
180
+ })
172
181
  },
173
182
 
174
183
  enterDelay: 0,
@@ -227,6 +236,7 @@ export default class FixedHeader {
227
236
  this.mobileMenuOpen = false
228
237
  this.timer = null
229
238
  this.resetResizeTimer = null
239
+ this.scrollSettleTimeout = null
230
240
 
231
241
  if (this.opts.intersects) {
232
242
  this.intersectingElements = Dom.all('[data-intersect]')
@@ -297,6 +307,26 @@ export default class FixedHeader {
297
307
  capture: false,
298
308
  passive: true,
299
309
  })
310
+
311
+ // Add debounced scroll listener for accurate top/bottom detection after scroll settles
312
+ // RAF-throttled events can lag behind actual scroll position during fast scrolls
313
+ window.addEventListener('scroll', () => {
314
+ clearTimeout(this.scrollSettleTimeout)
315
+ this.scrollSettleTimeout = setTimeout(() => {
316
+ // Get real-time scroll position after scroll has settled
317
+ const actualScrollY = this.opts.canvas === window || this.opts.canvas === document.body
318
+ ? window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
319
+ : this.opts.canvas.scrollTop
320
+
321
+ // Update current scroll and force accurate boundary checks
322
+ this.currentScrollY = actualScrollY
323
+ this.checkTop(true)
324
+ this.checkBot(true)
325
+ }, 100)
326
+ }, {
327
+ capture: false,
328
+ passive: true,
329
+ })
300
330
  })
301
331
 
302
332
  this.app.registerCallback(
@@ -1,4 +1,4 @@
1
- import { gsap } from 'gsap/all'
1
+ import { set } from '../../utils/motion-helpers'
2
2
  import _defaultsDeep from 'lodash.defaultsdeep'
3
3
 
4
4
  const DEFAULT_OPTIONS = {
@@ -14,14 +14,14 @@ export default class FooterReveal {
14
14
  const main = document.querySelector('main')
15
15
  const footer = document.querySelector('[data-footer-reveal]')
16
16
  // fix footer
17
- gsap.set(footer, {
17
+ set(footer, {
18
18
  'z-index': -100,
19
19
  position: 'fixed',
20
20
  bottom: 0,
21
21
  })
22
22
  const footerHeight = footer.offsetHeight
23
23
  // add height as margin
24
- gsap.set(main, { marginBottom: footerHeight })
24
+ set(main, { marginBottom: footerHeight })
25
25
  if (this.opts.shadow) {
26
26
  const shadowStyle = `0 50px 50px -20px ${this.opts.shadowColor}`
27
27
  main.style.mozBoxShadow = shadowStyle