@brandocms/jupiter 3.55.0 → 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 (76) hide show
  1. package/README.md +509 -54
  2. package/package.json +30 -18
  3. package/src/index.js +15 -10
  4. package/src/modules/Application/index.js +236 -158
  5. package/src/modules/Breakpoints/index.js +116 -36
  6. package/src/modules/Cookies/index.js +95 -64
  7. package/src/modules/CoverOverlay/index.js +21 -14
  8. package/src/modules/Dataloader/index.js +71 -24
  9. package/src/modules/Dataloader/url-sync.js +238 -0
  10. package/src/modules/Dom/index.js +24 -0
  11. package/src/modules/DoubleHeader/index.js +571 -0
  12. package/src/modules/Dropdown/index.js +108 -73
  13. package/src/modules/EqualHeightElements/index.js +8 -8
  14. package/src/modules/EqualHeightImages/index.js +15 -7
  15. package/src/modules/FixedHeader/index.js +116 -30
  16. package/src/modules/FooterReveal/index.js +5 -5
  17. package/src/modules/HeroSlider/index.js +231 -106
  18. package/src/modules/HeroVideo/index.js +72 -44
  19. package/src/modules/Lazyload/index.js +128 -80
  20. package/src/modules/Lightbox/index.js +101 -80
  21. package/src/modules/Links/index.js +77 -51
  22. package/src/modules/Looper/index.js +1737 -0
  23. package/src/modules/Marquee/index.js +106 -37
  24. package/src/modules/MobileMenu/index.js +105 -130
  25. package/src/modules/Moonwalk/index.js +479 -153
  26. package/src/modules/Parallax/index.js +280 -57
  27. package/src/modules/Popover/index.js +187 -17
  28. package/src/modules/Popup/index.js +172 -53
  29. package/src/modules/ScrollSpy/index.js +21 -0
  30. package/src/modules/StackedBoxes/index.js +8 -6
  31. package/src/modules/StickyHeader/index.js +394 -164
  32. package/src/modules/Toggler/index.js +207 -11
  33. package/src/modules/Typography/index.js +33 -20
  34. package/src/utils/motion-helpers.js +330 -0
  35. package/types/README.md +159 -0
  36. package/types/events/index.d.ts +20 -0
  37. package/types/index.d.ts +6 -0
  38. package/types/modules/Application/index.d.ts +168 -0
  39. package/types/modules/Breakpoints/index.d.ts +40 -0
  40. package/types/modules/Cookies/index.d.ts +81 -0
  41. package/types/modules/CoverOverlay/index.d.ts +6 -0
  42. package/types/modules/Dataloader/index.d.ts +38 -0
  43. package/types/modules/Dataloader/url-sync.d.ts +36 -0
  44. package/types/modules/Dom/index.d.ts +47 -0
  45. package/types/modules/DoubleHeader/index.d.ts +63 -0
  46. package/types/modules/Dropdown/index.d.ts +15 -0
  47. package/types/modules/EqualHeightElements/index.d.ts +8 -0
  48. package/types/modules/EqualHeightImages/index.d.ts +11 -0
  49. package/types/modules/FeatureTests/index.d.ts +27 -0
  50. package/types/modules/FixedHeader/index.d.ts +219 -0
  51. package/types/modules/Fontloader/index.d.ts +5 -0
  52. package/types/modules/FooterReveal/index.d.ts +5 -0
  53. package/types/modules/HeroSlider/index.d.ts +28 -0
  54. package/types/modules/HeroVideo/index.d.ts +83 -0
  55. package/types/modules/Lazyload/index.d.ts +80 -0
  56. package/types/modules/Lightbox/index.d.ts +123 -0
  57. package/types/modules/Links/index.d.ts +55 -0
  58. package/types/modules/Looper/index.d.ts +127 -0
  59. package/types/modules/Marquee/index.d.ts +23 -0
  60. package/types/modules/MobileMenu/index.d.ts +63 -0
  61. package/types/modules/Moonwalk/index.d.ts +322 -0
  62. package/types/modules/Parallax/index.d.ts +71 -0
  63. package/types/modules/Popover/index.d.ts +29 -0
  64. package/types/modules/Popup/index.d.ts +76 -0
  65. package/types/modules/ScrollSpy/index.d.ts +29 -0
  66. package/types/modules/StackedBoxes/index.d.ts +9 -0
  67. package/types/modules/StickyHeader/index.d.ts +220 -0
  68. package/types/modules/Toggler/index.d.ts +48 -0
  69. package/types/modules/Typography/index.d.ts +77 -0
  70. package/types/utils/dispatchElementEvent.d.ts +1 -0
  71. package/types/utils/imageIsLoaded.d.ts +1 -0
  72. package/types/utils/imagesAreLoaded.d.ts +1 -0
  73. package/types/utils/loadScript.d.ts +2 -0
  74. package/types/utils/prefersReducedMotion.d.ts +4 -0
  75. package/types/utils/rafCallback.d.ts +2 -0
  76. package/types/utils/zoom.d.ts +4 -0
@@ -1,6 +1,7 @@
1
- import { gsap } from 'gsap'
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>
@@ -27,6 +28,8 @@ const DEFAULT_OPTIONS = {
27
28
  menu: '[data-dropdown-menu]',
28
29
  menuItems: '[data-dropdown-menu] > li',
29
30
  },
31
+ overlapTweens: true,
32
+ menuOpenDuration: 0.1,
30
33
  tweens: {
31
34
  items: {
32
35
  duration: 0.2,
@@ -48,10 +51,15 @@ export default class Dropdown {
48
51
  this.elements = {}
49
52
  this.open = false
50
53
  this.element = opts.el
51
- this.timeline = gsap.timeline({ paused: true, reversed: true })
52
54
 
53
- this.elements.trigger = Dom.find(this.element, this.opts.selectors.trigger)
54
- 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')) {
55
63
  const dropdownTarget = this.elements.trigger.getAttribute(
56
64
  'data-dropdown-target'
57
65
  )
@@ -73,75 +81,62 @@ export default class Dropdown {
73
81
  }
74
82
 
75
83
  initialize() {
76
- this.timeline
77
- .set(this.elements.menu, { display: 'none', clearProps: 'height' })
78
- .set(this.elements.menu, { display: 'flex', opacity: 0 })
79
- .from(
80
- this.elements.menu,
81
- {
82
- className: `${this.elements.menu.className} zero-height`,
83
- duration: 0.1,
84
- },
85
- 'open'
86
- )
87
- .to(
88
- this.elements.menu,
89
- {
90
- height: 'auto',
91
- duration: 0.1,
92
- },
93
- 'open'
94
- )
95
- .call(() => {
96
- // Get current bounds and viewport dimensions
97
- const menuRect = this.elements.menu.getBoundingClientRect()
98
- const viewportHeight = window.innerHeight
99
- const viewportWidth = window.innerWidth
100
- const menuHeight = menuRect.height
101
- const menuTop = menuRect.top
102
-
103
- // Update CSS variable for height (if used in your styles)
104
- Dom.setCSSVar(
105
- 'dropdown-menu-height',
106
- `${menuHeight}px`,
107
- this.elements.menu
108
- )
109
-
110
- // Vertical placement: if the menu overflows the bottom, set placement to "top"
111
- if (menuHeight + menuTop > viewportHeight) {
112
- this.elements.menu.setAttribute('data-dropdown-placement', 'top')
113
- } else {
114
- this.elements.menu.setAttribute('data-dropdown-placement', 'bottom')
115
- }
84
+ if (!this.elements.menu) {
85
+ console.error('Dropdown menu element not found')
86
+ return
87
+ }
116
88
 
117
- // Horizontal check: adjust left offset if the menu is offscreen
118
- const computedStyle = window.getComputedStyle(this.elements.menu)
119
- let currentLeft = parseFloat(computedStyle.left) || 0
89
+ // Initial setup - menu hidden with height cleared
90
+ this.elements.menu.style.removeProperty('height')
91
+ set(this.elements.menu, { display: 'none', opacity: 0 })
120
92
 
121
- if (menuRect.left < 0) {
122
- // Shift right by the amount it’s off the left edge
123
- this.elements.menu.style.left = `${currentLeft - menuRect.left}px`
124
- } else if (menuRect.right > viewportWidth) {
125
- // Shift left by the amount it’s off the right edge
126
- this.elements.menu.style.left = `${currentLeft - (menuRect.right - viewportWidth)}px`
127
- }
128
- })
129
- .to(this.elements.menu, { opacity: 1 })
130
-
131
- if (this.elements.menuItems.length) {
132
- this.timeline.from(
133
- this.elements.menuItems,
134
- this.opts.tweens.items,
135
- 'open+=.1'
136
- )
93
+ // Store initial menu items opacity
94
+ if (this.elements.menuItems && this.elements.menuItems.length) {
95
+ set(this.elements.menuItems, { opacity: 0 })
137
96
  }
138
97
 
139
98
  if (!this.elements.trigger) {
99
+ console.error('Dropdown trigger element not found')
140
100
  return
141
101
  }
142
102
  this.elements.trigger.addEventListener('click', this.onClick.bind(this))
143
103
  }
144
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
+
145
140
  async onClick(event) {
146
141
  event.preventDefault()
147
142
  event.stopPropagation()
@@ -160,7 +155,11 @@ export default class Dropdown {
160
155
  async openMenu() {
161
156
  if (!this.opts.multipleActive) {
162
157
  if (this.app.currentMenu) {
163
- this.app.currentMenu.closeMenu()
158
+ if (this.opts.overlapTweens) {
159
+ this.app.currentMenu.closeMenu()
160
+ } else {
161
+ await this.app.currentMenu.closeMenu()
162
+ }
164
163
  }
165
164
  this.app.currentMenu = this
166
165
  }
@@ -170,10 +169,30 @@ export default class Dropdown {
170
169
  // Add document click listener when menu is open.
171
170
  document.addEventListener('click', this.handleDocumentClick)
172
171
 
173
- if (this.timeline.reversed()) {
174
- await this.timeline.play()
175
- } else {
176
- 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
177
196
  }
178
197
  }
179
198
 
@@ -185,11 +204,27 @@ export default class Dropdown {
185
204
  // Remove the document click listener when menu closes.
186
205
  document.removeEventListener('click', this.handleDocumentClick)
187
206
 
188
- if (this.timeline.reversed()) {
189
- await this.timeline.play()
190
- } else {
191
- 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
192
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')
193
228
  }
194
229
 
195
230
  // Handler that checks if a click was outside the dropdown element.
@@ -202,7 +237,7 @@ export default class Dropdown {
202
237
  }
203
238
 
204
239
  checkForInitialOpen() {
205
- if (this.elements.trigger.hasAttribute('data-dropdown-active')) {
240
+ if (this.elements.trigger && this.elements.trigger.hasAttribute('data-dropdown-active')) {
206
241
  this.openMenu()
207
242
  }
208
243
  }
@@ -1,4 +1,4 @@
1
- import { gsap } from 'gsap'
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,21 +13,21 @@ 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]', { clearProps: 'minHeight' })
16
+ clearProps('[data-eq-height-elements-adjusted]', 'minHeight')
17
17
  this.initialize()
18
18
  })
19
19
  }
20
20
 
21
21
  initialize() {
22
22
  const canvases = Dom.all(this.container, '[data-eq-height-elements]')
23
- Array.from(canvases).forEach(canvas => {
23
+ Array.from(canvases).forEach((canvas) => {
24
24
  let lastTop = null
25
25
  const actionables = []
26
26
  let elements = []
27
27
  let height = 0
28
28
  const eqElements = Dom.all(canvas, this.selector)
29
29
 
30
- eqElements.forEach(el => {
30
+ eqElements.forEach((el) => {
31
31
  const rect = el.getBoundingClientRect()
32
32
 
33
33
  if (lastTop === null) {
@@ -58,10 +58,10 @@ export default class EqualHeightElements {
58
58
  }
59
59
 
60
60
  if (actionables.length) {
61
- actionables.forEach(a => {
62
- gsap.set(a.elements, {
63
- minHeight: a.height,
64
- attr: { 'data-eq-height-elements-adjusted': true }
61
+ actionables.forEach((a) => {
62
+ a.elements.forEach(el => {
63
+ el.style.minHeight = `${a.height}px`
64
+ el.setAttribute('data-eq-height-elements-adjusted', 'true')
65
65
  })
66
66
  })
67
67
  }
@@ -1,11 +1,10 @@
1
- import { gsap } from 'gsap'
2
1
  import Dom from '../Dom'
3
2
  import * as Events from '../../events'
4
3
  import imagesAreLoaded from '../../utils/imagesAreLoaded'
5
4
  import _defaultsDeep from 'lodash.defaultsdeep'
6
5
 
7
6
  const DEFAULT_OPTIONS = {
8
- listenForResize: true
7
+ listenForResize: true,
9
8
  }
10
9
 
11
10
  export default class EqualHeightImages {
@@ -23,15 +22,19 @@ export default class EqualHeightImages {
23
22
  }
24
23
 
25
24
  run() {
26
- Array.from(this.canvases).forEach(canvas => {
25
+ Array.from(this.canvases).forEach((canvas) => {
27
26
  let lastTop = null
28
27
  const actionables = []
29
28
  let elements = []
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
- imgs.forEach(el => {
37
+ imgs.forEach((el) => {
35
38
  const rect = el.getBoundingClientRect()
36
39
  const size = this.getImgSizeInfo(el)
37
40
 
@@ -62,8 +65,10 @@ export default class EqualHeightImages {
62
65
  }
63
66
 
64
67
  if (actionables.length) {
65
- actionables.forEach(a => {
66
- gsap.set(a.elements, { minHeight: a.height })
68
+ actionables.forEach((a) => {
69
+ a.elements.forEach((el) => {
70
+ el.style.minHeight = `${a.height}px`
71
+ })
67
72
  })
68
73
  }
69
74
  })
@@ -94,7 +99,10 @@ export default class EqualHeightImages {
94
99
  }
95
100
 
96
101
  getImgSizeInfo(img) {
97
- const pos = window.getComputedStyle(img).getPropertyValue('object-position').split(' ')
102
+ const pos = window
103
+ .getComputedStyle(img)
104
+ .getPropertyValue('object-position')
105
+ .split(' ')
98
106
 
99
107
  return this.getRenderedSize(
100
108
  true,
@@ -23,48 +23,97 @@
23
23
  *
24
24
  */
25
25
 
26
- import { gsap } from 'gsap'
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
 
32
+ /**
33
+ * @typedef {Object} FixedHeaderEvents
34
+ * @property {Function} [onPin] - Called when header is pinned
35
+ * @property {Function} [onUnpin] - Called when header is unpinned
36
+ * @property {Function} [onAltBg] - Called when alternate background is applied
37
+ * @property {Function} [onNotAltBg] - Called when regular background is applied
38
+ * @property {Function} [onSmall] - Called when header becomes small
39
+ * @property {Function} [onNotSmall] - Called when header becomes normal size
40
+ * @property {Function} [onTop] - Called when page is at the top
41
+ * @property {Function} [onNotTop] - Called when page is not at the top
42
+ * @property {Function} [onBottom] - Called when page is at the bottom
43
+ * @property {Function} [onNotBottom] - Called when page is not at the bottom
44
+ * @property {Function} [onMobileMenuOpen] - Called when mobile menu opens
45
+ * @property {Function} [onMobileMenuClose] - Called when mobile menu closes
46
+ * @property {Function} [onIntersect] - Called when header intersects with an element
47
+ * @property {Function} [onOutline] - Called when user tabs (outline mode)
48
+ */
49
+
50
+ /**
51
+ * @typedef {Object} FixedHeaderSectionOptions
52
+ * @property {boolean} [unPinOnResize=true] - Whether to unpin header on window resize
53
+ * @property {Window|HTMLElement} [canvas=window] - Scrolling element
54
+ * @property {string|null} [intersects=null] - Selector for elements to check intersection with
55
+ * @property {Function} [beforeEnter] - Called before header enters
56
+ * @property {Function} [enter] - Called when header enters
57
+ * @property {number} [enterDelay=0] - Delay before enter animation
58
+ * @property {number} [tolerance=3] - Scroll tolerance before triggering hide/show
59
+ * @property {number|string|Function} [offset=0] - Offset from top before triggering hide
60
+ * @property {number|string|Function} [offsetSmall=50] - Offset from top before shrinking header
61
+ * @property {number|string|Function} [offsetBg=200] - Offset from top before changing background color
62
+ * @property {string|null} [regBgColor=null] - Regular background color
63
+ * @property {string|null} [altBgColor=null] - Alternate background color
64
+ */
65
+
66
+ /**
67
+ * @typedef {Object} FixedHeaderOptions
68
+ * @property {string|HTMLElement} [el='header[data-nav]'] - Header element or selector
69
+ * @property {string} [on=Events.APPLICATION_REVEALED] - Event to initialize on
70
+ * @property {boolean} [unpinOnForcedScrollStart=true] - Whether to unpin on forced scroll start
71
+ * @property {boolean} [pinOnForcedScrollEnd=true] - Whether to pin on forced scroll end
72
+ * @property {boolean} [ignoreForcedScroll=false] - Whether to ignore forced scroll events
73
+ * @property {boolean} [rafScroll=true] - Whether to use requestAnimationFrame for scrolling
74
+ * @property {FixedHeaderSectionOptions} [default] - Default options for all sections
75
+ * @property {Object.<string, FixedHeaderSectionOptions>} [sections] - Section-specific options
76
+ */
77
+
78
+ /** @type {FixedHeaderEvents} */
31
79
  const DEFAULT_EVENTS = {
32
80
  onPin: (h) => {
33
- gsap.to(h.el, {
81
+ animate(h.el, {
82
+ yPercent: '0'
83
+ }, {
34
84
  duration: 0.35,
35
- yPercent: '0',
36
- ease: 'sine.out',
37
- autoRound: true,
85
+ ease: 'easeOut'
38
86
  })
39
87
  },
40
88
 
41
89
  onUnpin: (h) => {
42
90
  h._hiding = true
43
- gsap.to(h.el, {
91
+ animate(h.el, {
92
+ yPercent: '-100'
93
+ }, {
44
94
  duration: 0.25,
45
- yPercent: '-100',
46
- ease: 'sine.in',
47
- autoRound: true,
48
- onComplete: () => {
49
- h._hiding = false
50
- },
95
+ ease: 'easeIn'
96
+ }).finished.then(() => {
97
+ h._hiding = false
51
98
  })
52
99
  },
53
100
 
54
101
  onAltBg: (h) => {
55
102
  if (h.opts.altBgColor) {
56
- gsap.to(h.el, {
57
- duration: 0.2,
58
- backgroundColor: h.opts.altBgColor,
103
+ animate(h.el, {
104
+ backgroundColor: h.opts.altBgColor
105
+ }, {
106
+ duration: 0.2
59
107
  })
60
108
  }
61
109
  },
62
110
 
63
111
  onNotAltBg: (h) => {
64
112
  if (h.opts.regBgColor) {
65
- gsap.to(h.el, {
66
- duration: 0.4,
67
- backgroundColor: h.opts.regBgColor,
113
+ animate(h.el, {
114
+ backgroundColor: h.opts.regBgColor
115
+ }, {
116
+ duration: 0.4
68
117
  })
69
118
  }
70
119
  },
@@ -93,6 +142,7 @@ const DEFAULT_EVENTS = {
93
142
  },
94
143
  }
95
144
 
145
+ /** @type {FixedHeaderOptions} */
96
146
  const DEFAULT_OPTIONS = {
97
147
  el: 'header[data-nav]',
98
148
  on: Events.APPLICATION_REVEALED,
@@ -106,21 +156,28 @@ const DEFAULT_OPTIONS = {
106
156
  canvas: window,
107
157
  intersects: null,
108
158
  beforeEnter: (h) => {
109
- const timeline = gsap.timeline()
110
- timeline.set(h.el, { yPercent: -100 }).set(h.lis, { opacity: 0 })
159
+ set(h.el, { yPercent: -100 })
160
+ set(h.lis, { opacity: 0 })
111
161
  },
112
162
 
113
163
  enter: (h) => {
114
- const timeline = gsap.timeline()
115
- timeline
116
- .to(h.el, {
117
- duration: 1,
118
- yPercent: 0,
119
- delay: h.opts.enterDelay,
120
- ease: 'power3.out',
121
- autoRound: true,
122
- })
123
- .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
+ })
124
181
  },
125
182
 
126
183
  enterDelay: 0,
@@ -134,7 +191,15 @@ const DEFAULT_OPTIONS = {
134
191
  },
135
192
  }
136
193
 
194
+ /**
195
+ * FixedHeader component for sticky navigation headers with scroll behaviors
196
+ */
137
197
  export default class FixedHeader {
198
+ /**
199
+ * Create a new FixedHeader instance
200
+ * @param {Object} app - Application instance
201
+ * @param {FixedHeaderOptions} [opts={}] - FixedHeader options
202
+ */
138
203
  constructor(app, opts = {}) {
139
204
  this.app = app
140
205
  this.mainOpts = _defaultsDeep(opts, DEFAULT_OPTIONS)
@@ -171,6 +236,7 @@ export default class FixedHeader {
171
236
  this.mobileMenuOpen = false
172
237
  this.timer = null
173
238
  this.resetResizeTimer = null
239
+ this.scrollSettleTimeout = null
174
240
 
175
241
  if (this.opts.intersects) {
176
242
  this.intersectingElements = Dom.all('[data-intersect]')
@@ -241,6 +307,26 @@ export default class FixedHeader {
241
307
  capture: false,
242
308
  passive: true,
243
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
+ })
244
330
  })
245
331
 
246
332
  this.app.registerCallback(
@@ -1,9 +1,9 @@
1
- import { gsap } from 'gsap'
1
+ import { set } from '../../utils/motion-helpers'
2
2
  import _defaultsDeep from 'lodash.defaultsdeep'
3
3
 
4
4
  const DEFAULT_OPTIONS = {
5
5
  shadow: false,
6
- shadowColor: 'rgba(255, 255, 255, 1)'
6
+ shadowColor: 'rgba(255, 255, 255, 1)',
7
7
  }
8
8
 
9
9
  export default class FooterReveal {
@@ -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
- bottom: 0
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