@brandocms/jupiter 5.0.0-beta.10 → 5.0.0-beta.12

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.10",
3
+ "version": "5.0.0-beta.12",
4
4
  "description": "Frontend helpers.",
5
5
  "author": "Univers/Twined",
6
6
  "license": "UNLICENSED",
@@ -44,13 +44,13 @@
44
44
  "types": "types/index.d.ts",
45
45
  "dependencies": {
46
46
  "lodash.defaultsdeep": "^4.6.1",
47
- "motion": "^12.35.1"
47
+ "motion": "^12.35.2"
48
48
  },
49
49
  "devDependencies": {
50
- "@playwright/test": "^1.57.0",
51
- "@types/node": "^22.19.3",
50
+ "@playwright/test": "^1.58.2",
51
+ "@types/node": "^22",
52
52
  "typescript": "^5.9.3",
53
- "vite": "^6.4.1"
53
+ "vite": "^7.3.1"
54
54
  },
55
55
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
56
56
  }
@@ -202,6 +202,12 @@ export default class FixedHeader {
202
202
  */
203
203
  constructor(app, opts = {}) {
204
204
  this.app = app
205
+ // Preserve raw section configs before _defaultsDeep mutates them
206
+ this._rawSections = opts.sections
207
+ ? Object.fromEntries(
208
+ Object.entries(opts.sections).map(([k, v]) => [k, { ...v }])
209
+ )
210
+ : {}
205
211
  this.mainOpts = _defaultsDeep(opts, DEFAULT_OPTIONS)
206
212
 
207
213
  if (typeof this.mainOpts.el === 'string') {
@@ -704,6 +710,59 @@ export default class FixedHeader {
704
710
  )
705
711
  }
706
712
 
713
+ /**
714
+ * Reconfigure the header for a new section/page.
715
+ * Call this after a view transition or SPA navigation
716
+ * to re-resolve section options and reset scroll state.
717
+ */
718
+ reconfigure() {
719
+ const section = document.body.getAttribute('data-script')
720
+
721
+ // Build a fresh opts object from raw sections so function
722
+ // offsets that were previously resolved to numbers are restored
723
+ const freshOpts = {
724
+ ...this.mainOpts,
725
+ sections: Object.fromEntries(
726
+ Object.entries(this._rawSections).map(([k, v]) => [k, { ...v }])
727
+ )
728
+ }
729
+ this.opts = this._getOptionsForSection(section, freshOpts)
730
+
731
+ // Re-resolve dynamic offsets
732
+ if (typeof this.opts.offsetBg === 'string') {
733
+ const offsetBgElm = document.querySelector(this.opts.offsetBg)
734
+ this.opts.offsetBg = offsetBgElm ? offsetBgElm.offsetTop : 200
735
+ } else if (typeof this.opts.offsetBg === 'function') {
736
+ this.opts.offsetBg = this.opts.offsetBg(this) - 1
737
+ }
738
+
739
+ if (typeof this.opts.offset === 'string') {
740
+ const offsetElm = document.querySelector(this.opts.offset)
741
+ this.opts.offset = offsetElm ? offsetElm.offsetTop - 1 : 0
742
+ } else if (typeof this.opts.offset === 'function') {
743
+ this.opts.offset = this.opts.offset(this) - 1
744
+ }
745
+
746
+ if (typeof this.opts.offsetSmall === 'string') {
747
+ const offsetSmallElm = document.querySelector(this.opts.offsetSmall)
748
+ this.opts.offsetSmall = offsetSmallElm ? offsetSmallElm.offsetTop - 1 : 50
749
+ } else if (typeof this.opts.offsetSmall === 'function') {
750
+ this.opts.offsetSmall = this.opts.offsetSmall(this) - 1
751
+ }
752
+
753
+ // Reset scroll tracking to prevent the scroll-height-change
754
+ // guard from bailing out after content swap
755
+ this.lastKnownScrollY = this.getScrollY()
756
+ this.lastKnownScrollHeight = document.body.scrollHeight
757
+ this.currentScrollY = this.lastKnownScrollY
758
+ this.currentScrollHeight = this.lastKnownScrollHeight
759
+
760
+ // Re-check current state
761
+ this.checkSize(true)
762
+ this.checkBg(true)
763
+ this.checkTop(true)
764
+ }
765
+
707
766
  _getOptionsForSection(section, opts) {
708
767
  // if section is not a key in opts, return default opts
709
768
  if (
@@ -18,6 +18,7 @@ import Dom from '../Dom'
18
18
 
19
19
  const DEFAULT_OPTIONS = {
20
20
  center: false,
21
+ peek: false, // Centers viewport on the gap between two items (half | full | full | half)
21
22
  snap: false, // Set to true to enable snap-to-item behavior
22
23
  crawl: true, // Continuous auto-scrolling
23
24
  loop: true, // Infinite looping (false for linear scrolling)
@@ -257,7 +258,15 @@ function horizontalLoop(app, items, config) {
257
258
 
258
259
  // Calculate max scroll for non-looping based on endAlignment
259
260
  if (!shouldLoop && originalItemCount > 0) {
260
- if (config.centerSlide) {
261
+ if (config.peek) {
262
+ // Peek mode: max scroll where gap after last item is at viewport center
263
+ const lastItemIndex = originalItemCount - 1
264
+ const lastItemRightEdge = offsetLefts[lastItemIndex] + widths[lastItemIndex] - startX
265
+ const viewportCenter = containerWidth / 2
266
+ const idealMaxScroll = lastItemRightEdge + gap / 2 - viewportCenter
267
+ const absoluteMax = lastItemRightEdge - containerWidth
268
+ maxScrollPosition = Math.max(0, Math.min(idealMaxScroll, absoluteMax))
269
+ } else if (config.centerSlide) {
261
270
  // Center mode: max scroll is where last item's center is at viewport center
262
271
  // But clamped so we don't show empty space
263
272
  const lastItemIndex = originalItemCount - 1
@@ -294,7 +303,13 @@ function horizontalLoop(app, items, config) {
294
303
  const curX = (xPercents[i] / 100) * widths[i]
295
304
  let snapPos
296
305
 
297
- if (config.centerSlide) {
306
+ if (config.peek) {
307
+ // Peek mode: viewport center at the gap AFTER this item
308
+ // This shows: half(i) | gap | full(i+1) | gap | full(i+2) | gap | half(i+3)
309
+ const itemRightEdge = item.offsetLeft + curX + widths[i] - startX
310
+ const viewportCenter = containerWidth / 2
311
+ snapPos = itemRightEdge + gap / 2 - viewportCenter
312
+ } else if (config.centerSlide) {
298
313
  // Center mode: item's center at viewport's center
299
314
  const itemCenter = item.offsetLeft + curX + widths[i] / 2 - startX
300
315
  const viewportCenter = containerWidth / 2
@@ -314,7 +329,12 @@ function horizontalLoop(app, items, config) {
314
329
  const curX = (xPercents[i] / 100) * widths[i]
315
330
  let snapPos
316
331
 
317
- if (config.centerSlide) {
332
+ if (config.peek) {
333
+ // Peek mode: viewport center at the gap AFTER this item
334
+ const itemRightEdge = item.offsetLeft + curX + widths[i] - startX
335
+ const viewportCenter = containerWidth / 2
336
+ snapPos = itemRightEdge + gap / 2 - viewportCenter
337
+ } else if (config.centerSlide) {
318
338
  // Center mode: item's center at viewport's center
319
339
  const itemCenter = item.offsetLeft + curX + widths[i] / 2 - startX
320
340
  const viewportCenter = containerWidth / 2
@@ -555,6 +575,19 @@ function horizontalLoop(app, items, config) {
555
575
  populateWidths()
556
576
  populateSnapTimes()
557
577
 
578
+ // Warn if [data-looper] has overflow-x: clip — this clips wrapped items
579
+ // whose individual translateX positions fall outside the container's bounds,
580
+ // even when they are visually positioned within the viewport.
581
+ // Apply overflow-x: clip on a parent element instead.
582
+ const itemsContainer = items[0].parentElement
583
+ const containerOverflow = getComputedStyle(itemsContainer).overflowX
584
+ if (containerOverflow === 'clip') {
585
+ console.warn(
586
+ `[Looper] ⚠️ [data-looper] has overflow-x: clip which will hide looped items. Apply overflow-x: clip on a parent wrapper element instead.`,
587
+ itemsContainer
588
+ )
589
+ }
590
+
558
591
  // Set initial container position
559
592
  const containerElement = items[0].parentElement
560
593
  containerElement.style.willChange = 'transform'
@@ -1614,6 +1647,12 @@ export default class Looper {
1614
1647
  const centerValue = looperEl?.getAttribute('data-looper-center')
1615
1648
  const shouldCenterSlide = centerValue === 'false' ? false : hasCenterAttr
1616
1649
 
1650
+ // Peek: data-looper-peek or data-looper-peek="false"
1651
+ // Centers viewport on the gap between two items (half | full | full | half)
1652
+ const hasPeekAttr = looperEl?.hasAttribute('data-looper-peek')
1653
+ const peekValue = looperEl?.getAttribute('data-looper-peek')
1654
+ const shouldPeek = peekValue === 'false' ? false : hasPeekAttr || this.opts.peek
1655
+
1617
1656
  // Create stub for Moonwalk compatibility
1618
1657
  const stubLoop = {
1619
1658
  play: () => {},
@@ -1633,7 +1672,8 @@ export default class Looper {
1633
1672
  repeat: -1,
1634
1673
  draggable: this.opts.draggable,
1635
1674
  center: this.opts.center,
1636
- centerSlide: shouldCenterSlide,
1675
+ centerSlide: shouldCenterSlide || shouldPeek,
1676
+ peek: shouldPeek,
1637
1677
  snap: shouldSnap,
1638
1678
  speed,
1639
1679
  reversed: isReverse,