@thefittingroom/shop-ui 5.0.26 → 5.0.28

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.
Files changed (2) hide show
  1. package/dist/index.js +442 -138
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14100,6 +14100,40 @@ function _init$7() {
14100
14100
  useMainStore.setState({
14101
14101
  fittingRoom: items
14102
14102
  });
14103
+ if (typeof window !== "undefined") {
14104
+ window.addEventListener("storage", (e) => {
14105
+ if (e.key !== STORAGE_KEY) {
14106
+ return;
14107
+ }
14108
+ useMainStore.setState({
14109
+ fittingRoom: readFittingRoom(brandId)
14110
+ });
14111
+ });
14112
+ }
14113
+ }
14114
+ function resolveCurrentProductCsaId(productId, size, color) {
14115
+ if (size == null) {
14116
+ return null;
14117
+ }
14118
+ const {
14119
+ currentProduct
14120
+ } = getStaticData();
14121
+ if (!currentProduct) {
14122
+ return null;
14123
+ }
14124
+ const stored = useMainStore.getState().productData[productId];
14125
+ if (!stored || "error" in stored) {
14126
+ return null;
14127
+ }
14128
+ const sizeRec = stored.sizeFitRecommendation.available_sizes.find((s) => getSizeLabelFromSize(s) === size);
14129
+ if (!sizeRec) {
14130
+ return null;
14131
+ }
14132
+ const csa = sizeRec.colorway_size_assets.find((c) => {
14133
+ const variant = currentProduct.variants.find((v) => v.sku === c.sku);
14134
+ return variant?.color === color;
14135
+ });
14136
+ return csa?.id ?? null;
14103
14137
  }
14104
14138
  async function addFittingRoomItem(productId, handle, isPdp) {
14105
14139
  const state = useMainStore.getState();
@@ -14108,9 +14142,7 @@ async function addFittingRoomItem(productId, handle, isPdp) {
14108
14142
  handle,
14109
14143
  isPdp
14110
14144
  });
14111
- let size = null;
14112
14145
  let color = null;
14113
- let colorwaySizeAssetId = null;
14114
14146
  let resolvedHandle = handle;
14115
14147
  if (isPdp) {
14116
14148
  const {
@@ -14122,37 +14154,59 @@ async function addFittingRoomItem(productId, handle, isPdp) {
14122
14154
  }
14123
14155
  try {
14124
14156
  const selection = await currentProduct.getSelectedOptions();
14125
- size = selection.size || null;
14126
14157
  color = selection.color;
14127
14158
  } catch (error) {
14128
14159
  logger$g.logWarn("Failed to read selected options from currentProduct", {
14129
14160
  error
14130
14161
  });
14131
14162
  }
14132
- const stored = state.productData[productId];
14133
- if (stored && !("error" in stored) && size != null) {
14134
- const sizeRec = stored.sizeFitRecommendation.available_sizes.find((s) => getSizeLabelFromSize(s) === size);
14135
- if (sizeRec) {
14136
- const csa = sizeRec.colorway_size_assets.find((c) => {
14137
- const variant = currentProduct.variants.find((v) => v.sku === c.sku);
14138
- return variant?.color === color;
14139
- });
14140
- if (csa) {
14141
- colorwaySizeAssetId = csa.id;
14142
- }
14143
- }
14144
- }
14145
14163
  }
14146
14164
  }
14147
14165
  state.addToFittingRoom({
14148
14166
  externalId: productId,
14149
14167
  handle: resolvedHandle,
14150
- size,
14168
+ size: null,
14151
14169
  color,
14152
- colorwaySizeAssetId,
14170
+ colorwaySizeAssetId: null,
14153
14171
  addedAt: Date.now()
14154
14172
  });
14155
14173
  }
14174
+ async function syncCurrentProductSelection() {
14175
+ const {
14176
+ currentProduct
14177
+ } = getStaticData();
14178
+ if (!currentProduct) {
14179
+ return;
14180
+ }
14181
+ const state = useMainStore.getState();
14182
+ const existing = state.fittingRoom.find((item) => item.externalId === currentProduct.externalId);
14183
+ if (!existing) {
14184
+ return;
14185
+ }
14186
+ let color = null;
14187
+ try {
14188
+ const selection = await currentProduct.getSelectedOptions();
14189
+ color = selection.color;
14190
+ } catch (error) {
14191
+ logger$g.logWarn("syncCurrentProductSelection: failed to read selected options", {
14192
+ error
14193
+ });
14194
+ return;
14195
+ }
14196
+ if (color === existing.color) {
14197
+ return;
14198
+ }
14199
+ const colorwaySizeAssetId = resolveCurrentProductCsaId(currentProduct.externalId, existing.size, color);
14200
+ state.updateFittingRoomItem(currentProduct.externalId, {
14201
+ color,
14202
+ colorwaySizeAssetId
14203
+ });
14204
+ logger$g.logDebug("{{ts}} - Synced PDP color into fitting-room", {
14205
+ externalId: currentProduct.externalId,
14206
+ color,
14207
+ colorwaySizeAssetId
14208
+ });
14209
+ }
14156
14210
  async function toggleFittingRoomItem(productId, handle, isPdp) {
14157
14211
  const state = useMainStore.getState();
14158
14212
  const isInFittingRoom = state.fittingRoom.some((item) => item.externalId === productId);
@@ -19304,7 +19358,7 @@ const loading$1 = "Loading...";
19304
19358
  const add_to_fitting_room$1 = "Add to Fitting Room";
19305
19359
  const added_to_fitting_room$1 = "In Fitting Room";
19306
19360
  const view_fitting_room$1 = "Fitting Room";
19307
- const fitting_room$1 = { "title": "Fitting Room", "empty": "Your fitting room is empty.", "size_not_chosen": "Size not chosen — pick one before trying on.", "remove": "Remove", "ungrouped": "Other", "try_it_on": "Try it on", "view_product_details": "View product details", "hide_product_details": "Hide product details", "add_to_cart": "Add to cart", "recommended_sizes": "Recommended sizes", "shop_now": "Shop now", "sign_out": "Sign out", "try_it_tucked_in": "Try it tucked in", "try_it_untucked": "Try it untucked", "avatar_placeholder_empty": "Add clothes to try on by selecting items in the rails.", "avatar_placeholder_selected": "Avatar render will appear here once VTO is wired up.", "see_selected_items": "See Selected Items", "no_selection": "Nothing selected yet.", "tuck_in": "Tuck In", "untuck": "Untuck", "zoom_in": "Zoom In", "zoom_out": "Zoom Out", "vto_error": "Couldn't load the try-on. Please try again." };
19361
+ const fitting_room$1 = { "title": "Fitting Room", "empty": "Your fitting room is empty.", "size_not_chosen": "Size not chosen — pick one before trying on.", "remove": "Remove", "ungrouped": "Other", "try_it_on": "Try it on", "view_product_details": "View product details", "hide_product_details": "Hide product details", "add_to_cart": "Add to cart", "recommended_sizes": "Recommended sizes", "shop_now": "Shop now", "sign_out": "Sign out", "clear_all": "Clear All", "try_it_tucked_in": "Try it tucked in", "try_it_untucked": "Try it untucked", "avatar_placeholder_empty": "Add clothes to try on by selecting items in the rails.", "avatar_placeholder_selected": "Avatar render will appear here once VTO is wired up.", "see_selected_items": "See Selected Items", "no_selection": "Nothing selected yet.", "tuck_in": "Tuck In", "untuck": "Untuck", "zoom_in": "Zoom In", "zoom_out": "Zoom Out", "vto_error": "Couldn't load the try-on. Please try again." };
19308
19362
  const landing$1 = { "header": "Meet your mini me!", "description": "Loose on your waist but tight on your hips? We’ll show you.", "get_the_app": "Get the app", "already_have_account": "Already have an account?", "sign_in": "Sign in." };
19309
19363
  const en$1 = {
19310
19364
  try_it_on: try_it_on$1,
@@ -41378,7 +41432,7 @@ async function _init$4() {
41378
41432
  config,
41379
41433
  testHooks
41380
41434
  } = getStaticData();
41381
- if (testHooks !== void 0) {
41435
+ if (testHooks && (testHooks.auth !== void 0 || testHooks.firestore !== void 0)) {
41382
41436
  const seedDocs = {
41383
41437
  ...testHooks.firestore?.docs ?? {}
41384
41438
  };
@@ -42201,7 +42255,12 @@ function AvatarControls({
42201
42255
  // clip rendered the text at fractional opacity while it reflowed each
42202
42256
  // frame, which subpixel-shimmered. The max-width clip alone reveals
42203
42257
  // the label cleanly at full opacity.
42204
- transition: "max-width 500ms cubic-bezier(0.22, 1, 0.36, 1)"
42258
+ transition: "max-width 500ms cubic-bezier(0.22, 1, 0.36, 1)",
42259
+ // Belt-and-suspenders with pillBaseStyle on the parent — rapid clicks
42260
+ // on the avatar's rotation chevrons can otherwise extend a triple-
42261
+ // click selection into these labels.
42262
+ userSelect: "none",
42263
+ WebkitUserSelect: "none"
42205
42264
  },
42206
42265
  pillLabelCollapsed: {
42207
42266
  maxWidth: 0
@@ -42317,37 +42376,43 @@ function MobileTuckControl({
42317
42376
  ] }) });
42318
42377
  }
42319
42378
  const DRAG_STEP_PX = 50;
42320
- function useFrameRotation(frameUrls, setSelectedFrameIndex) {
42379
+ const AXIS_LOCK_PX$1 = 8;
42380
+ function applyDragSteps(deltaX, rotateLeft, rotateRight) {
42381
+ const steps = Math.floor(Math.abs(deltaX) / DRAG_STEP_PX);
42382
+ if (steps === 0) {
42383
+ return 0;
42384
+ }
42385
+ const fn = deltaX > 0 ? rotateRight : rotateLeft;
42386
+ for (let i = 0; i < steps; i++) {
42387
+ fn();
42388
+ }
42389
+ return (deltaX > 0 ? 1 : -1) * steps * DRAG_STEP_PX;
42390
+ }
42391
+ function useFrameRotation(frameUrls, setSelectedFrameIndex, onUserInteract) {
42321
42392
  const frameCount = frameUrls?.length ?? 0;
42322
42393
  const rotateLeft = reactExports.useCallback(() => {
42394
+ onUserInteract?.();
42323
42395
  setSelectedFrameIndex((prev2) => {
42324
42396
  if (prev2 == null || frameCount === 0) {
42325
42397
  return prev2;
42326
42398
  }
42327
42399
  return prev2 === 0 ? frameCount - 1 : prev2 - 1;
42328
42400
  });
42329
- }, [frameCount, setSelectedFrameIndex]);
42401
+ }, [frameCount, onUserInteract, setSelectedFrameIndex]);
42330
42402
  const rotateRight = reactExports.useCallback(() => {
42403
+ onUserInteract?.();
42331
42404
  setSelectedFrameIndex((prev2) => {
42332
42405
  if (prev2 == null || frameCount === 0) {
42333
42406
  return prev2;
42334
42407
  }
42335
42408
  return prev2 === frameCount - 1 ? 0 : prev2 + 1;
42336
42409
  });
42337
- }, [frameCount, setSelectedFrameIndex]);
42410
+ }, [frameCount, onUserInteract, setSelectedFrameIndex]);
42338
42411
  const handleMouseDragStart = reactExports.useCallback((e) => {
42339
42412
  e.preventDefault();
42340
42413
  let startX = e.clientX;
42341
42414
  const onMove = (move) => {
42342
- const deltaX = move.clientX - startX;
42343
- if (Math.abs(deltaX) >= DRAG_STEP_PX) {
42344
- if (deltaX > 0) {
42345
- rotateRight();
42346
- } else {
42347
- rotateLeft();
42348
- }
42349
- startX = move.clientX;
42350
- }
42415
+ startX += applyDragSteps(move.clientX - startX, rotateLeft, rotateRight);
42351
42416
  };
42352
42417
  const onUp = () => {
42353
42418
  window.removeEventListener("mousemove", onMove);
@@ -42357,25 +42422,37 @@ function useFrameRotation(frameUrls, setSelectedFrameIndex) {
42357
42422
  window.addEventListener("mouseup", onUp);
42358
42423
  }, [rotateLeft, rotateRight]);
42359
42424
  const handleTouchDragStart = reactExports.useCallback((e) => {
42360
- e.preventDefault();
42361
42425
  let startX = e.touches[0].clientX;
42426
+ const startY = e.touches[0].clientY;
42427
+ let mode = "unknown";
42362
42428
  const onMove = (move) => {
42363
- const deltaX = move.touches[0].clientX - startX;
42364
- if (Math.abs(deltaX) >= DRAG_STEP_PX) {
42365
- if (deltaX > 0) {
42366
- rotateRight();
42367
- } else {
42368
- rotateLeft();
42429
+ const currentX = move.touches[0].clientX;
42430
+ const currentY = move.touches[0].clientY;
42431
+ if (mode === "unknown") {
42432
+ const absDx = Math.abs(currentX - startX);
42433
+ const absDy = Math.abs(currentY - startY);
42434
+ if (absDx < AXIS_LOCK_PX$1 && absDy < AXIS_LOCK_PX$1) {
42435
+ return;
42369
42436
  }
42370
- startX = move.touches[0].clientX;
42437
+ mode = absDx >= absDy ? "horizontal" : "vertical";
42438
+ startX = currentX;
42439
+ }
42440
+ if (mode !== "horizontal") {
42441
+ return;
42371
42442
  }
42443
+ move.preventDefault();
42444
+ startX += applyDragSteps(currentX - startX, rotateLeft, rotateRight);
42372
42445
  };
42373
42446
  const onEnd = () => {
42374
42447
  window.removeEventListener("touchmove", onMove);
42375
42448
  window.removeEventListener("touchend", onEnd);
42449
+ window.removeEventListener("touchcancel", onEnd);
42376
42450
  };
42377
- window.addEventListener("touchmove", onMove);
42451
+ window.addEventListener("touchmove", onMove, {
42452
+ passive: false
42453
+ });
42378
42454
  window.addEventListener("touchend", onEnd);
42455
+ window.addEventListener("touchcancel", onEnd);
42379
42456
  }, [rotateLeft, rotateRight]);
42380
42457
  return {
42381
42458
  rotateLeft,
@@ -42390,7 +42467,8 @@ function AvatarFrameViewer({
42390
42467
  setSelectedFrameIndex,
42391
42468
  imageContainerStyle,
42392
42469
  imageStyle,
42393
- loadingT = "quick-view.avatar_loading"
42470
+ loadingT = "quick-view.avatar_loading",
42471
+ onUserInteract
42394
42472
  }) {
42395
42473
  const css2 = useCss((_theme) => ({
42396
42474
  imageContainer: {
@@ -42398,21 +42476,31 @@ function AvatarFrameViewer({
42398
42476
  },
42399
42477
  image: {
42400
42478
  objectFit: "contain",
42401
- cursor: "grab"
42479
+ cursor: "grab",
42480
+ // Reserve horizontal touch gestures for our rotation handler — the
42481
+ // browser can still pan vertically natively. Without this, the
42482
+ // browser may start consuming a horizontal swipe as a scroll/zoom
42483
+ // before our touchmove listener can preventDefault, which produced
42484
+ // the "janky first swipe" reports.
42485
+ touchAction: "pan-y"
42402
42486
  },
42403
42487
  chevronLeftContainer: {
42404
42488
  position: "absolute",
42405
42489
  top: "50%",
42406
42490
  left: "0",
42407
42491
  transform: "translateY(-50%)",
42408
- cursor: "pointer"
42492
+ cursor: "pointer",
42493
+ userSelect: "none",
42494
+ WebkitUserSelect: "none"
42409
42495
  },
42410
42496
  chevronRightContainer: {
42411
42497
  position: "absolute",
42412
42498
  top: "50%",
42413
42499
  right: "0",
42414
42500
  transform: "translateY(-50%)",
42415
- cursor: "pointer"
42501
+ cursor: "pointer",
42502
+ userSelect: "none",
42503
+ WebkitUserSelect: "none"
42416
42504
  },
42417
42505
  chevronIcon: {
42418
42506
  width: "48px",
@@ -42420,46 +42508,83 @@ function AvatarFrameViewer({
42420
42508
  }
42421
42509
  }));
42422
42510
  reactExports.useEffect(() => {
42423
- if (!frameUrls || frameUrls.length === 0 || selectedFrameIndex != null) {
42424
- return;
42511
+ if (frameUrls && frameUrls.length > 0 && selectedFrameIndex == null) {
42512
+ setSelectedFrameIndex(0);
42425
42513
  }
42426
- let currentFrameIndex = 0;
42427
- setSelectedFrameIndex(currentFrameIndex);
42428
- const intervalId = setInterval(() => {
42429
- currentFrameIndex = (currentFrameIndex + 1) % frameUrls.length;
42430
- setSelectedFrameIndex(currentFrameIndex);
42431
- if (currentFrameIndex === 0) {
42432
- clearInterval(intervalId);
42433
- }
42434
- }, 500);
42435
- return () => clearInterval(intervalId);
42436
42514
  }, [frameUrls, selectedFrameIndex, setSelectedFrameIndex]);
42437
42515
  const {
42438
42516
  rotateLeft,
42439
42517
  rotateRight,
42440
42518
  handleMouseDragStart,
42441
42519
  handleTouchDragStart
42442
- } = useFrameRotation(frameUrls, setSelectedFrameIndex);
42520
+ } = useFrameRotation(frameUrls, setSelectedFrameIndex, onUserInteract);
42443
42521
  if (!frameUrls || selectedFrameIndex == null) {
42444
42522
  return /* @__PURE__ */ jsx$1(Loading, { t: loadingT });
42445
42523
  }
42446
42524
  return /* @__PURE__ */ jsxs("div", { css: css2.imageContainer, style: imageContainerStyle, children: [
42447
42525
  /* @__PURE__ */ jsx$1("img", { src: frameUrls[selectedFrameIndex], css: css2.image, style: imageStyle, onMouseDown: handleMouseDragStart, onTouchStart: handleTouchDragStart }),
42448
- /* @__PURE__ */ jsx$1("div", { css: css2.chevronLeftContainer, onClick: rotateLeft, children: /* @__PURE__ */ jsx$1(SvgChevronLeft, { css: css2.chevronIcon }) }),
42449
- /* @__PURE__ */ jsx$1("div", { css: css2.chevronRightContainer, onClick: rotateRight, children: /* @__PURE__ */ jsx$1(SvgChevronRight, { css: css2.chevronIcon }) })
42526
+ /* @__PURE__ */ jsx$1("div", { role: "button", "aria-label": "Rotate left", css: css2.chevronLeftContainer, onMouseDown: (e) => e.preventDefault(), onClick: rotateLeft, children: /* @__PURE__ */ jsx$1(SvgChevronLeft, { css: css2.chevronIcon }) }),
42527
+ /* @__PURE__ */ jsx$1("div", { role: "button", "aria-label": "Rotate right", css: css2.chevronRightContainer, onMouseDown: (e) => e.preventDefault(), onClick: rotateRight, children: /* @__PURE__ */ jsx$1(SvgChevronRight, { css: css2.chevronIcon }) })
42450
42528
  ] });
42451
42529
  }
42530
+ const AUTO_ROTATE_DURATION_MS = 4e3;
42531
+ function useAutoRotate(trigger, frameUrls, selectedFrameIndex, setSelectedFrameIndex) {
42532
+ const lastFiredRef = reactExports.useRef(void 0);
42533
+ const intervalIdRef = reactExports.useRef(null);
42534
+ const indexRef = reactExports.useRef(selectedFrameIndex);
42535
+ reactExports.useEffect(() => {
42536
+ indexRef.current = selectedFrameIndex;
42537
+ }, [selectedFrameIndex]);
42538
+ const cancelAutoRotate = reactExports.useCallback(() => {
42539
+ if (intervalIdRef.current !== null) {
42540
+ clearInterval(intervalIdRef.current);
42541
+ intervalIdRef.current = null;
42542
+ }
42543
+ }, []);
42544
+ const frameCount = frameUrls?.length ?? 0;
42545
+ reactExports.useEffect(() => {
42546
+ if (trigger === void 0) {
42547
+ return;
42548
+ }
42549
+ if (frameCount === 0) {
42550
+ return;
42551
+ }
42552
+ if (trigger === lastFiredRef.current) {
42553
+ return;
42554
+ }
42555
+ const tickMs = Math.round(AUTO_ROTATE_DURATION_MS / frameCount);
42556
+ const startFrameIndex = (indexRef.current ?? 0) % frameCount;
42557
+ let currentFrameIndex = startFrameIndex;
42558
+ let didFirstTick = false;
42559
+ intervalIdRef.current = setInterval(() => {
42560
+ if (!didFirstTick) {
42561
+ didFirstTick = true;
42562
+ lastFiredRef.current = trigger;
42563
+ }
42564
+ const nextFrameIndex = (currentFrameIndex + 1) % frameCount;
42565
+ currentFrameIndex = nextFrameIndex;
42566
+ setSelectedFrameIndex(nextFrameIndex);
42567
+ if (nextFrameIndex === startFrameIndex) {
42568
+ cancelAutoRotate();
42569
+ }
42570
+ }, tickMs);
42571
+ return cancelAutoRotate;
42572
+ }, [trigger, frameCount, setSelectedFrameIndex, cancelAutoRotate]);
42573
+ return cancelAutoRotate;
42574
+ }
42452
42575
  function AvatarPane({
42453
42576
  frameUrls,
42454
42577
  hasSelection,
42455
42578
  controls,
42456
42579
  mobileFullscreen,
42457
42580
  selectedFrameIndex: indexProp,
42458
- setSelectedFrameIndex: setIndexProp
42581
+ setSelectedFrameIndex: setIndexProp,
42582
+ autoRotateTrigger
42459
42583
  }) {
42460
42584
  const [localFrameIndex, setLocalFrameIndex] = reactExports.useState(null);
42461
42585
  const selectedFrameIndex = indexProp !== void 0 ? indexProp : localFrameIndex;
42462
42586
  const setSelectedFrameIndex = setIndexProp ?? setLocalFrameIndex;
42587
+ const cancelAutoRotate = useAutoRotate(autoRotateTrigger, frameUrls, selectedFrameIndex, setSelectedFrameIndex);
42463
42588
  const css2 = useCss((theme) => ({
42464
42589
  container: {
42465
42590
  width: "100%",
@@ -42517,7 +42642,7 @@ function AvatarPane({
42517
42642
  }
42518
42643
  }));
42519
42644
  if (frameUrls && frameUrls.length > 0) {
42520
- const viewer = /* @__PURE__ */ jsx$1(AvatarFrameViewer, { frameUrls, selectedFrameIndex, setSelectedFrameIndex, imageContainerStyle: css2.frameContainer, imageStyle: css2.frameImage, loadingT: "quick-view.avatar_loading" });
42645
+ const viewer = /* @__PURE__ */ jsx$1(AvatarFrameViewer, { frameUrls, selectedFrameIndex, setSelectedFrameIndex, imageContainerStyle: css2.frameContainer, imageStyle: css2.frameImage, loadingT: "quick-view.avatar_loading", onUserInteract: cancelAutoRotate });
42521
42646
  if (mobileFullscreen) {
42522
42647
  return /* @__PURE__ */ jsxs("div", { css: css2.mobileContainer, children: [
42523
42648
  /* @__PURE__ */ jsxs("div", { css: css2.mobileFrameSlot, children: [
@@ -42644,8 +42769,19 @@ function ProductCard({
42644
42769
  onClick();
42645
42770
  };
42646
42771
  const name2 = item.merchantProduct?.productName ?? item.externalId;
42647
- const imageUrl = item.merchantProduct?.imageUrl ?? null;
42648
- const price = item.merchantProduct?.variants[0]?.priceFormatted ?? null;
42772
+ let effectiveColor = item.storage.color;
42773
+ if (!effectiveColor && item.loadedProduct) {
42774
+ const sizeRec = item.loadedProduct.sizeFitRecommendation;
42775
+ const recommendedSize = sizeRec.available_sizes.find((s) => s.id === sizeRec.recommended_size.id);
42776
+ const firstCsa = recommendedSize?.colorway_size_assets[0];
42777
+ if (firstCsa) {
42778
+ const variant = item.merchantProduct?.variants.find((v) => v.sku === firstCsa.sku);
42779
+ effectiveColor = variant?.color ?? null;
42780
+ }
42781
+ }
42782
+ const selectedVariant = item.merchantProduct?.variants.find((v) => v.color === effectiveColor && (!item.storage.size || v.size === item.storage.size));
42783
+ const imageUrl = selectedVariant?.imageUrl ?? item.merchantProduct?.imageUrl ?? null;
42784
+ const price = selectedVariant?.priceFormatted ?? item.merchantProduct?.variants[0]?.priceFormatted ?? null;
42649
42785
  return /* @__PURE__ */ jsxs("div", { css: /* @__PURE__ */ css$1({
42650
42786
  ...css2.container,
42651
42787
  ...selected && css2.containerSelected,
@@ -43353,7 +43489,6 @@ function DetailAccordion({
43353
43489
  }) });
43354
43490
  }
43355
43491
  const AXIS_LOCK_PX = 8;
43356
- const ROTATE_STEP_PX = 50;
43357
43492
  function ZoomModal({
43358
43493
  frameUrls,
43359
43494
  selectedFrameIndex,
@@ -43399,15 +43534,7 @@ function ZoomModal({
43399
43534
  if (mode === "scroll" && scrollArea) {
43400
43535
  scrollArea.scrollTop = startScrollTop - deltaY;
43401
43536
  } else if (mode === "rotate") {
43402
- const rotateDelta = move.clientX - lastRotateX;
43403
- if (Math.abs(rotateDelta) >= ROTATE_STEP_PX) {
43404
- if (rotateDelta > 0) {
43405
- rotateRight();
43406
- } else {
43407
- rotateLeft();
43408
- }
43409
- lastRotateX = move.clientX;
43410
- }
43537
+ lastRotateX += applyDragSteps(move.clientX - lastRotateX, rotateLeft, rotateRight);
43411
43538
  }
43412
43539
  };
43413
43540
  const onUp = () => {
@@ -43453,7 +43580,11 @@ function ZoomModal({
43453
43580
  transform: "translateY(-50%)",
43454
43581
  display: "flex",
43455
43582
  cursor: "pointer",
43456
- zIndex: 1
43583
+ zIndex: 1,
43584
+ // Rapid clicks shouldn't initiate a text-selection drag into the
43585
+ // close-button glyph or any other overlay text.
43586
+ userSelect: "none",
43587
+ WebkitUserSelect: "none"
43457
43588
  },
43458
43589
  chevronLeft: {
43459
43590
  left: "8px"
@@ -43499,11 +43630,11 @@ function ZoomModal({
43499
43630
  /* @__PURE__ */ jsx$1("div", { css: /* @__PURE__ */ css$1({
43500
43631
  ...css2.chevron,
43501
43632
  ...css2.chevronLeft
43502
- }, "", ""), onClick: rotateLeft, children: /* @__PURE__ */ jsx$1(SvgChevronLeft, { css: css2.chevronIcon }) }),
43633
+ }, "", ""), onMouseDown: (e) => e.preventDefault(), onClick: rotateLeft, children: /* @__PURE__ */ jsx$1(SvgChevronLeft, { css: css2.chevronIcon }) }),
43503
43634
  /* @__PURE__ */ jsx$1("div", { css: /* @__PURE__ */ css$1({
43504
43635
  ...css2.chevron,
43505
43636
  ...css2.chevronRight
43506
- }, "", ""), onClick: rotateRight, children: /* @__PURE__ */ jsx$1(SvgChevronRight, { css: css2.chevronIcon }) }),
43637
+ }, "", ""), onMouseDown: (e) => e.preventDefault(), onClick: rotateRight, children: /* @__PURE__ */ jsx$1(SvgChevronRight, { css: css2.chevronIcon }) }),
43507
43638
  /* @__PURE__ */ jsx$1("button", { css: css2.close, onClick: onClose, "aria-label": "Close zoom", children: "×" })
43508
43639
  ] });
43509
43640
  }
@@ -43523,6 +43654,7 @@ function DesktopLayout$1({
43523
43654
  forceUntuck,
43524
43655
  canTuck,
43525
43656
  frameUrls,
43657
+ autoRotateTrigger,
43526
43658
  onSelectItem,
43527
43659
  onRemoveItem,
43528
43660
  onOpenAccordionItem,
@@ -43531,7 +43663,8 @@ function DesktopLayout$1({
43531
43663
  onChangeColor,
43532
43664
  onAddToCart,
43533
43665
  onToggleUntuck,
43534
- onSignOut
43666
+ onSignOut,
43667
+ onClearAll
43535
43668
  }) {
43536
43669
  const hasSelection = selectedItems.length > 0;
43537
43670
  const [avatarHovered, setAvatarHovered] = reactExports.useState(false);
@@ -43592,46 +43725,65 @@ function DesktopLayout$1({
43592
43725
  gap: "24px",
43593
43726
  padding: `26px ${EDGE_INSET_PX}px ${EDGE_INSET_PX}px 8px`
43594
43727
  },
43595
- // Sign-out is overlaid on the top-right corner of the rails column so it
43596
- // shares a row with the first card-rail header rather than reserving its
43597
- // own row. It scrolls away with the content, which is fine — it only
43598
- // needs to overlap that first header.
43599
- signOutWrapper: {
43600
- position: "absolute",
43601
- // Near the overlay top, partially overlapping the first card-rail
43602
- // header row below it.
43603
- top: "15px",
43604
- right: "24px",
43605
- zIndex: 3,
43728
+ // Shared icon+link visual for the rails column's two utility actions
43729
+ // (Clear All in the top-right corner, Sign Out at the bottom). Callers
43730
+ // add their own positioning on top.
43731
+ utilityLink: {
43606
43732
  display: "inline-flex",
43607
43733
  alignItems: "center",
43608
43734
  gap: "8px",
43609
43735
  cursor: "pointer",
43610
43736
  color: _theme.color_tfr_800
43611
43737
  },
43612
- signOutIcon: {
43738
+ utilityIcon: {
43613
43739
  width: "12px",
43614
43740
  height: "22px",
43615
43741
  fill: _theme.color_tfr_800,
43616
43742
  flex: "none"
43617
43743
  },
43618
- signOut: {
43744
+ utilityText: {
43619
43745
  color: _theme.color_tfr_800,
43620
43746
  fontSize: "14px"
43747
+ },
43748
+ // Clear All is overlaid on the top-right corner of the rails column so it
43749
+ // shares a row with the first card-rail header rather than reserving its
43750
+ // own row. It scrolls away with the content, which is fine — it only
43751
+ // needs to overlap that first header.
43752
+ clearAllWrapper: {
43753
+ position: "absolute",
43754
+ top: "15px",
43755
+ right: "24px",
43756
+ zIndex: 3
43757
+ },
43758
+ // Sign Out sits at the bottom of the rails column, after the last card
43759
+ // rail. It scrolls with the content — visible once the shopper reaches
43760
+ // the end of their items. marginTop: auto in a flex column would only
43761
+ // help with a non-scrolling parent; here the scroll context makes a
43762
+ // top margin enough to separate it from the last rail above.
43763
+ signOutWrapper: {
43764
+ marginTop: "8px",
43765
+ justifyContent: "center"
43621
43766
  }
43622
43767
  }));
43623
43768
  const controls = hasSelection ? /* @__PURE__ */ jsx$1(AvatarControls, { selectedItems, canTuck, forceUntuck, zoomed: zoomOpen, expanded: avatarHovered, onToggleUntuck, onToggleZoom: () => setZoomOpen(true), onRemoveItem }) : null;
43624
43769
  return /* @__PURE__ */ jsxs("div", { ref: containerRef, css: css2.container, style: {
43625
43770
  gridTemplateColumns
43626
43771
  }, children: [
43627
- /* @__PURE__ */ jsx$1("div", { css: css2.avatarColumn, onMouseEnter: () => setAvatarHovered(true), onMouseLeave: () => setAvatarHovered(false), children: /* @__PURE__ */ jsx$1(AvatarPane, { hasSelection, frameUrls, controls, selectedFrameIndex, setSelectedFrameIndex }) }),
43772
+ /* @__PURE__ */ jsx$1("div", { css: css2.avatarColumn, onMouseEnter: () => setAvatarHovered(true), onMouseLeave: () => setAvatarHovered(false), children: /* @__PURE__ */ jsx$1(AvatarPane, { hasSelection, frameUrls, controls, selectedFrameIndex, setSelectedFrameIndex, autoRotateTrigger }) }),
43628
43773
  hasSelection ? /* @__PURE__ */ jsx$1("div", { css: css2.detailColumn, children: /* @__PURE__ */ jsx$1(DetailAccordion, { items: selectedItems, openItemExternalId: openAccordionItemId, platform: "desktop", detailMode, isMobileQuickRow: false, forceUntuck, canTuck, onOpenItem: onOpenAccordionItem, onChangeDetailMode, onChangeSize, onChangeColor, onAddToCart, onToggleUntuck }) }) : null,
43629
43774
  /* @__PURE__ */ jsxs("div", { css: css2.railsColumn, children: [
43630
- /* @__PURE__ */ jsxs("span", { css: css2.signOutWrapper, onClick: onSignOut, children: [
43631
- /* @__PURE__ */ jsx$1(SvgTfrIcon, { css: css2.signOutIcon }),
43632
- /* @__PURE__ */ jsx$1(LinkT, { variant: "underline", css: css2.signOut, t: "fitting_room.sign_out" })
43633
- ] }),
43634
- resolved.groups.map((group) => /* @__PURE__ */ jsx$1(CardRail, { group, availabilityByExternalId, onSelectItem, onRemoveItem }, group.group.name))
43775
+ /* @__PURE__ */ jsx$1("span", { css: /* @__PURE__ */ css$1({
43776
+ ...css2.utilityLink,
43777
+ ...css2.clearAllWrapper
43778
+ }, "", ""), onClick: onClearAll, children: /* @__PURE__ */ jsx$1(LinkT, { variant: "underline", css: css2.utilityText, t: "fitting_room.clear_all" }) }),
43779
+ resolved.groups.map((group) => /* @__PURE__ */ jsx$1(CardRail, { group, availabilityByExternalId, onSelectItem, onRemoveItem }, group.group.name)),
43780
+ /* @__PURE__ */ jsxs("span", { css: /* @__PURE__ */ css$1({
43781
+ ...css2.utilityLink,
43782
+ ...css2.signOutWrapper
43783
+ }, "", ""), onClick: onSignOut, children: [
43784
+ /* @__PURE__ */ jsx$1(SvgTfrIcon, { css: css2.utilityIcon }),
43785
+ /* @__PURE__ */ jsx$1(LinkT, { variant: "underline", css: css2.utilityText, t: "fitting_room.sign_out" })
43786
+ ] })
43635
43787
  ] }),
43636
43788
  zoomOpen && frameUrls && frameUrls.length > 0 ? /* @__PURE__ */ jsx$1(ZoomModal, { frameUrls, selectedFrameIndex, setSelectedFrameIndex, onClose: () => setZoomOpen(false) }) : null
43637
43789
  ] });
@@ -43751,6 +43903,7 @@ function MobileLayout$1({
43751
43903
  forceUntuck,
43752
43904
  canTuck,
43753
43905
  frameUrls,
43906
+ autoRotateTrigger,
43754
43907
  sheetSnap,
43755
43908
  sheetTouchStart,
43756
43909
  onSelectItem,
@@ -43762,12 +43915,14 @@ function MobileLayout$1({
43762
43915
  onChangeSize,
43763
43916
  onChangeColor,
43764
43917
  onAddToCart,
43765
- onToggleUntuck
43918
+ onToggleUntuck,
43919
+ onSignOut,
43920
+ onClearAll
43766
43921
  }) {
43767
43922
  if (mode === "browse") {
43768
- return /* @__PURE__ */ jsx$1(BrowseView, { resolved, availabilityByExternalId, selectedCount: selectedItems.length, onSelectItem, onRemoveItem, onTryItOn });
43923
+ return /* @__PURE__ */ jsx$1(BrowseView, { resolved, availabilityByExternalId, selectedCount: selectedItems.length, onSelectItem, onRemoveItem, onTryItOn, onSignOut, onClearAll });
43769
43924
  }
43770
- return /* @__PURE__ */ jsx$1(TryOnView, { selectedItems, openAccordionItemId, detailMode, forceUntuck, canTuck, frameUrls, sheetSnap, sheetTouchStart, onBackToBrowse, onOpenAccordionItem, onChangeDetailMode, onChangeSize, onChangeColor, onAddToCart, onToggleUntuck });
43925
+ return /* @__PURE__ */ jsx$1(TryOnView, { selectedItems, openAccordionItemId, detailMode, forceUntuck, canTuck, frameUrls, autoRotateTrigger, sheetSnap, sheetTouchStart, onBackToBrowse, onOpenAccordionItem, onChangeDetailMode, onChangeSize, onChangeColor, onAddToCart, onToggleUntuck });
43771
43926
  }
43772
43927
  function BrowseView({
43773
43928
  resolved,
@@ -43775,7 +43930,9 @@ function BrowseView({
43775
43930
  selectedCount,
43776
43931
  onSelectItem,
43777
43932
  onRemoveItem,
43778
- onTryItOn
43933
+ onTryItOn,
43934
+ onSignOut,
43935
+ onClearAll
43779
43936
  }) {
43780
43937
  const railsAreaRef = reactExports.useRef(null);
43781
43938
  const sectionRefs = reactExports.useRef(/* @__PURE__ */ new Map());
@@ -43818,13 +43975,13 @@ function BrowseView({
43818
43975
  behavior: "smooth"
43819
43976
  });
43820
43977
  }, []);
43821
- const css2 = useCss((_theme) => ({
43978
+ const css2 = useCss((theme) => ({
43822
43979
  container: {
43823
43980
  display: "flex",
43824
43981
  flexDirection: "column",
43825
43982
  height: "100%",
43826
43983
  width: "100%",
43827
- // Positioning context for the floating SectionNav pill.
43984
+ // Positioning context for the floating SectionNav + Clear All pills.
43828
43985
  position: "relative"
43829
43986
  },
43830
43987
  railsArea: {
@@ -43835,6 +43992,62 @@ function BrowseView({
43835
43992
  flexDirection: "column",
43836
43993
  gap: "24px"
43837
43994
  },
43995
+ // Sign Out lives at the very bottom of the scrollable card list — same
43996
+ // pattern as desktop. Centered icon+link in TFR teal. Scrolls into view
43997
+ // once the shopper reaches the end of their items.
43998
+ signOutWrapper: {
43999
+ display: "inline-flex",
44000
+ alignItems: "center",
44001
+ justifyContent: "center",
44002
+ gap: "8px",
44003
+ cursor: "pointer",
44004
+ color: theme.color_tfr_800,
44005
+ marginTop: "8px"
44006
+ },
44007
+ signOutIcon: {
44008
+ width: "12px",
44009
+ height: "22px",
44010
+ fill: theme.color_tfr_800,
44011
+ flex: "none"
44012
+ },
44013
+ signOutText: {
44014
+ color: theme.color_tfr_800,
44015
+ fontSize: "14px"
44016
+ },
44017
+ // Clear All — white pill with thin black border, black text. Positioned
44018
+ // absolute over the BrowseView container, bottom-right, offset to clear
44019
+ // the Try It On CTA bar below it. Pill shape matches the SectionNav
44020
+ // pill at the top-right, but inverted on color for visual contrast.
44021
+ clearAllPill: {
44022
+ position: "absolute",
44023
+ bottom: "96px",
44024
+ right: "16px",
44025
+ zIndex: 5,
44026
+ display: "inline-flex",
44027
+ alignItems: "center",
44028
+ gap: "8px",
44029
+ padding: "6px 16px",
44030
+ borderRadius: "999px",
44031
+ backgroundColor: "#FFFFFF",
44032
+ color: theme.color_fg_text,
44033
+ border: `1px solid ${theme.color_fg_text}`,
44034
+ cursor: "pointer",
44035
+ fontSize: "13px",
44036
+ fontWeight: "500",
44037
+ letterSpacing: "0.5px",
44038
+ textTransform: "uppercase",
44039
+ whiteSpace: "nowrap"
44040
+ },
44041
+ clearAllText: {
44042
+ color: theme.color_fg_text
44043
+ },
44044
+ clearAllIcon: {
44045
+ width: "12px",
44046
+ height: "12px",
44047
+ flex: "none"
44048
+ // close-icon.svg's <path>s have a hardcoded dark fill — already matches
44049
+ // the new black-on-white pill, no override needed.
44050
+ },
43838
44051
  bottomBar: {
43839
44052
  flex: "none",
43840
44053
  padding: "16px",
@@ -43844,13 +44057,23 @@ function BrowseView({
43844
44057
  }));
43845
44058
  return /* @__PURE__ */ jsxs("div", { css: css2.container, children: [
43846
44059
  !resolved.isLoading && resolved.groups.length > 0 ? /* @__PURE__ */ jsx$1(SectionNav, { sections, activeName: activeSectionName, onSelect: scrollToSection }) : null,
43847
- /* @__PURE__ */ jsx$1("div", { ref: railsAreaRef, css: css2.railsArea, onScroll: recomputeActiveSection, children: resolved.groups.map((group) => /* @__PURE__ */ jsx$1("div", { ref: (el) => {
43848
- if (el) {
43849
- sectionRefs.current.set(group.group.name, el);
43850
- } else {
43851
- sectionRefs.current.delete(group.group.name);
43852
- }
43853
- }, children: /* @__PURE__ */ jsx$1(CardRail, { group, availabilityByExternalId, onSelectItem, onRemoveItem }) }, group.group.name)) }),
44060
+ /* @__PURE__ */ jsxs("div", { ref: railsAreaRef, css: css2.railsArea, onScroll: recomputeActiveSection, children: [
44061
+ resolved.groups.map((group) => /* @__PURE__ */ jsx$1("div", { ref: (el) => {
44062
+ if (el) {
44063
+ sectionRefs.current.set(group.group.name, el);
44064
+ } else {
44065
+ sectionRefs.current.delete(group.group.name);
44066
+ }
44067
+ }, children: /* @__PURE__ */ jsx$1(CardRail, { group, availabilityByExternalId, onSelectItem, onRemoveItem }) }, group.group.name)),
44068
+ /* @__PURE__ */ jsxs("span", { css: css2.signOutWrapper, onClick: onSignOut, children: [
44069
+ /* @__PURE__ */ jsx$1(SvgTfrIcon, { css: css2.signOutIcon }),
44070
+ /* @__PURE__ */ jsx$1(LinkT, { variant: "underline", css: css2.signOutText, t: "fitting_room.sign_out" })
44071
+ ] })
44072
+ ] }),
44073
+ /* @__PURE__ */ jsxs(Button, { variant: "base", css: css2.clearAllPill, onClick: onClearAll, children: [
44074
+ /* @__PURE__ */ jsx$1(TextT, { variant: "base", css: css2.clearAllText, t: "fitting_room.clear_all" }),
44075
+ /* @__PURE__ */ jsx$1(SvgCloseIcon, { css: css2.clearAllIcon })
44076
+ ] }),
43854
44077
  resolved.groups.length > 0 ? /* @__PURE__ */ jsx$1("div", { css: css2.bottomBar, children: /* @__PURE__ */ jsx$1(ButtonT, { variant: "brand", t: "fitting_room.try_it_on", onClick: onTryItOn, disabled: selectedCount === 0 }) }) : null
43855
44078
  ] });
43856
44079
  }
@@ -43861,6 +44084,7 @@ function TryOnView({
43861
44084
  forceUntuck,
43862
44085
  canTuck,
43863
44086
  frameUrls,
44087
+ autoRotateTrigger,
43864
44088
  sheetSnap,
43865
44089
  sheetTouchStart,
43866
44090
  onBackToBrowse,
@@ -43961,7 +44185,7 @@ function TryOnView({
43961
44185
  }
43962
44186
  }));
43963
44187
  return /* @__PURE__ */ jsxs("div", { css: css2.container, children: [
43964
- /* @__PURE__ */ jsx$1(AvatarPane, { hasSelection: selectedItems.length > 0, frameUrls, mobileFullscreen: true, controls: /* @__PURE__ */ jsx$1(MobileTuckControl, { canTuck, forceUntuck, onToggleUntuck }) }),
44188
+ /* @__PURE__ */ jsx$1(AvatarPane, { hasSelection: selectedItems.length > 0, frameUrls, autoRotateTrigger, mobileFullscreen: true, controls: /* @__PURE__ */ jsx$1(MobileTuckControl, { canTuck, forceUntuck, onToggleUntuck }) }),
43965
44189
  /* @__PURE__ */ jsx$1(Button, { variant: "base", css: css2.backButton, onClick: onBackToBrowse, "aria-label": "Back to browse", children: /* @__PURE__ */ jsx$1(SvgIconLeftArrow, { css: css2.backArrow }) }),
43966
44190
  /* @__PURE__ */ jsx$1("div", { css: css2.sheetOuter, style: sheetStyle, children: /* @__PURE__ */ jsxs("div", { ref: innerRef, css: css2.sheetInner, style: sheetStyle, children: [
43967
44191
  /* @__PURE__ */ jsxs("div", { css: css2.sheetHandleRow, onTouchStart: sheetTouchStart, children: [
@@ -44106,9 +44330,12 @@ function FittingRoomOverlay({
44106
44330
  const closeOverlay = useMainStore((state) => state.closeOverlay);
44107
44331
  const openOverlay = useMainStore((state) => state.openOverlay);
44108
44332
  const updateFittingRoomItem = useMainStore((state) => state.updateFittingRoomItem);
44333
+ const removeFromFittingRoom = useMainStore((state) => state.removeFromFittingRoom);
44334
+ const clearFittingRoom = useMainStore((state) => state.clearFittingRoom);
44109
44335
  const resolved = useResolvedFittingRoom();
44110
44336
  const [topOffset, setTopOffset] = reactExports.useState(0);
44111
44337
  const [selectedExternalIds, setSelectedExternalIds] = reactExports.useState(() => /* @__PURE__ */ new Set());
44338
+ const [autoRotateTrigger, setAutoRotateTrigger] = reactExports.useState(void 0);
44112
44339
  const [openAccordionItemId, setOpenAccordionItemId] = reactExports.useState(null);
44113
44340
  const [detailMode, setDetailMode] = reactExports.useState("compact");
44114
44341
  const [forceUntuck, setForceUntuck] = reactExports.useState(false);
@@ -44215,6 +44442,7 @@ function FittingRoomOverlay({
44215
44442
  nextSelected.add(externalId);
44216
44443
  ensureSizeForItem(item);
44217
44444
  setLastAddedExternalId(externalId);
44445
+ setAutoRotateTrigger((n) => (n ?? 0) + 1);
44218
44446
  if (!isMobileLayout) {
44219
44447
  setOpenAccordionItemId(externalId);
44220
44448
  setDetailMode("compact");
@@ -44307,7 +44535,8 @@ function FittingRoomOverlay({
44307
44535
  if (openAccordionItemId === externalId) {
44308
44536
  setOpenAccordionItemId(null);
44309
44537
  }
44310
- }, [openAccordionItemId]);
44538
+ removeFromFittingRoom(externalId);
44539
+ }, [openAccordionItemId, removeFromFittingRoom]);
44311
44540
  const handleTryItOn = reactExports.useCallback(() => {
44312
44541
  setMobileMode("try-on");
44313
44542
  setSheetSnap("collapsed");
@@ -44325,7 +44554,8 @@ function FittingRoomOverlay({
44325
44554
  if (preselectAppliedRef.current || !preselectExternalId) {
44326
44555
  return;
44327
44556
  }
44328
- if (!resolved.items.some((i) => i.externalId === preselectExternalId)) {
44557
+ const item = resolved.items.find((i) => i.externalId === preselectExternalId);
44558
+ if (!item || !item.merchantProduct || !item.loadedProduct) {
44329
44559
  return;
44330
44560
  }
44331
44561
  preselectAppliedRef.current = true;
@@ -44380,6 +44610,11 @@ function FittingRoomOverlay({
44380
44610
  });
44381
44611
  });
44382
44612
  }, [closeOverlay]);
44613
+ const handleClearAll = reactExports.useCallback(() => {
44614
+ clearFittingRoom();
44615
+ setSelectedExternalIds(/* @__PURE__ */ new Set());
44616
+ setOpenAccordionItemId(null);
44617
+ }, [clearFittingRoom]);
44383
44618
  const handleShopNow = reactExports.useCallback(() => {
44384
44619
  closeOverlay();
44385
44620
  }, [closeOverlay]);
@@ -44452,7 +44687,7 @@ function FittingRoomOverlay({
44452
44687
  /* @__PURE__ */ jsx$1(TextT, { variant: "base", css: css2.emptyTagline, t: "landing.description" }),
44453
44688
  /* @__PURE__ */ jsx$1("div", { css: css2.emptyShopNow, children: /* @__PURE__ */ jsx$1(ButtonT, { variant: "primary", t: "fitting_room.shop_now", onClick: handleShopNow }) }),
44454
44689
  userIsLoggedIn ? /* @__PURE__ */ jsx$1(LinkT, { variant: "underline", css: css2.emptySignOut, t: "fitting_room.sign_out", onClick: handleSignOut }) : null
44455
- ] }) }) : isMobileLayout ? /* @__PURE__ */ jsx$1(MobileLayout$1, { mode: mobileMode, resolved, selectedItems, availabilityByExternalId, openAccordionItemId, detailMode, forceUntuck, canTuck, frameUrls, sheetSnap, sheetTouchStart, onSelectItem: handleSelectItem, onRemoveItem: handleRemoveItem, onTryItOn: handleTryItOn, onBackToBrowse: handleBackToBrowse, onOpenAccordionItem: setOpenAccordionItemId, onChangeDetailMode: setDetailMode, onChangeSize: handleChangeSize, onChangeColor: handleChangeColor, onAddToCart: handleAddToCart, onToggleUntuck: handleToggleUntuck }) : /* @__PURE__ */ jsx$1(DesktopLayout$1, { resolved, selectedItems, availabilityByExternalId, openAccordionItemId, detailMode, forceUntuck, canTuck, frameUrls, onSelectItem: handleSelectItem, onRemoveItem: handleRemoveItem, onOpenAccordionItem: setOpenAccordionItemId, onChangeDetailMode: setDetailMode, onChangeSize: handleChangeSize, onChangeColor: handleChangeColor, onAddToCart: handleAddToCart, onToggleUntuck: handleToggleUntuck, onSignOut: handleSignOut }),
44690
+ ] }) }) : isMobileLayout ? /* @__PURE__ */ jsx$1(MobileLayout$1, { mode: mobileMode, resolved, selectedItems, availabilityByExternalId, openAccordionItemId, detailMode, forceUntuck, canTuck, frameUrls, autoRotateTrigger, sheetSnap, sheetTouchStart, onSelectItem: handleSelectItem, onRemoveItem: handleRemoveItem, onTryItOn: handleTryItOn, onBackToBrowse: handleBackToBrowse, onOpenAccordionItem: setOpenAccordionItemId, onChangeDetailMode: setDetailMode, onChangeSize: handleChangeSize, onChangeColor: handleChangeColor, onAddToCart: handleAddToCart, onToggleUntuck: handleToggleUntuck, onSignOut: handleSignOut, onClearAll: handleClearAll }) : /* @__PURE__ */ jsx$1(DesktopLayout$1, { resolved, selectedItems, availabilityByExternalId, openAccordionItemId, detailMode, forceUntuck, canTuck, frameUrls, autoRotateTrigger, onSelectItem: handleSelectItem, onRemoveItem: handleRemoveItem, onOpenAccordionItem: setOpenAccordionItemId, onChangeDetailMode: setDetailMode, onChangeSize: handleChangeSize, onChangeColor: handleChangeColor, onAddToCart: handleAddToCart, onToggleUntuck: handleToggleUntuck, onSignOut: handleSignOut, onClearAll: handleClearAll }),
44456
44691
  vtoError ? /* @__PURE__ */ jsx$1(Snackbar, { messageKey: "fitting_room.vto_error", onDismiss: clearVtoError }) : null
44457
44692
  ] }) });
44458
44693
  }
@@ -45179,6 +45414,8 @@ function FitChart({
45179
45414
  const AVATAR_IMAGE_ASPECT_RATIO = 2 / 3;
45180
45415
  const AVATAR_GUTTER_HEIGHT_PX = 100;
45181
45416
  const CONTENT_AREA_WIDTH_PX = 550;
45417
+ const SHOW_ROTATION_SLIDER = false;
45418
+ const DESKTOP_AVATAR_GUTTER_HEIGHT_PX = 0;
45182
45419
  const logger$5 = getLogger("overlays/quick-view");
45183
45420
  function QuickViewOverlay() {
45184
45421
  const userIsLoggedIn = useMainStore((state) => state.userIsLoggedIn);
@@ -45188,6 +45425,8 @@ function QuickViewOverlay() {
45188
45425
  const deviceLayout = useMainStore((state) => state.deviceLayout);
45189
45426
  const openOverlay = useMainStore((state) => state.openOverlay);
45190
45427
  const closeOverlay = useMainStore((state) => state.closeOverlay);
45428
+ const updateFittingRoomItem = useMainStore((state) => state.updateFittingRoomItem);
45429
+ const fittingRoomItems = useMainStore((state) => state.fittingRoom);
45191
45430
  const [vtoProductData, setVtoProductData] = reactExports.useState(null);
45192
45431
  const [selectedSizeLabel, setSelectedSizeLabel] = reactExports.useState(null);
45193
45432
  const [selectedColorLabel, setSelectedColorLabel] = reactExports.useState(null);
@@ -45422,6 +45661,27 @@ function QuickViewOverlay() {
45422
45661
  });
45423
45662
  });
45424
45663
  }, [closeOverlay]);
45664
+ const handleChangeColor = reactExports.useCallback((newColorLabel) => {
45665
+ setSelectedColorLabel(newColorLabel);
45666
+ const currentProduct = getStaticData().currentProduct;
45667
+ if (!currentProduct) {
45668
+ return;
45669
+ }
45670
+ const inRoom = fittingRoomItems.some((item) => item.externalId === currentProduct.externalId);
45671
+ if (!inRoom || !vtoProductData || !selectedSizeLabel) {
45672
+ return;
45673
+ }
45674
+ const sizeRec = vtoProductData.sizes.find((s) => s.sizeLabel === selectedSizeLabel);
45675
+ const csa = sizeRec?.colors.find((c) => c.colorLabel === newColorLabel);
45676
+ if (!csa) {
45677
+ return;
45678
+ }
45679
+ updateFittingRoomItem(currentProduct.externalId, {
45680
+ colorwaySizeAssetId: csa.colorwaySizeAssetId,
45681
+ size: selectedSizeLabel,
45682
+ color: csa.colorLabel
45683
+ });
45684
+ }, [fittingRoomItems, selectedSizeLabel, updateFittingRoomItem, vtoProductData]);
45425
45685
  const handleAddToCartClick = reactExports.useCallback(async () => {
45426
45686
  try {
45427
45687
  if (!selectedSizeLabel) {
@@ -45457,7 +45717,7 @@ function QuickViewOverlay() {
45457
45717
  Layout = DesktopLayout;
45458
45718
  }
45459
45719
  return /* @__PURE__ */ jsxs(SidecarModalFrame, { onRequestClose: closeOverlay, contentStyle: modalStyle, children: [
45460
- /* @__PURE__ */ jsx$1(Layout, { loadedProductData: vtoProductData, selectedColorSizeRecord, availableColorLabels, selectedColorLabel, selectedSizeLabel, frameUrls, setModalStyle, onClose: closeOverlay, onChangeColor: setSelectedColorLabel, onChangeSize: setSelectedSizeLabel, onAddToCart: handleAddToCartClick, onSignOut: handleSignOutClick }),
45720
+ /* @__PURE__ */ jsx$1(Layout, { loadedProductData: vtoProductData, selectedColorSizeRecord, availableColorLabels, selectedColorLabel, selectedSizeLabel, frameUrls, setModalStyle, onClose: closeOverlay, onChangeColor: handleChangeColor, onChangeSize: setSelectedSizeLabel, onAddToCart: handleAddToCartClick, onSignOut: handleSignOutClick }),
45461
45721
  vtoError ? /* @__PURE__ */ jsx$1(Snackbar, { messageKey: "quick-view.vto_error", onDismiss: clearVtoError }) : null
45462
45722
  ] });
45463
45723
  }
@@ -45695,7 +45955,10 @@ function MobileLayout({
45695
45955
  }
45696
45956
  function MobileContentCollapsed({
45697
45957
  loadedProductData,
45958
+ availableColorLabels,
45959
+ selectedColorLabel,
45698
45960
  selectedSizeLabel,
45961
+ onChangeColor,
45699
45962
  onChangeSize
45700
45963
  }) {
45701
45964
  const css2 = useCss((_theme) => ({
@@ -45705,17 +45968,24 @@ function MobileContentCollapsed({
45705
45968
  selectSizeLabelText: {},
45706
45969
  sizeSelectorContainer: {
45707
45970
  marginTop: "8px"
45971
+ },
45972
+ colorSelectorContainer: {
45973
+ marginTop: "8px"
45708
45974
  }
45709
45975
  }));
45710
45976
  return /* @__PURE__ */ jsxs(Fragment, { children: [
45711
45977
  /* @__PURE__ */ jsx$1("div", { css: css2.selectSizeLabelContainer, children: /* @__PURE__ */ jsx$1(TextT, { variant: "base", css: css2.selectSizeLabelText, t: "size-rec.select_size" }) }),
45712
- /* @__PURE__ */ jsx$1("div", { css: css2.sizeSelectorContainer, children: /* @__PURE__ */ jsx$1(SizeSelector, { loadedProductData, selectedSizeLabel, onChangeSize }) })
45978
+ /* @__PURE__ */ jsx$1("div", { css: css2.sizeSelectorContainer, children: /* @__PURE__ */ jsx$1(SizeSelector, { loadedProductData, selectedSizeLabel, onChangeSize }) }),
45979
+ /* @__PURE__ */ jsx$1("div", { css: css2.colorSelectorContainer, children: /* @__PURE__ */ jsx$1(ColorSelector, { availableColorLabels, selectedColorLabel, onChangeColor }) })
45713
45980
  ] });
45714
45981
  }
45715
45982
  function MobileContentExpanded({
45716
45983
  loadedProductData,
45984
+ availableColorLabels,
45985
+ selectedColorLabel,
45717
45986
  selectedSizeLabel,
45718
45987
  onChangeContentView,
45988
+ onChangeColor,
45719
45989
  onChangeSize,
45720
45990
  onAddToCart
45721
45991
  }) {
@@ -45727,6 +45997,9 @@ function MobileContentExpanded({
45727
45997
  sizeSelectorContainer: {
45728
45998
  marginTop: "8px"
45729
45999
  },
46000
+ colorSelectorContainer: {
46001
+ marginTop: "8px"
46002
+ },
45730
46003
  itemFitTextContainer: {
45731
46004
  marginTop: "8px"
45732
46005
  },
@@ -45756,6 +46029,7 @@ function MobileContentExpanded({
45756
46029
  return /* @__PURE__ */ jsxs(Fragment, { children: [
45757
46030
  /* @__PURE__ */ jsx$1("div", { css: css2.selectSizeLabelContainer, children: /* @__PURE__ */ jsx$1(TextT, { variant: "base", css: css2.selectSizeLabelText, t: "size-rec.select_size" }) }),
45758
46031
  /* @__PURE__ */ jsx$1("div", { css: css2.sizeSelectorContainer, children: /* @__PURE__ */ jsx$1(SizeSelector, { loadedProductData, selectedSizeLabel, onChangeSize }) }),
46032
+ /* @__PURE__ */ jsx$1("div", { css: css2.colorSelectorContainer, children: /* @__PURE__ */ jsx$1(ColorSelector, { availableColorLabels, selectedColorLabel, onChangeColor }) }),
45759
46033
  /* @__PURE__ */ jsx$1("div", { css: css2.itemFitTextContainer, children: /* @__PURE__ */ jsx$1(ItemFitText, { loadedProductData }) }),
45760
46034
  /* @__PURE__ */ jsx$1("div", { css: css2.itemFitDetailsContainer, children: /* @__PURE__ */ jsx$1(ItemFitDetails, { loadedProductData, selectedSizeLabel }) }),
45761
46035
  /* @__PURE__ */ jsxs("div", { css: css2.buttonContainer, children: [
@@ -45909,7 +46183,7 @@ function DesktopLayout({
45909
46183
  productNameContainer: {},
45910
46184
  productNameText: {
45911
46185
  fontFamily: "'Inter', sans-serif",
45912
- fontSize: "32px",
46186
+ fontSize: "24px",
45913
46187
  fontWeight: 300
45914
46188
  },
45915
46189
  priceContainer: {
@@ -46029,6 +46303,7 @@ function Avatar({
46029
46303
  });
46030
46304
  const [selectedFrameIndex, setSelectedFrameIndex] = reactExports.useState(null);
46031
46305
  const [zoomOpen, setZoomOpen] = reactExports.useState(false);
46306
+ const cancelAutoRotate = useAutoRotate(1, frameUrls, selectedFrameIndex, setSelectedFrameIndex);
46032
46307
  const css2 = useCss((theme) => ({
46033
46308
  topContainer: {
46034
46309
  flex: "none",
@@ -46038,8 +46313,11 @@ function Avatar({
46038
46313
  },
46039
46314
  zoomPill: {
46040
46315
  position: "absolute",
46041
- // Bottom-right of the avatar image, clear of the slider gutter below it.
46042
- bottom: `${AVATAR_GUTTER_HEIGHT_PX + 16}px`,
46316
+ // Bottom-right of the avatar image, 16px above the image's bottom edge.
46317
+ // When the rotation slider is on, that's also `clear of the gutter` —
46318
+ // when the slider is hidden, the gutter collapses to 0 and the 16px
46319
+ // offset alone keeps the pill inset from the image's true bottom.
46320
+ bottom: `${DESKTOP_AVATAR_GUTTER_HEIGHT_PX + 16}px`,
46043
46321
  right: "16px",
46044
46322
  display: "inline-flex",
46045
46323
  alignItems: "center",
@@ -46053,6 +46331,10 @@ function Avatar({
46053
46331
  letterSpacing: "0.5px",
46054
46332
  textTransform: "uppercase",
46055
46333
  cursor: "pointer",
46334
+ // Rapid clicks on the rotation chevrons can otherwise spill a triple-
46335
+ // click selection into this label.
46336
+ userSelect: "none",
46337
+ WebkitUserSelect: "none",
46056
46338
  zIndex: 2
46057
46339
  },
46058
46340
  zoomPillIcon: {
@@ -46129,7 +46411,7 @@ function Avatar({
46129
46411
  }
46130
46412
  } else {
46131
46413
  const screenHeightPx = window.innerHeight;
46132
- const bottomContainerHeightPx = AVATAR_GUTTER_HEIGHT_PX;
46414
+ const bottomContainerHeightPx = DESKTOP_AVATAR_GUTTER_HEIGHT_PX;
46133
46415
  const imageHeightPx = screenHeightPx - bottomContainerHeightPx;
46134
46416
  const imageWidthPx = Math.floor(imageHeightPx * AVATAR_IMAGE_ASPECT_RATIO);
46135
46417
  const modalWidthPx = imageWidthPx + CONTENT_AREA_WIDTH_PX;
@@ -46172,13 +46454,13 @@ function Avatar({
46172
46454
  }, [isMobileLayout, setModalStyle]);
46173
46455
  const isReady = !!frameUrls && selectedFrameIndex != null;
46174
46456
  return /* @__PURE__ */ jsxs("div", { css: css2.topContainer, style: layoutData.topContainerStyle, children: [
46175
- /* @__PURE__ */ jsx$1(AvatarFrameViewer, { frameUrls, selectedFrameIndex, setSelectedFrameIndex, imageContainerStyle: layoutData.imageContainerStyle, imageStyle: layoutData.imageStyle, loadingT: "quick-view.avatar_loading" }),
46457
+ /* @__PURE__ */ jsx$1(AvatarFrameViewer, { frameUrls, selectedFrameIndex, setSelectedFrameIndex, imageContainerStyle: layoutData.imageContainerStyle, imageStyle: layoutData.imageStyle, loadingT: "quick-view.avatar_loading", onUserInteract: cancelAutoRotate }),
46176
46458
  isReady && !isMobileLayout ? /* @__PURE__ */ jsxs(Button, { variant: "base", css: css2.zoomPill, onClick: () => setZoomOpen(true), children: [
46177
46459
  /* @__PURE__ */ jsx$1(SvgIconZoom, { css: css2.zoomPillIcon }),
46178
46460
  /* @__PURE__ */ jsx$1(TextT, { variant: "base", t: "quick-view.zoom_in" })
46179
46461
  ] }) : null,
46180
46462
  zoomOpen && frameUrls && frameUrls.length > 0 ? /* @__PURE__ */ jsx$1(ZoomModal, { frameUrls, selectedFrameIndex, setSelectedFrameIndex, onClose: () => setZoomOpen(false) }) : null,
46181
- frameUrls && selectedFrameIndex != null ? /* @__PURE__ */ jsx$1("div", { css: css2.bottomContainer, style: layoutData.bottomContainerStyle, children: isMobileLayout ? /* @__PURE__ */ jsx$1(Fragment, { children: " " }) : /* @__PURE__ */ jsxs(Fragment, { children: [
46463
+ frameUrls && selectedFrameIndex != null && (isMobileLayout || SHOW_ROTATION_SLIDER) ? /* @__PURE__ */ jsx$1("div", { css: css2.bottomContainer, style: layoutData.bottomContainerStyle, children: isMobileLayout ? /* @__PURE__ */ jsx$1(Fragment, { children: " " }) : /* @__PURE__ */ jsxs(Fragment, { children: [
46182
46464
  /* @__PURE__ */ jsx$1("input", { type: "range", min: 0, max: frameUrls.length - 1, step: 1, value: selectedFrameIndex, onChange: (e) => setSelectedFrameIndex(Number(e.target.value)), css: css2.sliderInput }),
46183
46465
  /* @__PURE__ */ jsx$1(TextT, { variant: "base", t: "quick-view.slide_to_rotate", css: css2.sliderText })
46184
46466
  ] }) }) : null
@@ -46543,6 +46825,13 @@ var DeviceLayout = /* @__PURE__ */ ((DeviceLayout2) => {
46543
46825
  })(DeviceLayout || {});
46544
46826
  function _init$1() {
46545
46827
  function getDeviceData() {
46828
+ const testDevice = getStaticData().testHooks?.device;
46829
+ if (testDevice) {
46830
+ return {
46831
+ isMobileDevice: testDevice.isMobileDevice ?? testDevice.layout.startsWith("mobile"),
46832
+ deviceLayout: testDevice.layout
46833
+ };
46834
+ }
46546
46835
  const bowserParser = Bowser.getParser(window.navigator.userAgent);
46547
46836
  const {
46548
46837
  width,
@@ -46706,28 +46995,41 @@ const useMainStore = create((set) => ({
46706
46995
  }
46707
46996
  })),
46708
46997
  // Fitting room:
46998
+ //
46999
+ // Each mutation reads the latest localStorage state before applying the
47000
+ // change, so two tabs adding different products at the same time merge
47001
+ // instead of last-write-wins. The in-memory Zustand value is just a
47002
+ // cache of "what was in localStorage the last time we touched it".
47003
+ // Cross-tab UI freshness (Tab B's open fitting-room sees Tab A's add
47004
+ // without a mutation of its own) is handled by the `storage` event
47005
+ // listener registered in fitting-room-storage.ts::_init.
46709
47006
  fittingRoom: [],
46710
- addToFittingRoom: (item) => set((prevState) => {
46711
- const filtered = prevState.fittingRoom.filter((existing) => existing.externalId !== item.externalId);
46712
- const next2 = [...filtered, item];
46713
- writeFittingRoom(getStaticData().brandId, next2);
47007
+ addToFittingRoom: (item) => set(() => {
47008
+ const brandId = getStaticData().brandId;
47009
+ const fresh = readFittingRoom(brandId);
47010
+ const next2 = [...fresh.filter((existing) => existing.externalId !== item.externalId), item];
47011
+ writeFittingRoom(brandId, next2);
46714
47012
  return {
46715
47013
  fittingRoom: next2
46716
47014
  };
46717
47015
  }),
46718
- removeFromFittingRoom: (externalId) => set((prevState) => {
46719
- const next2 = prevState.fittingRoom.filter((existing) => existing.externalId !== externalId);
46720
- writeFittingRoom(getStaticData().brandId, next2);
47016
+ removeFromFittingRoom: (externalId) => set(() => {
47017
+ const brandId = getStaticData().brandId;
47018
+ const fresh = readFittingRoom(brandId);
47019
+ const next2 = fresh.filter((existing) => existing.externalId !== externalId);
47020
+ writeFittingRoom(brandId, next2);
46721
47021
  return {
46722
47022
  fittingRoom: next2
46723
47023
  };
46724
47024
  }),
46725
- updateFittingRoomItem: (externalId, patch) => set((prevState) => {
46726
- const next2 = prevState.fittingRoom.map((existing) => existing.externalId === externalId ? {
47025
+ updateFittingRoomItem: (externalId, patch) => set(() => {
47026
+ const brandId = getStaticData().brandId;
47027
+ const fresh = readFittingRoom(brandId);
47028
+ const next2 = fresh.map((existing) => existing.externalId === externalId ? {
46727
47029
  ...existing,
46728
47030
  ...patch
46729
47031
  } : existing);
46730
- writeFittingRoom(getStaticData().brandId, next2);
47032
+ writeFittingRoom(brandId, next2);
46731
47033
  return {
46732
47034
  fittingRoom: next2
46733
47035
  };
@@ -46794,9 +47096,9 @@ const SHARED_CONFIG = {
46794
47096
  appGooglePlayUrl: "https://play.google.com/store/apps/details?id=com.thefittingroom.marketplace"
46795
47097
  },
46796
47098
  build: {
46797
- version: `${"5.0.26"}`,
46798
- commitHash: `${"357d81c"}`,
46799
- date: `${"2026-05-23T18:08:09.620Z"}`
47099
+ version: `${"5.0.28"}`,
47100
+ commitHash: `${"1e1de65"}`,
47101
+ date: `${"2026-05-25T00:50:20.004Z"}`
46800
47102
  }
46801
47103
  };
46802
47104
  const CONFIGS = {
@@ -47016,10 +47318,12 @@ async function logout() {
47016
47318
  }
47017
47319
  const TFR = {
47018
47320
  init,
47019
- logout
47321
+ logout,
47322
+ syncCurrentProductSelection
47020
47323
  };
47021
47324
  export {
47022
47325
  TFR as default,
47023
47326
  init,
47024
- logout
47327
+ logout,
47328
+ syncCurrentProductSelection
47025
47329
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thefittingroom/shop-ui",
3
- "version": "5.0.26",
3
+ "version": "5.0.28",
4
4
  "description": "the fitting room UI library",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",