News
We Go Season 2 Episode 3: The Path to The Eddie
Shop The Story
{"id":8047520350343,
"handle":"ahi-mens-boat-shoes-ash-sterling",
"title":"‘Ahi - Ash \/ Sterling",
"available":true,
"tags":["ahi","arch:standard","Beach\/Boat","best:beach_boat","boat","boat ahoe","boat shoe","boaters","boatshoes","cat:shoes","drop-in heel®","fisherman","fisherman shoe","fishing","fishing shoe","fit:standard","free shipping","gender:men","landtextile","material:textilemens","men's","RGroup_10554","shoes","size:true","sneakers","split_color","style:10554","swim shoes","water friendly shoe","water resisitant","water resistant","water-friendly","waterboat","watershoe","watershoes"],
"type":"Men’s Performance Marine Shoe",
"options_with_values": [{"name":"Color","position":1,"values":["Ash \/ Sterling"]},{"name":"Size","position":2,"values":["7","8","8.5","9","9.5","10","10.5","11","11.5","12","13","14","15"]}],
"price":18000,
"price_min":18000,
"price_max":18000,
"compare_at_price":null,"featured_image": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_001_M_AHI_AshSterling.png?v=1741308464",
"featured_image_aspect_ratio": 1.0,
"featured_image_width": 2000,
"featured_image_height": 2000,
"featured_image_id": 33819070169223,
"featured_image_alt": "Ash \/ Sterling",
"hover_image": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_103_678d4a23-4d9a-4869-a8b3-07fb9edcc30e.jpg?v=1744996484",
"hover_image_alt": "Ash \/ Sterling",
"images": [{
"id": 33819070169223,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_001_M_AHI_AshSterling.png?v=1741308464",
"alt": "Ash \/ Sterling",
"width": 2000,
"height": 2000
},{
"id": 34414987411591,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_103_678d4a23-4d9a-4869-a8b3-07fb9edcc30e.jpg?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 1080,
"height": 1080
},{
"id": 34414987444359,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/Ahi_-_10554-AU6L_Ash_-_Sterling_1.jpg?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 2000,
"height": 2000
},{
"id": 34414987477127,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_002_M_AHI_AshSterling_827409b1-c954-4365-a9f1-27adb436229e.png?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 2000,
"height": 2000
},{
"id": 34414987509895,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_101_a19cc6b2-725f-4e24-ac06-069a5d4de2f4.jpg?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 2000,
"height": 2000
},{
"id": 34414987542663,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_103_f8694666-9f7d-4d80-91ba-50acb488ba0e.jpg?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 2000,
"height": 2000
},{
"id": 34414987575431,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/Ahi_-_10554-AU6L_Ash_-_Sterling_2.jpg?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 2000,
"height": 2000
},{
"id": 34414987608199,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_003_M_AHI_AshSterling_2474c930-944d-4dd7-8e45-e3bc98b5f9d0.png?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 2000,
"height": 2000
},{
"id": 34414987640967,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_102_a226a075-ae5a-46e6-9636-a1b6ccff0615.jpg?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 2000,
"height": 2000
},{
"id": 34414987673735,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_104_d085b704-1682-4176-accc-cd7432d24a23.jpg?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 1080,
"height": 1080
},{
"id": 34414987706503,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_004_M_AHI_AshSterling_ab4e6c88-df8f-4e5d-93a5-5dc866ed6aef.png?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 2000,
"height": 2000
},{
"id": 34414987739271,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/Ash-Wet_Grip.jpg?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 750,
"height": 750
},{
"id": 34414987772039,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10554_AU6L_005_M_AHI_AshSterling_0ccd2385-0c00-44b6-8840-dc258c816634.png?v=1744996484",
"alt": "Ash \/ Sterling",
"width": 2000,
"height": 2000
}],
"plp_image_order_positions": "",
"sku": "10554-AU6L-8"
}
{ $dispatch('product-item:images-changed', { product: product, source: 'swatch-container' }) })"
@pointercancel="intendedSwatchIndex = null; intendedSwatchEl = null"
x-data="{
wrapper: $el,
selectedSibling:product.siblings[0],
intendedSwatchIndex: null,
intendedSwatchEl: null,
swatchGap: 8,
scrollable: product.siblings.filter(s => !!s.available).length > 9.5,
canPrev: false,
canNext: true,
_edgeReady: false,
imageLimit: 0,
limitImages(nextProduct) {
const images = Array.isArray(nextProduct && nextProduct.images) ? nextProduct.images : [];
const limitedImages = this.imageLimit > 0 ? images.slice(0, this.imageLimit) : images;
const hoverImage = limitedImages[1] || null;
return Object.assign({}, nextProduct, {
images: limitedImages,
hover_image: hoverImage ? hoverImage.src : null,
hover_image_alt: hoverImage ? hoverImage.alt : null
});
},
selectProduct(nextProduct) {
const productRoot = $el.closest('.product-item');
const productData = productRoot && productRoot._x_dataStack && productRoot._x_dataStack[0];
if (productData && typeof productData.setProduct === 'function') {
productData.setProduct(nextProduct);
} else {
$data.product = this.limitImages(nextProduct);
}
},
getSwatchImageSrc(sibling) {
return sibling.images[0].src;
},
buildSwatchSrcset(src) {
if (!src) return '';
const widths = ['48', '96', '192'];
const separator = src.includes('?') ? '&' : '?';
return widths.map(w => `${src}${separator}width=${w} ${w}w`).join(', ');
},
isSwatchPriority(index) {
return window.innerWidth >= 1024 ? index < 3 : index < 2;
},
swatchBg(sibling) {
// swatch_bg precomputed at hydration. Nullish (not ||): null = no colors set
// (fall back to theme default), empty string = invalid combo, no fallback.
return sibling.swatch_bg ?? '#000000';
},
centerSwatch(el) {
if ($data.wrapper.scrollWidth <= $data.wrapper.clientWidth) return false
const wrapperRect = $data.wrapper.getBoundingClientRect()
const elRect = el.getBoundingClientRect()
const centerPoint = wrapperRect.x + (wrapperRect.width / 2)
const behavior = window.innerWidth < 1024 ? 'auto' : 'smooth'
$data.wrapper.scrollTo({
left: $data.wrapper.scrollLeft + (elRect.x - centerPoint + (elRect.width / 2)),
behavior
})
},
selectSwatchFromTap(siblingIndex, el) {
const idx = (this.intendedSwatchIndex !== null && this.intendedSwatchIndex !== undefined)
? this.intendedSwatchIndex
: siblingIndex
const targetEl = this.intendedSwatchEl || el
this.intendedSwatchIndex = null
this.intendedSwatchEl = null
const next = product.siblings[idx]
if (next) product = next
this.centerSwatch(targetEl)
},
scroll(direction = 'forward') {
const swatch = $data.wrapper.querySelector('.product-item__swatch')
const unit = swatch ? swatch.offsetWidth + $data.swatchGap : $data.wrapper.clientWidth * 0.3
let amount = 2 * unit
$data.wrapper.scrollTo({
left: direction == 'forward' ? $data.wrapper.scrollLeft + amount : $data.wrapper.scrollLeft - amount,
behavior:'smooth'
})
},
// Lazily wire the prev/next edge detection on first desktop hover/focus —
// the arrows are a desktop affordance, so no observer is built at load and
// none is ever built on mobile. canPrev/canNext default to the at-start state.
initSwatchEdges() {
if (this._edgeReady || window.innerWidth <= 1023) return
this._edgeReady = true
const root = this.wrapper
const swatches = root.querySelectorAll('.product-item__swatch')
if (swatches.length < 2) return
const first = swatches[0], last = swatches[swatches.length - 1]
const io = new IntersectionObserver((entries) => {
const d = window.Alpine && window.Alpine.$data ? window.Alpine.$data(root) : null
if (!d) return
entries.forEach((e) => {
if (e.target === first) d.canPrev = !e.isIntersecting
if (e.target === last) d.canNext = !e.isIntersecting
})
}, { root, threshold: 0.99 })
io.observe(first)
io.observe(last)
root._swatchEdgeIO = io
}
}"
@pointerenter="initSwatchEdges()"
@focusin="initSwatchEdges()"
>
{"id":8134359122055,
"handle":"makiki-mens-athletic-shoes-cloud-grey-poi",
"title":"Makiki - Cloud Grey \/ Poi",
"available":true,
"tags":["arch:standard","best seller","best selling","best:everday_wear","best:travel","boat shoe","boatshoes","cat:shoes","color:blue","drop-in heel®","enhance gip","enhanced traction outsole","Everyday Wear","fit:standard","free shipping","gender:men","labor day","landtextile","likes:beach","likes:exercising","likes:friends","likes:hiking","likes:relaxing","likes:urban-exploring","makiki","makiki olukai","makiki shoes for sea","material:textile","mens","mesh","no shoestring","RGroup_10567","shoes","size:true","sneakers","split_color","style:10567","swim","swim shoes","Travel","vegan","water-friendly","wide toed","wide width"],
"type":"Men’s Athletic Shoes",
"options_with_values": [{"name":"Color","position":1,"values":["Cloud Grey \/ Poi"]},{"name":"Size","position":2,"values":["7","8","8.5","9","9.5","10","10.5","11","11.5","12","13","14","15"]}],
"price":18000,
"price_min":18000,
"price_max":18000,
"compare_at_price":null,"featured_image": "\/\/olukai.ca\/cdn\/shop\/files\/10567_2N25_001_M_MAKIKI_CloudGreyPoi_f4b5d736-60bd-465a-b5a0-4780b9efe973.png?v=1759467643",
"featured_image_aspect_ratio": 1.0,
"featured_image_width": 2000,
"featured_image_height": 2000,
"featured_image_id": 35110507217031,
"featured_image_alt": "Cloud Grey \/ Poi",
"hover_image": "\/\/olukai.ca\/cdn\/shop\/files\/10567-2N25-202.jpg?v=1751320159",
"hover_image_alt": "Makiki - Cloud Grey \/ Poi",
"images": [{
"id": 35110507217031,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10567_2N25_001_M_MAKIKI_CloudGreyPoi_f4b5d736-60bd-465a-b5a0-4780b9efe973.png?v=1759467643",
"alt": "Cloud Grey \/ Poi",
"width": 2000,
"height": 2000
},{
"id": 35180883509383,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10567-2N25-202.jpg?v=1751320159",
"alt": "Makiki - Cloud Grey \/ Poi",
"width": 2000,
"height": 2000
},{
"id": 35110507249799,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10567_2N25_003_M_MAKIKI_CloudGreyPoi_1806c052-4b56-4ca7-a824-1a889b780abd.png?v=1751320159",
"alt": "Cloud Grey \/ Poi",
"width": 2000,
"height": 2000
},{
"id": 35180881543303,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10567-2N25-201.jpg?v=1751320159",
"alt": "Makiki - Cloud Grey \/ Poi",
"width": 2000,
"height": 2000
},{
"id": 35110507282567,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10567_2N25_004_M_MAKIKI_CloudGreyPoi_85d5203c-d7aa-43e9-96b2-d3384cdcf099.png?v=1751320158",
"alt": "Cloud Grey \/ Poi",
"width": 2000,
"height": 2000
},{
"id": 35110507348103,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10567_2N25_005_M_MAKIKI_CloudGreyPoi_e42c63f4-1f7a-403e-bddf-c7734400bc7a.png?v=1751320158",
"alt": "Cloud Grey \/ Poi",
"width": 2000,
"height": 2000
}],
"plp_image_order_positions": "",
"sku": "10567-2N25-7"
}
{ $dispatch('product-item:images-changed', { product: product, source: 'swatch-container' }) })"
@pointercancel="intendedSwatchIndex = null; intendedSwatchEl = null"
x-data="{
wrapper: $el,
selectedSibling:product.siblings[0],
intendedSwatchIndex: null,
intendedSwatchEl: null,
swatchGap: 8,
scrollable: product.siblings.filter(s => !!s.available).length > 9.5,
canPrev: false,
canNext: true,
_edgeReady: false,
imageLimit: 0,
limitImages(nextProduct) {
const images = Array.isArray(nextProduct && nextProduct.images) ? nextProduct.images : [];
const limitedImages = this.imageLimit > 0 ? images.slice(0, this.imageLimit) : images;
const hoverImage = limitedImages[1] || null;
return Object.assign({}, nextProduct, {
images: limitedImages,
hover_image: hoverImage ? hoverImage.src : null,
hover_image_alt: hoverImage ? hoverImage.alt : null
});
},
selectProduct(nextProduct) {
const productRoot = $el.closest('.product-item');
const productData = productRoot && productRoot._x_dataStack && productRoot._x_dataStack[0];
if (productData && typeof productData.setProduct === 'function') {
productData.setProduct(nextProduct);
} else {
$data.product = this.limitImages(nextProduct);
}
},
getSwatchImageSrc(sibling) {
return sibling.images[0].src;
},
buildSwatchSrcset(src) {
if (!src) return '';
const widths = ['48', '96', '192'];
const separator = src.includes('?') ? '&' : '?';
return widths.map(w => `${src}${separator}width=${w} ${w}w`).join(', ');
},
isSwatchPriority(index) {
return window.innerWidth >= 1024 ? index < 3 : index < 2;
},
swatchBg(sibling) {
// swatch_bg precomputed at hydration. Nullish (not ||): null = no colors set
// (fall back to theme default), empty string = invalid combo, no fallback.
return sibling.swatch_bg ?? '#000000';
},
centerSwatch(el) {
if ($data.wrapper.scrollWidth <= $data.wrapper.clientWidth) return false
const wrapperRect = $data.wrapper.getBoundingClientRect()
const elRect = el.getBoundingClientRect()
const centerPoint = wrapperRect.x + (wrapperRect.width / 2)
const behavior = window.innerWidth < 1024 ? 'auto' : 'smooth'
$data.wrapper.scrollTo({
left: $data.wrapper.scrollLeft + (elRect.x - centerPoint + (elRect.width / 2)),
behavior
})
},
selectSwatchFromTap(siblingIndex, el) {
const idx = (this.intendedSwatchIndex !== null && this.intendedSwatchIndex !== undefined)
? this.intendedSwatchIndex
: siblingIndex
const targetEl = this.intendedSwatchEl || el
this.intendedSwatchIndex = null
this.intendedSwatchEl = null
const next = product.siblings[idx]
if (next) product = next
this.centerSwatch(targetEl)
},
scroll(direction = 'forward') {
const swatch = $data.wrapper.querySelector('.product-item__swatch')
const unit = swatch ? swatch.offsetWidth + $data.swatchGap : $data.wrapper.clientWidth * 0.3
let amount = 2 * unit
$data.wrapper.scrollTo({
left: direction == 'forward' ? $data.wrapper.scrollLeft + amount : $data.wrapper.scrollLeft - amount,
behavior:'smooth'
})
},
// Lazily wire the prev/next edge detection on first desktop hover/focus —
// the arrows are a desktop affordance, so no observer is built at load and
// none is ever built on mobile. canPrev/canNext default to the at-start state.
initSwatchEdges() {
if (this._edgeReady || window.innerWidth <= 1023) return
this._edgeReady = true
const root = this.wrapper
const swatches = root.querySelectorAll('.product-item__swatch')
if (swatches.length < 2) return
const first = swatches[0], last = swatches[swatches.length - 1]
const io = new IntersectionObserver((entries) => {
const d = window.Alpine && window.Alpine.$data ? window.Alpine.$data(root) : null
if (!d) return
entries.forEach((e) => {
if (e.target === first) d.canPrev = !e.isIntersecting
if (e.target === last) d.canNext = !e.isIntersecting
})
}, { root, threshold: 0.99 })
io.observe(first)
io.observe(last)
root._swatchEdgeIO = io
}
}"
@pointerenter="initSwatchEdges()"
@focusin="initSwatchEdges()"
>
{"id":5156404330631,
"handle":"ohana-mens-beach-sandals-black",
"title":"‘Ohana - Black",
"available":true,
"tags":["'oahni","arch:high","beach sandals","Beach\/Boat","beachbum","best selling","best:beach_boat","best:everday_wear","best:post_workout","black fliflps","cat:sandals","collection:tide-tracker","collection:wfh","color:Black","enhanced traction outsole","Everyday Wear","extra-colors","fit:wide","flip flops","gender:men","hana","labor day","likes:beach","material:synthetic","men","men s flipslops","mens","men’s","o hana","o hanas","obama","Oh","oha","ohan","ohana men s sandals","ohanaa","ohanaflip flops","ohanna","ohano","ohanu","ohona","ohsna","omaha","Post-Workout","prime","RGroup_10110","size:true","split_color","style:10110","synthetic","tempstandard","vacation:tropical","vegan","water-friendly","wide sizes","wide width","‘ohana"],
"type":"Men’s Water-Friendly Beach Sandals",
"options_with_values": [{"name":"Color","position":1,"values":["Black"]},{"name":"Size","position":2,"values":["7","8","9","10","11","12","13","14","15","16","17","18"]}],
"price":10000,
"price_min":10000,
"price_max":10000,
"compare_at_price":null,"featured_image": "\/\/olukai.ca\/cdn\/shop\/files\/10110_4040_001_M_Ohana_Black_84c6271e-8234-42af-9968-71b01e32ef62.png?v=1762441464",
"featured_image_aspect_ratio": 1.0,
"featured_image_width": 2000,
"featured_image_height": 2000,
"featured_image_id": 34400636338311,
"featured_image_alt": "Black",
"hover_image": "\/\/olukai.ca\/cdn\/shop\/files\/10110_4040_105_M_Ohana_BlackBlack_png_355aee4c-6dc7-4bc7-856f-07c0ca88d3bc.jpg?v=1746734369",
"hover_image_alt": "Black",
"images": [{
"id": 34400636338311,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10110_4040_001_M_Ohana_Black_84c6271e-8234-42af-9968-71b01e32ef62.png?v=1762441464",
"alt": "Black",
"width": 2000,
"height": 2000
},{
"id": 34400636371079,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10110_4040_105_M_Ohana_BlackBlack_png_355aee4c-6dc7-4bc7-856f-07c0ca88d3bc.jpg?v=1746734369",
"alt": "Black",
"width": 2000,
"height": 2000
},{
"id": 34400636403847,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10110_4040_201_c2335a22-00b9-4183-b4bf-7d2d47d4f276.jpg?v=1746734369",
"alt": "Black",
"width": 2000,
"height": 2000
},{
"id": 34400636436615,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/Ohana-Black-galleryimage-03_dc258274-50fd-493b-959e-8bd4afefc665.jpg?v=1746734369",
"alt": "Black",
"width": 2000,
"height": 2000
},{
"id": 34400636469383,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10110_4040_102_M_Ohana_BlackBlack_png_2adb8891-22a1-4031-9d42-2002cb6cb522.jpg?v=1746734369",
"alt": "Black",
"width": 2000,
"height": 2000
},{
"id": 34400636502151,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10110_4040_103_M_Ohana_BlackBlack_png_d155f466-0b21-4c8c-8b01-dd4e445a094f.jpg?v=1746734369",
"alt": "Black",
"width": 2000,
"height": 2000
},{
"id": 34400636534919,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10110_4040_202_eadd8bc1-03c8-4170-9c11-95f95f221b6e.jpg?v=1746734369",
"alt": "Black",
"width": 2000,
"height": 2000
},{
"id": 34400636567687,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10110_4040_203_acd9975b-82b2-44c7-981c-c4dbf57a2bee.jpg?v=1746734369",
"alt": "Black",
"width": 2000,
"height": 2000
},{
"id": 34400636600455,
"src": "\/\/olukai.ca\/cdn\/shop\/files\/10110_4040_005_M_Ohana_Black.png?v=1746734369",
"alt": "Black",
"width": 2000,
"height": 2000
}],
"plp_image_order_positions": "",
"sku": "10110-4040-7"
}
{ $dispatch('product-item:images-changed', { product: product, source: 'swatch-container' }) })"
@pointercancel="intendedSwatchIndex = null; intendedSwatchEl = null"
x-data="{
wrapper: $el,
selectedSibling:product.siblings[0],
intendedSwatchIndex: null,
intendedSwatchEl: null,
swatchGap: 8,
scrollable: product.siblings.filter(s => !!s.available).length > 9.5,
canPrev: false,
canNext: true,
_edgeReady: false,
imageLimit: 0,
limitImages(nextProduct) {
const images = Array.isArray(nextProduct && nextProduct.images) ? nextProduct.images : [];
const limitedImages = this.imageLimit > 0 ? images.slice(0, this.imageLimit) : images;
const hoverImage = limitedImages[1] || null;
return Object.assign({}, nextProduct, {
images: limitedImages,
hover_image: hoverImage ? hoverImage.src : null,
hover_image_alt: hoverImage ? hoverImage.alt : null
});
},
selectProduct(nextProduct) {
const productRoot = $el.closest('.product-item');
const productData = productRoot && productRoot._x_dataStack && productRoot._x_dataStack[0];
if (productData && typeof productData.setProduct === 'function') {
productData.setProduct(nextProduct);
} else {
$data.product = this.limitImages(nextProduct);
}
},
getSwatchImageSrc(sibling) {
return sibling.images[0].src;
},
buildSwatchSrcset(src) {
if (!src) return '';
const widths = ['48', '96', '192'];
const separator = src.includes('?') ? '&' : '?';
return widths.map(w => `${src}${separator}width=${w} ${w}w`).join(', ');
},
isSwatchPriority(index) {
return window.innerWidth >= 1024 ? index < 3 : index < 2;
},
swatchBg(sibling) {
// swatch_bg precomputed at hydration. Nullish (not ||): null = no colors set
// (fall back to theme default), empty string = invalid combo, no fallback.
return sibling.swatch_bg ?? '#000000';
},
centerSwatch(el) {
if ($data.wrapper.scrollWidth <= $data.wrapper.clientWidth) return false
const wrapperRect = $data.wrapper.getBoundingClientRect()
const elRect = el.getBoundingClientRect()
const centerPoint = wrapperRect.x + (wrapperRect.width / 2)
const behavior = window.innerWidth < 1024 ? 'auto' : 'smooth'
$data.wrapper.scrollTo({
left: $data.wrapper.scrollLeft + (elRect.x - centerPoint + (elRect.width / 2)),
behavior
})
},
selectSwatchFromTap(siblingIndex, el) {
const idx = (this.intendedSwatchIndex !== null && this.intendedSwatchIndex !== undefined)
? this.intendedSwatchIndex
: siblingIndex
const targetEl = this.intendedSwatchEl || el
this.intendedSwatchIndex = null
this.intendedSwatchEl = null
const next = product.siblings[idx]
if (next) product = next
this.centerSwatch(targetEl)
},
scroll(direction = 'forward') {
const swatch = $data.wrapper.querySelector('.product-item__swatch')
const unit = swatch ? swatch.offsetWidth + $data.swatchGap : $data.wrapper.clientWidth * 0.3
let amount = 2 * unit
$data.wrapper.scrollTo({
left: direction == 'forward' ? $data.wrapper.scrollLeft + amount : $data.wrapper.scrollLeft - amount,
behavior:'smooth'
})
},
// Lazily wire the prev/next edge detection on first desktop hover/focus —
// the arrows are a desktop affordance, so no observer is built at load and
// none is ever built on mobile. canPrev/canNext default to the at-start state.
initSwatchEdges() {
if (this._edgeReady || window.innerWidth <= 1023) return
this._edgeReady = true
const root = this.wrapper
const swatches = root.querySelectorAll('.product-item__swatch')
if (swatches.length < 2) return
const first = swatches[0], last = swatches[swatches.length - 1]
const io = new IntersectionObserver((entries) => {
const d = window.Alpine && window.Alpine.$data ? window.Alpine.$data(root) : null
if (!d) return
entries.forEach((e) => {
if (e.target === first) d.canPrev = !e.isIntersecting
if (e.target === last) d.canNext = !e.isIntersecting
})
}, { root, threshold: 0.99 })
io.observe(first)
io.observe(last)
root._swatchEdgeIO = io
}
}"
@pointerenter="initSwatchEdges()"
@focusin="initSwatchEdges()"
>
{ $dispatch('product-item:images-changed', { product: product, source: 'swatch-container' }) })"
@pointercancel="intendedSwatchIndex = null; intendedSwatchEl = null"
x-data="{
wrapper: $el,
selectedSibling:product.siblings[0],
intendedSwatchIndex: null,
intendedSwatchEl: null,
swatchGap: 8,
scrollable: product.siblings.filter(s => !!s.available).length > 9.5,
canPrev: false,
canNext: true,
_edgeReady: false,
imageLimit: 0,
limitImages(nextProduct) {
const images = Array.isArray(nextProduct && nextProduct.images) ? nextProduct.images : [];
const limitedImages = this.imageLimit > 0 ? images.slice(0, this.imageLimit) : images;
const hoverImage = limitedImages[1] || null;
return Object.assign({}, nextProduct, {
images: limitedImages,
hover_image: hoverImage ? hoverImage.src : null,
hover_image_alt: hoverImage ? hoverImage.alt : null
});
},
selectProduct(nextProduct) {
const productRoot = $el.closest('.product-item');
const productData = productRoot && productRoot._x_dataStack && productRoot._x_dataStack[0];
if (productData && typeof productData.setProduct === 'function') {
productData.setProduct(nextProduct);
} else {
$data.product = this.limitImages(nextProduct);
}
},
getSwatchImageSrc(sibling) {
return sibling.images[0].src;
},
buildSwatchSrcset(src) {
if (!src) return '';
const widths = ['48', '96', '192'];
const separator = src.includes('?') ? '&' : '?';
return widths.map(w => `${src}${separator}width=${w} ${w}w`).join(', ');
},
isSwatchPriority(index) {
return window.innerWidth >= 1024 ? index < 3 : index < 2;
},
swatchBg(sibling) {
// swatch_bg precomputed at hydration. Nullish (not ||): null = no colors set
// (fall back to theme default), empty string = invalid combo, no fallback.
return sibling.swatch_bg ?? '#000000';
},
centerSwatch(el) {
if ($data.wrapper.scrollWidth <= $data.wrapper.clientWidth) return false
const wrapperRect = $data.wrapper.getBoundingClientRect()
const elRect = el.getBoundingClientRect()
const centerPoint = wrapperRect.x + (wrapperRect.width / 2)
const behavior = window.innerWidth < 1024 ? 'auto' : 'smooth'
$data.wrapper.scrollTo({
left: $data.wrapper.scrollLeft + (elRect.x - centerPoint + (elRect.width / 2)),
behavior
})
},
selectSwatchFromTap(siblingIndex, el) {
const idx = (this.intendedSwatchIndex !== null && this.intendedSwatchIndex !== undefined)
? this.intendedSwatchIndex
: siblingIndex
const targetEl = this.intendedSwatchEl || el
this.intendedSwatchIndex = null
this.intendedSwatchEl = null
const next = product.siblings[idx]
if (next) product = next
this.centerSwatch(targetEl)
},
scroll(direction = 'forward') {
const swatch = $data.wrapper.querySelector('.product-item__swatch')
const unit = swatch ? swatch.offsetWidth + $data.swatchGap : $data.wrapper.clientWidth * 0.3
let amount = 2 * unit
$data.wrapper.scrollTo({
left: direction == 'forward' ? $data.wrapper.scrollLeft + amount : $data.wrapper.scrollLeft - amount,
behavior:'smooth'
})
},
// Lazily wire the prev/next edge detection on first desktop hover/focus —
// the arrows are a desktop affordance, so no observer is built at load and
// none is ever built on mobile. canPrev/canNext default to the at-start state.
initSwatchEdges() {
if (this._edgeReady || window.innerWidth <= 1023) return
this._edgeReady = true
const root = this.wrapper
const swatches = root.querySelectorAll('.product-item__swatch')
if (swatches.length < 2) return
const first = swatches[0], last = swatches[swatches.length - 1]
const io = new IntersectionObserver((entries) => {
const d = window.Alpine && window.Alpine.$data ? window.Alpine.$data(root) : null
if (!d) return
entries.forEach((e) => {
if (e.target === first) d.canPrev = !e.isIntersecting
if (e.target === last) d.canNext = !e.isIntersecting
})
}, { root, threshold: 0.99 })
io.observe(first)
io.observe(last)
root._swatchEdgeIO = io
}
}"
@pointerenter="initSwatchEdges()"
@focusin="initSwatchEdges()"
>
Explore Similar Stories
{ $dispatch('product-item:images-changed', { product: product, source: 'swatch-container' }) })"
@pointercancel="intendedSwatchIndex = null; intendedSwatchEl = null"
x-data="{
wrapper: $el,
selectedSibling:product.siblings[0],
intendedSwatchIndex: null,
intendedSwatchEl: null,
swatchGap: 8,
scrollable: product.siblings.filter(s => !!s.available).length > 9.5,
canPrev: false,
canNext: true,
_edgeReady: false,
imageLimit: 0,
limitImages(nextProduct) {
const images = Array.isArray(nextProduct && nextProduct.images) ? nextProduct.images : [];
const limitedImages = this.imageLimit > 0 ? images.slice(0, this.imageLimit) : images;
const hoverImage = limitedImages[1] || null;
return Object.assign({}, nextProduct, {
images: limitedImages,
hover_image: hoverImage ? hoverImage.src : null,
hover_image_alt: hoverImage ? hoverImage.alt : null
});
},
selectProduct(nextProduct) {
const productRoot = $el.closest('.product-item');
const productData = productRoot && productRoot._x_dataStack && productRoot._x_dataStack[0];
if (productData && typeof productData.setProduct === 'function') {
productData.setProduct(nextProduct);
} else {
$data.product = this.limitImages(nextProduct);
}
},
getSwatchImageSrc(sibling) {
return sibling.images[0].src;
},
buildSwatchSrcset(src) {
if (!src) return '';
const widths = ['48', '96', '192'];
const separator = src.includes('?') ? '&' : '?';
return widths.map(w => `${src}${separator}width=${w} ${w}w`).join(', ');
},
isSwatchPriority(index) {
return window.innerWidth >= 1024 ? index < 3 : index < 2;
},
swatchBg(sibling) {
// swatch_bg precomputed at hydration. Nullish (not ||): null = no colors set
// (fall back to theme default), empty string = invalid combo, no fallback.
return sibling.swatch_bg ?? '#000000';
},
centerSwatch(el) {
if ($data.wrapper.scrollWidth <= $data.wrapper.clientWidth) return false
const wrapperRect = $data.wrapper.getBoundingClientRect()
const elRect = el.getBoundingClientRect()
const centerPoint = wrapperRect.x + (wrapperRect.width / 2)
const behavior = window.innerWidth < 1024 ? 'auto' : 'smooth'
$data.wrapper.scrollTo({
left: $data.wrapper.scrollLeft + (elRect.x - centerPoint + (elRect.width / 2)),
behavior
})
},
selectSwatchFromTap(siblingIndex, el) {
const idx = (this.intendedSwatchIndex !== null && this.intendedSwatchIndex !== undefined)
? this.intendedSwatchIndex
: siblingIndex
const targetEl = this.intendedSwatchEl || el
this.intendedSwatchIndex = null
this.intendedSwatchEl = null
const next = product.siblings[idx]
if (next) product = next
this.centerSwatch(targetEl)
},
scroll(direction = 'forward') {
const swatch = $data.wrapper.querySelector('.product-item__swatch')
const unit = swatch ? swatch.offsetWidth + $data.swatchGap : $data.wrapper.clientWidth * 0.3
let amount = 2 * unit
$data.wrapper.scrollTo({
left: direction == 'forward' ? $data.wrapper.scrollLeft + amount : $data.wrapper.scrollLeft - amount,
behavior:'smooth'
})
},
// Lazily wire the prev/next edge detection on first desktop hover/focus —
// the arrows are a desktop affordance, so no observer is built at load and
// none is ever built on mobile. canPrev/canNext default to the at-start state.
initSwatchEdges() {
if (this._edgeReady || window.innerWidth <= 1023) return
this._edgeReady = true
const root = this.wrapper
const swatches = root.querySelectorAll('.product-item__swatch')
if (swatches.length < 2) return
const first = swatches[0], last = swatches[swatches.length - 1]
const io = new IntersectionObserver((entries) => {
const d = window.Alpine && window.Alpine.$data ? window.Alpine.$data(root) : null
if (!d) return
entries.forEach((e) => {
if (e.target === first) d.canPrev = !e.isIntersecting
if (e.target === last) d.canNext = !e.isIntersecting
})
}, { root, threshold: 0.99 })
io.observe(first)
io.observe(last)
root._swatchEdgeIO = io
}
}"
@pointerenter="initSwatchEdges()"
@focusin="initSwatchEdges()"
>