@pradip1995/theme-sahsha 3.1.1 → 3.1.3

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": "@pradip1995/theme-sahsha",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "description": "Sahsha storefront theme — Impulse-based editorial layout, luxury nav, and brand tokens",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -32,9 +32,16 @@ const CollectionsShowcaseClient = ({
32
32
  }: CollectionsShowcaseClientProps) => {
33
33
  const viewportRef = useRef<HTMLDivElement>(null)
34
34
  const [activeId, setActiveId] = useState(() => getDefaultActiveTabId(tabs))
35
+ const [contentAnimating, setContentAnimating] = useState(false)
35
36
 
36
37
  const activeProducts = productsByCollection[activeId] ?? []
37
38
 
39
+ const handleTabClick = (tabId: string) => {
40
+ if (tabId === activeId) return
41
+ setActiveId(tabId)
42
+ setContentAnimating(true)
43
+ }
44
+
38
45
  useEffect(() => {
39
46
  const viewport = viewportRef.current
40
47
  if (viewport) {
@@ -42,6 +49,13 @@ const CollectionsShowcaseClient = ({
42
49
  }
43
50
  }, [activeId])
44
51
 
52
+ useEffect(() => {
53
+ if (!contentAnimating) return
54
+
55
+ const timer = window.setTimeout(() => setContentAnimating(false), 360)
56
+ return () => window.clearTimeout(timer)
57
+ }, [contentAnimating, activeId])
58
+
45
59
  const scrollByPage = (direction: "prev" | "next") => {
46
60
  const viewport = viewportRef.current
47
61
  if (!viewport) return
@@ -69,9 +83,9 @@ const CollectionsShowcaseClient = ({
69
83
  type="button"
70
84
  className={`collections-showcase__tab${isActive ? " is-active" : ""}`}
71
85
  aria-current={isActive ? "true" : undefined}
72
- onClick={() => setActiveId(tab.id)}
86
+ onClick={() => handleTabClick(tab.id)}
73
87
  >
74
- {tab.title}
88
+ <span className="collections-showcase__tab-label">{tab.title}</span>
75
89
  </button>
76
90
  )
77
91
  })}
@@ -88,7 +102,12 @@ const CollectionsShowcaseClient = ({
88
102
  </button>
89
103
 
90
104
  <div ref={viewportRef} className="collections-showcase__viewport">
91
- <div className="collections-showcase__track">
105
+ <div
106
+ key={activeId}
107
+ className={`collections-showcase__track${
108
+ contentAnimating ? " collections-showcase__track--enter" : ""
109
+ }`}
110
+ >
92
111
  {activeProducts.length > 0 ? (
93
112
  activeProducts.map((product) => (
94
113
  <div key={product.id} className="collections-showcase__slide">
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import { useRef } from "react"
3
+ import { useCallback, useEffect, useRef, useState } from "react"
4
4
  import { ChevronLeft, ChevronRight } from "lucide-react"
5
5
  import { HttpTypes } from "@medusajs/types"
6
6
  import type { ProductCardRating } from "@core/types/product-card"
@@ -12,36 +12,160 @@ type ProductCarouselProps = {
12
12
  ratings?: ProductCardRating[]
13
13
  }
14
14
 
15
+ const AUTO_SCROLL_MS = 4200
16
+ const AUTO_SCROLL_RESUME_MS = 5000
17
+
15
18
  const ProductCarousel = ({ products, region, ratings }: ProductCarouselProps) => {
16
19
  const viewportRef = useRef<HTMLDivElement>(null)
17
20
  const trackRef = useRef<HTMLDivElement>(null)
21
+ const pauseAutoScrollRef = useRef(false)
22
+ const resumeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
23
+ const [canScrollLeft, setCanScrollLeft] = useState(false)
24
+ const [canScrollRight, setCanScrollRight] = useState(true)
25
+ const [reduceMotion, setReduceMotion] = useState(false)
18
26
 
19
- const scrollBySlide = (direction: "prev" | "next") => {
27
+ const getSlideStep = useCallback(() => {
20
28
  const viewport = viewportRef.current
21
29
  const track = trackRef.current
22
- if (!viewport || !track) return
30
+ if (!viewport || !track) return 0
23
31
 
24
32
  const firstSlide = track.querySelector<HTMLElement>(".bestsellers-carousel__slide")
25
- if (!firstSlide) return
33
+ if (!firstSlide) return viewport.clientWidth * 0.85
26
34
 
27
35
  const trackStyles = getComputedStyle(track)
28
36
  const gap =
29
37
  Number.parseFloat(trackStyles.columnGap || trackStyles.gap || "0") || 0
30
- const slideStep = firstSlide.offsetWidth + gap
31
38
 
32
- viewport.scrollBy({
33
- left: direction === "next" ? slideStep : -slideStep,
34
- behavior: "smooth",
35
- })
36
- }
39
+ return firstSlide.offsetWidth + gap
40
+ }, [])
41
+
42
+ const updateScrollState = useCallback(() => {
43
+ const viewport = viewportRef.current
44
+ if (!viewport) return
45
+
46
+ const { scrollLeft, scrollWidth, clientWidth } = viewport
47
+ const maxScroll = scrollWidth - clientWidth
48
+
49
+ setCanScrollLeft(scrollLeft > 4)
50
+ setCanScrollRight(scrollLeft < maxScroll - 4)
51
+ }, [])
52
+
53
+ const pauseAutoScroll = useCallback(() => {
54
+ pauseAutoScrollRef.current = true
55
+
56
+ if (resumeTimerRef.current) {
57
+ clearTimeout(resumeTimerRef.current)
58
+ }
59
+
60
+ resumeTimerRef.current = setTimeout(() => {
61
+ pauseAutoScrollRef.current = false
62
+ }, AUTO_SCROLL_RESUME_MS)
63
+ }, [])
64
+
65
+ const scrollBySlide = useCallback(
66
+ (direction: "prev" | "next") => {
67
+ const viewport = viewportRef.current
68
+ if (!viewport) return
69
+
70
+ pauseAutoScroll()
71
+
72
+ const slideStep = getSlideStep()
73
+ viewport.scrollBy({
74
+ left: direction === "next" ? slideStep : -slideStep,
75
+ behavior: "smooth",
76
+ })
77
+ },
78
+ [getSlideStep, pauseAutoScroll]
79
+ )
80
+
81
+ useEffect(() => {
82
+ const media = window.matchMedia("(prefers-reduced-motion: reduce)")
83
+ const update = () => setReduceMotion(media.matches)
84
+ update()
85
+ media.addEventListener("change", update)
86
+ return () => media.removeEventListener("change", update)
87
+ }, [])
88
+
89
+ useEffect(() => {
90
+ const viewport = viewportRef.current
91
+ if (!viewport) return
92
+
93
+ updateScrollState()
94
+
95
+ const onScroll = () => updateScrollState()
96
+ viewport.addEventListener("scroll", onScroll, { passive: true })
97
+ window.addEventListener("resize", updateScrollState)
98
+
99
+ return () => {
100
+ viewport.removeEventListener("scroll", onScroll)
101
+ window.removeEventListener("resize", updateScrollState)
102
+ }
103
+ }, [products, updateScrollState])
104
+
105
+ useEffect(() => {
106
+ if (reduceMotion || products.length < 3) return
107
+
108
+ const viewport = viewportRef.current
109
+ if (!viewport) return
110
+
111
+ const tick = () => {
112
+ if (pauseAutoScrollRef.current) return
113
+
114
+ const { scrollLeft, scrollWidth, clientWidth } = viewport
115
+ const maxScroll = scrollWidth - clientWidth
116
+ const step = getSlideStep()
117
+
118
+ if (maxScroll <= 0) return
119
+
120
+ if (scrollLeft >= maxScroll - 4) {
121
+ viewport.scrollTo({ left: 0, behavior: "smooth" })
122
+ return
123
+ }
124
+
125
+ viewport.scrollBy({ left: step, behavior: "smooth" })
126
+ }
127
+
128
+ const intervalId = window.setInterval(tick, AUTO_SCROLL_MS)
129
+
130
+ return () => window.clearInterval(intervalId)
131
+ }, [reduceMotion, products, getSlideStep])
132
+
133
+ useEffect(() => {
134
+ return () => {
135
+ if (resumeTimerRef.current) {
136
+ clearTimeout(resumeTimerRef.current)
137
+ }
138
+ }
139
+ }, [])
37
140
 
38
141
  return (
39
- <div className="bestsellers-carousel">
142
+ <div
143
+ className="bestsellers-carousel"
144
+ onMouseEnter={pauseAutoScroll}
145
+ onTouchStart={pauseAutoScroll}
146
+ onPointerDown={pauseAutoScroll}
147
+ >
148
+ <div
149
+ className={`bestsellers-carousel__fade bestsellers-carousel__fade--left${
150
+ canScrollLeft ? " is-visible" : ""
151
+ }`}
152
+ aria-hidden
153
+ />
154
+ <div
155
+ className={`bestsellers-carousel__fade bestsellers-carousel__fade--right${
156
+ canScrollRight ? " is-visible" : ""
157
+ }`}
158
+ aria-hidden
159
+ />
160
+
40
161
  <button
41
162
  type="button"
42
- className="bestsellers-carousel__nav bestsellers-carousel__nav--prev"
163
+ className={`bestsellers-carousel__nav bestsellers-carousel__nav--prev${
164
+ canScrollLeft ? "" : " is-hidden"
165
+ }`}
43
166
  onClick={() => scrollBySlide("prev")}
44
167
  aria-label="Previous products"
168
+ tabIndex={canScrollLeft ? 0 : -1}
45
169
  >
46
170
  <ChevronLeft size={20} strokeWidth={1.75} />
47
171
  </button>
@@ -66,9 +190,12 @@ const ProductCarousel = ({ products, region, ratings }: ProductCarouselProps) =>
66
190
 
67
191
  <button
68
192
  type="button"
69
- className="bestsellers-carousel__nav bestsellers-carousel__nav--next"
193
+ className={`bestsellers-carousel__nav bestsellers-carousel__nav--next${
194
+ canScrollRight ? "" : " is-hidden"
195
+ }`}
70
196
  onClick={() => scrollBySlide("next")}
71
197
  aria-label="Next products"
198
+ tabIndex={canScrollRight ? 0 : -1}
72
199
  >
73
200
  <ChevronRight size={20} strokeWidth={1.75} />
74
201
  </button>
@@ -99,13 +99,13 @@ export function ProductCTASection(props: ProductCTASectionProps) {
99
99
  </p>
100
100
  )}
101
101
 
102
- <div className="flex items-stretch gap-3 w-full">
102
+ <div className="product-cta-row flex items-stretch gap-2 sm:gap-3 w-full flex-nowrap">
103
103
  {variantInCart ? (
104
104
  <div className="flex h-12 shrink-0 w-[120px] border border-brand-accent rounded-full overflow-hidden">
105
105
  <button
106
106
  onClick={handleDecreaseQuantity}
107
107
  disabled={isAdding}
108
- className="w-10 flex items-center justify-center hover:bg-surface-muted transition-colors disabled:opacity-50"
108
+ className="w-10 flex items-center justify-center hover:bg-surface-muted disabled:opacity-50"
109
109
  aria-label="Decrease quantity"
110
110
  >
111
111
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -118,7 +118,7 @@ export function ProductCTASection(props: ProductCTASectionProps) {
118
118
  <button
119
119
  onClick={handleIncreaseQuantity}
120
120
  disabled={isAdding || !inStock}
121
- className="w-10 flex items-center justify-center hover:bg-surface-muted transition-colors disabled:opacity-50"
121
+ className="w-10 flex items-center justify-center hover:bg-surface-muted disabled:opacity-50"
122
122
  aria-label="Increase quantity"
123
123
  >
124
124
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -170,63 +170,50 @@ export function ProductCTASection(props: ProductCTASectionProps) {
170
170
  data-ga-event="add_to_bag_click"
171
171
  data-ga-label={product.title || "Product"}
172
172
  >
173
- {isAdding ? (
174
- <span className="product-cta-btn__label">
175
- <Spinner />
176
- </span>
177
- ) : (
178
- <span className="product-cta-btn__label">
179
- {!selectedVariant || !isValidVariant
180
- ? product.options?.every((opt) => options[opt.id])
181
- ? "Unavailable"
182
- : "Select options"
183
- : !inStock
184
- ? inventoryLimit !== null &&
185
- inventoryLimit + quantityInCart > 0 &&
186
- quantity > inventoryLimit
187
- ? inventoryLimit <= 0
188
- ? "Sold out"
189
- : "Max quantity reached"
190
- : "Sold out"
191
- : `Add to Cart${quantity > 1 ? ` (${quantity})` : ""}`}
192
- </span>
193
- )}
173
+ <span className="product-cta-btn__label whitespace-nowrap">
174
+ {isAdding ? (
175
+ "Adding..."
176
+ ) : !selectedVariant || !isValidVariant ? (
177
+ product.options?.every((opt) => options[opt.id]) ? (
178
+ "Unavailable"
179
+ ) : (
180
+ "Select options"
181
+ )
182
+ ) : !inStock ? (
183
+ inventoryLimit !== null &&
184
+ inventoryLimit + quantityInCart > 0 &&
185
+ quantity > inventoryLimit ? (
186
+ inventoryLimit <= 0 ? (
187
+ "Sold out"
188
+ ) : (
189
+ "Max quantity"
190
+ )
191
+ ) : (
192
+ "Sold out"
193
+ )
194
+ ) : (
195
+ `Add to Cart${quantity > 1 ? ` (${quantity})` : ""}`
196
+ )}
197
+ </span>
194
198
  </button>
195
199
  )}
196
- </div>
197
200
 
198
- <button
199
- onClick={handleBuyNow}
200
- disabled={
201
- !inStock ||
202
- !!disabled ||
203
- isAdding ||
204
- isBuyingNow ||
205
- (!isValidVariant && product.options?.every((opt) => options[opt.id]))
206
- }
207
- className={`${btnBase} w-full product-cta-btn--primary`}
208
- >
209
- {isBuyingNow ? (
210
- <span className="product-cta-btn__label">
211
- <Spinner />
201
+ <button
202
+ onClick={handleBuyNow}
203
+ disabled={
204
+ !inStock ||
205
+ !!disabled ||
206
+ isAdding ||
207
+ isBuyingNow ||
208
+ (!isValidVariant && product.options?.every((opt) => options[opt.id]))
209
+ }
210
+ className={`${btnBase} flex-1 min-w-0 product-cta-btn--primary`}
211
+ >
212
+ <span className="product-cta-btn__label whitespace-nowrap">
213
+ {isBuyingNow ? "Processing..." : "Buy It Now"}
212
214
  </span>
213
- ) : (
214
- <span className="product-cta-btn__label">Buy It Now</span>
215
- )}
216
- </button>
215
+ </button>
216
+ </div>
217
217
  </div>
218
218
  )
219
219
  }
220
-
221
- function Spinner() {
222
- return (
223
- <svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
224
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
225
- <path
226
- className="opacity-75"
227
- fill="currentColor"
228
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
229
- />
230
- </svg>
231
- )
232
- }
@@ -78,8 +78,8 @@ export function ProductDetailsSection({ product }: { product: HttpTypes.StorePro
78
78
  careGuide !== undefined &&
79
79
  String(careGuide).trim() !== ""
80
80
  const showProductDetails = hasProductDetailsContent(product)
81
- const [isDetailsOpen, setIsDetailsOpen] = useState(false)
82
- const [isDescriptionOpen, setIsDescriptionOpen] = useState(false)
81
+ const [isDetailsOpen, setIsDetailsOpen] = useState(true)
82
+ const [isDescriptionOpen, setIsDescriptionOpen] = useState(true)
83
83
  const [isCareGuideOpen, setIsCareGuideOpen] = useState(false)
84
84
 
85
85
  return (
@@ -6,6 +6,7 @@ import LocalizedClientLink from "@modules/common/components/localized-client-lin
6
6
  import { getProductPrice } from "@core/util/get-product-price"
7
7
  import { convertToLocale } from "@core/util/money"
8
8
  import PlaceholderImage from "@modules/common/icons/placeholder-image"
9
+ import { productCardImageProps } from "@lib/product-image"
9
10
  import ProductCardRatingDisplay from "@modules/common/components/product/product-card-rating"
10
11
  import WishlistIcon from "@modules/common/components/product/wishlist-icon"
11
12
  import {
@@ -128,10 +129,10 @@ export default function ProductCard({
128
129
  {thumbnail ? (
129
130
  <>
130
131
  <Image
132
+ {...productCardImageProps}
131
133
  src={thumbnail}
132
134
  alt={product.title || "Product"}
133
135
  fill
134
- sizes="(max-width:640px) 50vw, (max-width:1024px) 33vw, 25vw"
135
136
  className={clx(
136
137
  "object-cover transition-opacity duration-500",
137
138
  imageHover && secondImage ? "opacity-0" : "opacity-100"
@@ -139,10 +140,10 @@ export default function ProductCard({
139
140
  />
140
141
  {secondImage && (
141
142
  <Image
143
+ {...productCardImageProps}
142
144
  src={secondImage}
143
145
  alt=""
144
146
  fill
145
- sizes="(max-width:640px) 50vw, (max-width:1024px) 33vw, 25vw"
146
147
  className={clx(
147
148
  "object-cover transition-opacity duration-500",
148
149
  imageHover ? "opacity-100" : "opacity-0"
@@ -2583,9 +2583,21 @@ body.mobile-menu-open .promo-bar {
2583
2583
  }
2584
2584
  }
2585
2585
 
2586
+ @keyframes collections-showcase-tab-enter {
2587
+ from {
2588
+ opacity: 0;
2589
+ transform: translateY(10px);
2590
+ }
2591
+
2592
+ to {
2593
+ opacity: 1;
2594
+ transform: translateY(0);
2595
+ }
2596
+ }
2597
+
2586
2598
  .collections-showcase__tab {
2587
2599
  margin: 0;
2588
- padding: 0;
2600
+ padding: 0 0 0.4rem;
2589
2601
  border: none;
2590
2602
  background: none;
2591
2603
  cursor: pointer;
@@ -2597,22 +2609,83 @@ body.mobile-menu-open .promo-bar {
2597
2609
  line-height: 1.1;
2598
2610
  letter-spacing: 0.01em;
2599
2611
  color: #c8c8c8;
2600
- transition: color 0.25s ease, font-weight 0.25s ease;
2612
+ position: relative;
2613
+ transition:
2614
+ color 0.28s ease,
2615
+ transform 0.2s cubic-bezier(0.34, 1.4, 0.64, 1);
2601
2616
  -webkit-tap-highlight-color: transparent;
2602
2617
  outline: none;
2603
2618
  box-shadow: none;
2604
2619
  }
2605
2620
 
2621
+ .collections-showcase__tab-label {
2622
+ display: inline-block;
2623
+ transition: transform 0.2s cubic-bezier(0.34, 1.4, 0.64, 1);
2624
+ }
2625
+
2626
+ .collections-showcase__tab::after {
2627
+ content: "";
2628
+ position: absolute;
2629
+ left: 0;
2630
+ right: 0;
2631
+ bottom: 0;
2632
+ height: 2px;
2633
+ border-radius: 999px;
2634
+ background: currentColor;
2635
+ transform: scaleX(0);
2636
+ transform-origin: center;
2637
+ transition: transform 0.32s cubic-bezier(0.34, 1.2, 0.64, 1);
2638
+ }
2639
+
2606
2640
  .collections-showcase__tab.is-active {
2607
2641
  color: #1a1a1a;
2608
2642
  font-weight: 500;
2609
2643
  }
2610
2644
 
2645
+ .collections-showcase__tab.is-active::after {
2646
+ transform: scaleX(1);
2647
+ }
2648
+
2611
2649
  .collections-showcase__tab:hover:not(.is-active),
2612
2650
  .collections-showcase__tab:focus-visible:not(.is-active) {
2613
2651
  color: #9a9a9a;
2614
2652
  }
2615
2653
 
2654
+ .collections-showcase__tab:hover .collections-showcase__tab-label,
2655
+ .collections-showcase__tab:focus-visible .collections-showcase__tab-label {
2656
+ transform: translateY(-1px);
2657
+ }
2658
+
2659
+ .collections-showcase__tab:active .collections-showcase__tab-label {
2660
+ transform: scale(0.96);
2661
+ }
2662
+
2663
+ .collections-showcase__tab.is-active:active .collections-showcase__tab-label {
2664
+ transform: scale(0.98);
2665
+ }
2666
+
2667
+ .collections-showcase__track--enter {
2668
+ animation: collections-showcase-tab-enter 0.36s cubic-bezier(0.22, 1, 0.36, 1) both;
2669
+ }
2670
+
2671
+ @media (prefers-reduced-motion: reduce) {
2672
+ .collections-showcase__tab,
2673
+ .collections-showcase__tab-label,
2674
+ .collections-showcase__tab::after {
2675
+ transition: none;
2676
+ }
2677
+
2678
+ .collections-showcase__tab:hover .collections-showcase__tab-label,
2679
+ .collections-showcase__tab:focus-visible .collections-showcase__tab-label,
2680
+ .collections-showcase__tab:active .collections-showcase__tab-label {
2681
+ transform: none;
2682
+ }
2683
+
2684
+ .collections-showcase__track--enter {
2685
+ animation: none;
2686
+ }
2687
+ }
2688
+
2616
2689
  .collections-showcase__tab:focus,
2617
2690
  .collections-showcase__tab:focus-visible {
2618
2691
  outline: none;
@@ -3038,7 +3111,10 @@ body.mobile-menu-open .promo-bar {
3038
3111
  overflow: hidden;
3039
3112
  background: var(--color-brand-cream);
3040
3113
  box-shadow: 0 4px 20px rgba(90, 42, 67, 0.07);
3041
- transition: box-shadow 0.4s ease, transform 0.4s ease;
3114
+ transform-origin: center;
3115
+ transition:
3116
+ box-shadow 0.35s ease,
3117
+ transform 0.28s cubic-bezier(0.34, 1.2, 0.64, 1);
3042
3118
  }
3043
3119
 
3044
3120
  .shop-by-category__card--top .shop-by-category__frame,
@@ -3048,7 +3124,13 @@ body.mobile-menu-open .promo-bar {
3048
3124
 
3049
3125
  .shop-by-category__card:hover .shop-by-category__frame {
3050
3126
  box-shadow: 0 12px 36px rgba(90, 42, 67, 0.15);
3051
- transform: translateY(-3px);
3127
+ transform: translateY(-3px) scale(1.02);
3128
+ }
3129
+
3130
+ .shop-by-category__card:active .shop-by-category__frame {
3131
+ box-shadow: 0 6px 24px rgba(90, 42, 67, 0.12);
3132
+ transform: translateY(-1px) scale(0.98);
3133
+ transition-duration: 0.15s;
3052
3134
  }
3053
3135
 
3054
3136
  .shop-by-category__image {
@@ -3219,6 +3301,25 @@ body.mobile-menu-open .promo-bar {
3219
3301
  }
3220
3302
  }
3221
3303
 
3304
+ @media (prefers-reduced-motion: reduce) {
3305
+ .shop-by-category__frame,
3306
+ .shop-by-category__image,
3307
+ .shop-by-category__overlay,
3308
+ .shop-by-category__count,
3309
+ .shop-by-category__cta {
3310
+ transition: none;
3311
+ }
3312
+
3313
+ .shop-by-category__card:hover .shop-by-category__frame,
3314
+ .shop-by-category__card:active .shop-by-category__frame {
3315
+ transform: none;
3316
+ }
3317
+
3318
+ .shop-by-category__card:hover .shop-by-category__image {
3319
+ transform: none;
3320
+ }
3321
+ }
3322
+
3222
3323
  /* ── Account login / register (Sahsha split layout) ──────── */
3223
3324
  .account-login {
3224
3325
  display: flex;
@@ -5215,22 +5316,31 @@ body.mobile-menu-open .promo-bar {
5215
5316
  align-items: flex-start;
5216
5317
  }
5217
5318
 
5218
- #product-gallery-container {
5319
+ #product-gallery-container,
5320
+ .product-images-sticky {
5219
5321
  position: sticky !important;
5220
- top: calc(var(--header-height) + var(--promo-bar-height, 0px) + 1rem) !important;
5221
- max-height: calc(100vh - var(--header-height) - var(--promo-bar-height, 0px) - 2rem);
5222
- overflow: hidden;
5223
- z-index: 1;
5322
+ top: calc(var(--header-height) + var(--promo-bar-height, 0px) + 20px) !important;
5323
+ align-self: flex-start !important;
5324
+ width: 100% !important;
5325
+ min-width: 0 !important;
5326
+ height: fit-content !important;
5327
+ max-height: calc(100vh - var(--header-height) - var(--promo-bar-height, 0px) - 40px) !important;
5328
+ overflow: visible !important;
5329
+ z-index: 2;
5224
5330
  }
5225
5331
 
5226
- .product-details-column {
5227
- max-height: calc(100vh - var(--header-height) - var(--promo-bar-height, 0px) - 2rem);
5228
- overflow-x: hidden;
5229
- overflow-y: auto;
5230
- overscroll-behavior: contain;
5231
- -ms-overflow-style: none;
5232
- scrollbar-width: none;
5233
- padding-bottom: 1.5rem;
5332
+ .product-details-column,
5333
+ .product-details-scroll {
5334
+ flex: 1;
5335
+ min-width: 0;
5336
+ width: 100% !important;
5337
+ height: auto !important;
5338
+ max-height: none !important;
5339
+ overflow: visible !important;
5340
+ overflow-x: visible !important;
5341
+ overflow-y: visible !important;
5342
+ overscroll-behavior: auto !important;
5343
+ padding-bottom: 0 !important;
5234
5344
  }
5235
5345
 
5236
5346
  .product-details-column::-webkit-scrollbar {
@@ -5238,12 +5348,21 @@ body.mobile-menu-open .promo-bar {
5238
5348
  }
5239
5349
 
5240
5350
  .product-gallery__stage-img {
5241
- max-height: calc(100vh - var(--header-height) - var(--promo-bar-height, 0px) - 4rem);
5351
+ max-height: min(
5352
+ calc(100vh - var(--header-height) - var(--promo-bar-height, 0px) - 5rem),
5353
+ 680px
5354
+ );
5242
5355
  }
5243
5356
 
5244
5357
  .product-gallery__thumbs--scrollable {
5245
- height: calc(100vh - var(--header-height) - var(--promo-bar-height, 0px) - 4rem);
5246
- max-height: calc(100vh - var(--header-height) - var(--promo-bar-height, 0px) - 4rem);
5358
+ height: min(
5359
+ calc(100vh - var(--header-height) - var(--promo-bar-height, 0px) - 5rem),
5360
+ 680px
5361
+ );
5362
+ max-height: min(
5363
+ calc(100vh - var(--header-height) - var(--promo-bar-height, 0px) - 5rem),
5364
+ 680px
5365
+ );
5247
5366
  }
5248
5367
  }
5249
5368
 
@@ -5275,15 +5394,18 @@ body.mobile-menu-open .promo-bar {
5275
5394
  @media (min-width: 1024px) {
5276
5395
  .product-gallery {
5277
5396
  display: grid;
5278
- grid-template-columns: 5.25rem minmax(0, 1fr);
5397
+ grid-template-columns: 5.25rem auto;
5279
5398
  gap: 0.375rem;
5280
5399
  align-items: start;
5400
+ width: fit-content;
5401
+ max-width: 100%;
5281
5402
  background: #ffffff;
5282
5403
  }
5283
5404
 
5284
5405
  .product-gallery--single {
5285
5406
  display: block;
5286
- grid-template-columns: 1fr;
5407
+ width: fit-content;
5408
+ max-width: 100%;
5287
5409
  }
5288
5410
 
5289
5411
  .product-gallery__stage {
@@ -5308,8 +5430,14 @@ body.mobile-menu-open .promo-bar {
5308
5430
  }
5309
5431
 
5310
5432
  .product-gallery__thumbs--scrollable {
5311
- height: min(33.125rem, 95vh, 1200px);
5312
- max-height: min(33.125rem, 95vh, 1200px);
5433
+ height: min(
5434
+ calc(100vh - var(--header-height) - var(--promo-bar-height, 0px) - 5rem),
5435
+ 680px
5436
+ );
5437
+ max-height: min(
5438
+ calc(100vh - var(--header-height) - var(--promo-bar-height, 0px) - 5rem),
5439
+ 680px
5440
+ );
5313
5441
  }
5314
5442
 
5315
5443
  .product-gallery__thumbs--static {
@@ -5359,16 +5487,18 @@ body.mobile-menu-open .promo-bar {
5359
5487
  display: flex;
5360
5488
  align-items: flex-start;
5361
5489
  justify-content: center;
5362
- width: 100%;
5490
+ width: fit-content;
5491
+ max-width: 100%;
5363
5492
  min-height: 0;
5364
5493
  overflow: visible;
5365
5494
  }
5366
5495
 
5367
5496
  .product-gallery__stage-img {
5368
5497
  display: block;
5369
- width: 100%;
5498
+ width: auto;
5499
+ max-width: 100%;
5370
5500
  height: auto !important;
5371
- max-height: min(95vh, 1200px);
5501
+ max-height: min(70vh, 640px);
5372
5502
  object-fit: contain !important;
5373
5503
  object-position: center top;
5374
5504
  }
@@ -5912,6 +6042,45 @@ body.mobile-menu-open .promo-bar {
5912
6042
  color: #1a1a1a;
5913
6043
  }
5914
6044
 
6045
+ /* Single-line CTA row: qty + Add to Cart + Buy It Now (no slide animation) */
6046
+ .product-cta-row,
6047
+ .product-cta-row > .product-cta-btn {
6048
+ flex-wrap: nowrap;
6049
+ }
6050
+
6051
+ .product-cta-btn::before {
6052
+ content: none !important;
6053
+ display: none !important;
6054
+ }
6055
+
6056
+ .product-cta-btn {
6057
+ overflow: visible;
6058
+ }
6059
+
6060
+ .product-cta-btn__label {
6061
+ transition: none;
6062
+ }
6063
+
6064
+ .product-cta-btn--primary:hover:not(:disabled) {
6065
+ background: #4a2236 !important;
6066
+ color: #ffffff !important;
6067
+ border-color: #4a2236 !important;
6068
+ }
6069
+
6070
+ .product-cta-btn--primary:hover:not(:disabled) .product-cta-btn__label {
6071
+ color: #ffffff !important;
6072
+ }
6073
+
6074
+ .product-cta-btn--secondary:hover:not(:disabled) {
6075
+ background: #5a2a43 !important;
6076
+ color: #ffffff !important;
6077
+ border-color: #5a2a43 !important;
6078
+ }
6079
+
6080
+ .product-cta-btn--secondary:hover:not(:disabled) .product-cta-btn__label {
6081
+ color: #ffffff !important;
6082
+ }
6083
+
5915
6084
  .product-details__spec-heading {
5916
6085
  margin: 1.25rem 0 0.85rem;
5917
6086
  font-family: var(--font-sans);
@@ -10977,17 +11146,77 @@ body.checkout-modal-open nav {
10977
11146
  .bestsellers-carousel {
10978
11147
  position: relative;
10979
11148
  container-type: inline-size;
10980
- overflow: hidden;
11149
+ }
11150
+
11151
+ .new-arrivals .bestsellers-carousel,
11152
+ .bestsellers .bestsellers-carousel {
11153
+ margin-inline: -0.5rem;
11154
+ padding-inline: 0.5rem;
11155
+ }
11156
+
11157
+ @media (min-width: 640px) {
11158
+ .new-arrivals .bestsellers-carousel,
11159
+ .bestsellers .bestsellers-carousel {
11160
+ margin-inline: -0.75rem;
11161
+ padding-inline: 0.75rem;
11162
+ }
11163
+ }
11164
+
11165
+ @media (min-width: 1024px) {
11166
+ .new-arrivals .bestsellers-carousel,
11167
+ .bestsellers .bestsellers-carousel {
11168
+ margin-inline: -1rem;
11169
+ padding-inline: 1rem;
11170
+ }
11171
+ }
11172
+
11173
+ .bestsellers-carousel__fade {
11174
+ position: absolute;
11175
+ top: 0;
11176
+ bottom: 0.25rem;
11177
+ z-index: 1;
11178
+ width: 2.5rem;
11179
+ pointer-events: none;
11180
+ opacity: 0;
11181
+ transition: opacity 0.3s ease;
11182
+ }
11183
+
11184
+ .bestsellers-carousel__fade.is-visible {
11185
+ opacity: 1;
11186
+ }
11187
+
11188
+ .bestsellers-carousel__fade--left {
11189
+ left: 0;
11190
+ background: linear-gradient(
11191
+ to right,
11192
+ var(--color-page-bg, #faf9f7) 0%,
11193
+ transparent 100%
11194
+ );
11195
+ }
11196
+
11197
+ .bestsellers-carousel__fade--right {
11198
+ right: 0;
11199
+ background: linear-gradient(
11200
+ to left,
11201
+ var(--color-page-bg, #faf9f7) 0%,
11202
+ transparent 100%
11203
+ );
10981
11204
  }
10982
11205
 
10983
11206
  .bestsellers-carousel__viewport {
10984
11207
  overflow-x: auto;
10985
11208
  overflow-y: hidden;
10986
- scroll-snap-type: x mandatory;
11209
+ scroll-snap-type: x proximity;
10987
11210
  scroll-behavior: smooth;
11211
+ scroll-padding-inline: 0.5rem;
10988
11212
  -webkit-overflow-scrolling: touch;
10989
11213
  scrollbar-width: none;
10990
11214
  overscroll-behavior-x: contain;
11215
+ cursor: grab;
11216
+ }
11217
+
11218
+ .bestsellers-carousel__viewport:active {
11219
+ cursor: grabbing;
10991
11220
  }
10992
11221
 
10993
11222
  .bestsellers-carousel__viewport::-webkit-scrollbar {
@@ -10999,11 +11228,13 @@ body.checkout-modal-open nav {
10999
11228
  gap: 0.75rem;
11000
11229
  width: max-content;
11001
11230
  padding-bottom: 0.25rem;
11231
+ padding-inline-end: 0.5rem;
11002
11232
  }
11003
11233
 
11004
11234
  @media (min-width: 768px) {
11005
11235
  .bestsellers-carousel__track {
11006
11236
  gap: 1rem;
11237
+ padding-inline-end: 1rem;
11007
11238
  }
11008
11239
  }
11009
11240
 
@@ -11014,7 +11245,6 @@ body.checkout-modal-open nav {
11014
11245
  min-width: 0;
11015
11246
  overflow: hidden;
11016
11247
  scroll-snap-align: start;
11017
- scroll-snap-stop: always;
11018
11248
  }
11019
11249
 
11020
11250
  .bestsellers-carousel__slide .product-card {
@@ -11083,6 +11313,12 @@ body.checkout-modal-open nav {
11083
11313
  transform: translateY(-1px);
11084
11314
  }
11085
11315
 
11316
+ .bestsellers-carousel__nav.is-hidden {
11317
+ opacity: 0;
11318
+ visibility: hidden;
11319
+ pointer-events: none;
11320
+ }
11321
+
11086
11322
  /* ── Promotional Banners ─────────────────────────────────── */
11087
11323
  .promo-banners {
11088
11324
  width: 100%;