@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.
|
|
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.
|
|
47
|
+
"motion": "^12.35.2"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@playwright/test": "^1.
|
|
51
|
-
"@types/node": "^22
|
|
50
|
+
"@playwright/test": "^1.58.2",
|
|
51
|
+
"@types/node": "^22",
|
|
52
52
|
"typescript": "^5.9.3",
|
|
53
|
-
"vite": "^
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|