@brandocms/jupiter 5.0.0-beta.3 → 5.0.0-beta.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandocms/jupiter",
3
- "version": "5.0.0-beta.3",
3
+ "version": "5.0.0-beta.5",
4
4
  "description": "Frontend helpers.",
5
5
  "author": "Univers/Twined",
6
6
  "license": "UNLICENSED",
@@ -306,20 +306,30 @@ export default class HeroSlider {
306
306
  this._currentAnimation = animate(sequence)
307
307
 
308
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,
309
+ // Reset ALL slides using instant animations to ensure Motion.js state is clean
310
+ Array.from(this.slides).forEach((slide) => {
311
+ if (slide === this._currentSlide) return
312
+ const img = slide.querySelector('.hero-slide-img')
313
+ if (img) {
314
+ // Use animate with duration 0 to reset Motion.js internal state
315
+ animate(img, { scale: 1 }, { duration: 0 })
316
+ }
317
+ const isNext = slide === this._nextSlide
318
+ // Reset slide using animate with duration 0
319
+ animate(slide, {
320
+ width: '100%',
321
+ opacity: isNext ? 1 : 0,
322
+ }, { duration: 0 })
323
+ slide.style.overflow = ''
324
+ slide.style.zIndex = isNext ? this.opts.zIndex.next : this.opts.zIndex.regular
315
325
  })
316
- set(this._previousSlide, {
317
- zIndex: this.opts.zIndex.regular,
326
+
327
+ animate(this._currentSlide, {
318
328
  width: '100%',
319
- opacity: 0, // Hide previous slide
320
- })
321
- // Reset previous slide image scale for next time
322
- set(previousSlideImg, { scale: 1.0 })
329
+ opacity: 1,
330
+ }, { duration: 0 })
331
+ this._currentSlide.style.zIndex = this.opts.zIndex.visible
332
+
323
333
  this.next()
324
334
  })
325
335
  }
@@ -83,6 +83,65 @@ export default class Lazyload {
83
83
  this.initObserver(this.revealObserver, false)
84
84
  }
85
85
 
86
+ /**
87
+ * Observe new lazyload elements within a container
88
+ * Handles both [data-ll-image] and [data-ll-srcset] elements
89
+ * Useful for dynamically added content (e.g., Looper clones)
90
+ * @param {HTMLElement|HTMLElement[]|NodeList} elements - Container element(s) or lazyload element(s) to observe
91
+ */
92
+ observe(elements) {
93
+ // Handle NodeList, array, or single element
94
+ const els = elements instanceof NodeList ? Array.from(elements) :
95
+ Array.isArray(elements) ? elements : [elements]
96
+
97
+ let imgIdx = this.lazyImages?.length || 0
98
+ let picIdx = this.lazyPictures?.length || 0
99
+
100
+ els.forEach(el => {
101
+ // Handle [data-ll-image] elements
102
+ if (this.imageObserver) {
103
+ const images = el.matches?.('[data-ll-image]')
104
+ ? [el]
105
+ : el.querySelectorAll?.('[data-ll-image]') || []
106
+
107
+ images.forEach(img => {
108
+ // Skip if already observed or loaded
109
+ if (img.hasAttribute('data-ll-idx') || img.hasAttribute('data-ll-loaded')) return
110
+
111
+ img.setAttribute('data-ll-blurred', '')
112
+ img.setAttribute('data-ll-idx', imgIdx)
113
+ img.style.setProperty('--ll-idx', imgIdx)
114
+ this.imageObserver.observe(img)
115
+ imgIdx++
116
+ })
117
+ }
118
+
119
+ // Handle [data-ll-srcset] picture elements
120
+ if (this.loadObserver) {
121
+ const pictures = el.matches?.('[data-ll-srcset]')
122
+ ? [el]
123
+ : el.querySelectorAll?.('[data-ll-srcset]') || []
124
+
125
+ pictures.forEach(picture => {
126
+ // Skip if already loaded
127
+ if (picture.hasAttribute('data-ll-srcset-ready')) return
128
+
129
+ picture.setAttribute('data-ll-srcset-initialized', '')
130
+ picture.querySelectorAll('img:not([data-ll-loaded])').forEach(img => {
131
+ img.removeAttribute('data-ll-idx') // Clear cloned idx
132
+ img.setAttribute('data-ll-blurred', '')
133
+ img.setAttribute('data-ll-idx', picIdx)
134
+ img.style.setProperty('--ll-idx', picIdx)
135
+ })
136
+ // Add to both observers like initObserver does
137
+ this.loadObserver.observe(picture)
138
+ this.revealObserver?.observe(picture)
139
+ picIdx++
140
+ })
141
+ }
142
+ })
143
+ }
144
+
86
145
  initialize() {
87
146
  // initialize ResizeObserver for images with data-sizes="auto"
88
147
  this.initializeResizeObserver()
@@ -128,12 +187,7 @@ export default class Lazyload {
128
187
  )
129
188
 
130
189
  this.lazyImages = this.target.querySelectorAll('[data-ll-image]')
131
- this.lazyImages.forEach((img, idx) => {
132
- img.setAttribute('data-ll-blurred', '')
133
- img.setAttribute('data-ll-idx', idx)
134
- img.style.setProperty('--ll-idx', idx)
135
- this.imageObserver.observe(img)
136
- })
190
+ this.observe(this.lazyImages)
137
191
  }
138
192
 
139
193
  initObserver(observer, setAttrs = true) {
@@ -103,6 +103,10 @@ function horizontalLoop(app, items, config) {
103
103
  let positionUnsubscribe = null // Track position listener for cleanup
104
104
  let renderUnsubscribe = null // Track frame.render loop for cleanup
105
105
 
106
+ // Scroll direction tracking for wrap logic
107
+ let scrollDirection = 0 // -1 = backward, 0 = neutral, 1 = forward
108
+ let lastPositionForDirection = 0
109
+
106
110
  // Display elements for index/count
107
111
  let indexElements = []
108
112
  let countElements = []
@@ -163,24 +167,45 @@ function horizontalLoop(app, items, config) {
163
167
  // This prevents items from visibly moving to the back before they're off-screen
164
168
  const minRequiredWidth = containerWidth * 2.5 + maxItemWidth
165
169
 
166
- // Only replicate if needed
167
- if (totalWidth >= minRequiredWidth) {
168
- return
169
- }
170
-
171
170
  // Store original count to prevent exponential growth
172
171
  const originalItemCount = items.length
173
172
  const maxReplications = 10
174
173
  let count = 0
175
174
  let previousTotalWidth = totalWidth
176
175
 
177
- while (totalWidth < minRequiredWidth && count < maxReplications) {
176
+ // Always create at least one set of clones - the wrapping logic depends on clones existing
177
+ // Then continue until we have enough width for seamless looping
178
+ while ((count === 0 || totalWidth < minRequiredWidth) && count < maxReplications) {
178
179
  // Clone ONLY original items
179
180
  for (let i = 0; i < originalItemCount; i++) {
180
181
  const clone = items[i].cloneNode(true)
181
182
  clone.setAttribute('data-looper-clone', 'true')
182
183
  container.appendChild(clone)
183
184
  items.push(clone)
185
+
186
+ // Register cloned lazyload elements with Lazyload module if available
187
+ if (app?.lazyload?.observe) {
188
+ // Clear data-ll-idx from unloaded [data-ll-image] images
189
+ clone.querySelectorAll('[data-ll-image]:not([data-ll-loaded])').forEach(img => {
190
+ img.removeAttribute('data-ll-idx')
191
+ })
192
+
193
+ // Clear attributes from unloaded [data-ll-srcset] pictures so they can be re-observed
194
+ clone.querySelectorAll('[data-ll-srcset]:not([data-ll-srcset-ready])').forEach(picture => {
195
+ picture.removeAttribute('data-ll-srcset-initialized')
196
+ // Clear ready state from sources and img so they can be re-processed
197
+ picture.querySelectorAll('source').forEach(source => {
198
+ source.removeAttribute('data-ll-ready')
199
+ })
200
+ picture.querySelectorAll('img').forEach(img => {
201
+ img.removeAttribute('data-ll-idx')
202
+ img.removeAttribute('data-ll-loaded')
203
+ img.removeAttribute('data-ll-ready')
204
+ })
205
+ })
206
+
207
+ app.lazyload.observe(clone)
208
+ }
184
209
  }
185
210
 
186
211
  // Force layout recalculation
@@ -390,10 +415,11 @@ function horizontalLoop(app, items, config) {
390
415
  // In reset zone but item doesn't need reset → keep current offset
391
416
  newOffset = itemWrapOffsets[i]
392
417
  } else if (itemLeft < -(widths[i] + containerWidth * 0.5)) {
393
- // Item exited LEFT edge
394
- // Forward drag (low boundedPos): wrap to END
395
- // Backward drag (high boundedPos): don't wrap, clones fill in from right
396
- newOffset = boundedPos < originalItemsWidth / 2 ? wrapOffset : 0
418
+ // Item exited LEFT edge - only wrap during forward scroll
419
+ // During reverse scroll (scrollDirection < 0), items off-screen left
420
+ // will naturally scroll back into view - don't wrap them
421
+ const isForwardScroll = scrollDirection >= 0
422
+ newOffset = (isForwardScroll && boundedPos < originalItemsWidth / 2) ? wrapOffset : 0
397
423
  } else if (itemLeft > containerWidth + containerWidth * 0.5) {
398
424
  // Item exited RIGHT edge
399
425
  // This shouldn't happen much, but handle it
@@ -532,6 +558,13 @@ function horizontalLoop(app, items, config) {
532
558
  // Set up boundedPos motionValue to automatically sync with position
533
559
  // This calculates the bounded position (0 to originalItemsWidth)
534
560
  const positionUnsubscribe = position.on('change', latest => {
561
+ // Track scroll direction for wrap logic
562
+ const delta = latest - lastPositionForDirection
563
+ if (Math.abs(delta) > 1) {
564
+ scrollDirection = delta > 0 ? 1 : -1
565
+ }
566
+ lastPositionForDirection = latest
567
+
535
568
  const bounded = ((latest % originalItemsWidth) + originalItemsWidth) % originalItemsWidth
536
569
  boundedPos.set(bounded)
537
570
  })
@@ -1165,7 +1198,8 @@ function horizontalLoop(app, items, config) {
1165
1198
  const currentPos = position.get()
1166
1199
 
1167
1200
  // Calculate position within current cycle (using originalItemsWidth)
1168
- const cyclePos = currentPos % originalItemsWidth
1201
+ // Use proper modulo for negative positions (dragging right/backward)
1202
+ const cyclePos = ((currentPos % originalItemsWidth) + originalItemsWidth) % originalItemsWidth
1169
1203
  const remainingDist = originalItemsWidth - cyclePos
1170
1204
  const remainingDuration = remainingDist / pixelsPerSecond
1171
1205