@thefittingroom/shop-ui 5.0.25 → 5.0.27

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 +345 -125
  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);
@@ -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,9 @@ 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
+ const selectedVariant = item.merchantProduct?.variants.find((v) => v.color === item.storage.color && (!item.storage.size || v.size === item.storage.size));
42773
+ const imageUrl = selectedVariant?.imageUrl ?? item.merchantProduct?.imageUrl ?? null;
42774
+ const price = selectedVariant?.priceFormatted ?? item.merchantProduct?.variants[0]?.priceFormatted ?? null;
42649
42775
  return /* @__PURE__ */ jsxs("div", { css: /* @__PURE__ */ css$1({
42650
42776
  ...css2.container,
42651
42777
  ...selected && css2.containerSelected,
@@ -42875,7 +43001,7 @@ function ItemFitDetails({
42875
43001
  ] }) : fitLabel })
42876
43002
  ] }, index);
42877
43003
  });
42878
- }, [loadedProductData, selectedSizeLabel]);
43004
+ }, [loadedProductData, selectedSizeLabel, t, css2.detailCell, css2.firstLine, css2.line]);
42879
43005
  return /* @__PURE__ */ jsx$1("div", { css: css2.container, children: fitLineNodeList });
42880
43006
  }
42881
43007
  function ItemFitText({
@@ -42922,7 +43048,7 @@ function SizeSelector({
42922
43048
  }
42923
43049
  onChangeSize(sizeRecord.sizeLabel);
42924
43050
  }, children: sizeRecord.sizeLabel }, sizeRecord.sizeLabel);
42925
- }), [loadedProductData.sizes, selectedSizeLabel, onChangeSize]);
43051
+ }), [loadedProductData.sizes, selectedSizeLabel, onChangeSize, css2.button, css2.selectedButton]);
42926
43052
  return /* @__PURE__ */ jsx$1("div", { css: css2.container, children: sizeSelectorNodeList });
42927
43053
  }
42928
43054
  function DetailAccordionItem({
@@ -43019,8 +43145,10 @@ function DesktopAccordionItem({
43019
43145
  backgroundColor: ACCORDION_SHADE
43020
43146
  },
43021
43147
  categoryLabel: {
43022
- fontSize: "16px",
43023
- fontWeight: "400"
43148
+ fontFamily: "'Times New Roman', serif",
43149
+ fontSize: "20px",
43150
+ fontWeight: "400",
43151
+ letterSpacing: "0.04em"
43024
43152
  },
43025
43153
  chevron: {
43026
43154
  display: "inline-flex",
@@ -43041,6 +43169,7 @@ function DesktopAccordionItem({
43041
43169
  },
43042
43170
  productName: {
43043
43171
  fontSize: "24px",
43172
+ fontWeight: "300",
43044
43173
  lineHeight: "1.2"
43045
43174
  },
43046
43175
  price: {
@@ -43081,10 +43210,12 @@ function DesktopAccordionItem({
43081
43210
  },
43082
43211
  selectPrompt: {
43083
43212
  fontSize: "14px",
43213
+ fontWeight: "300",
43084
43214
  lineHeight: 1.5
43085
43215
  },
43086
43216
  fitText: {
43087
43217
  fontSize: "14px",
43218
+ fontWeight: "300",
43088
43219
  lineHeight: 1.5,
43089
43220
  // Tight 8px lift to the recommended-size line above; matches
43090
43221
  // quick-view's `itemFitContainer` marginTop.
@@ -43092,6 +43223,10 @@ function DesktopAccordionItem({
43092
43223
  },
43093
43224
  fitDetails: {
43094
43225
  width: "100%",
43226
+ // Cascades to the text inside <ItemFitDetails>; the size-selector
43227
+ // buttons and the bold recommended-size line above stay at their
43228
+ // own weights.
43229
+ fontWeight: 300,
43095
43230
  marginTop: "24px"
43096
43231
  },
43097
43232
  sizeRow: {
@@ -43187,12 +43322,16 @@ function MobileAccordionItem({
43187
43322
  minWidth: 0
43188
43323
  },
43189
43324
  categoryLabel: {
43190
- fontSize: "15px",
43325
+ fontFamily: "'Times New Roman', serif",
43326
+ fontSize: "16px",
43191
43327
  fontWeight: "400",
43328
+ letterSpacing: "0.04em",
43192
43329
  flex: "none"
43193
43330
  },
43194
43331
  productName: {
43195
- fontSize: "15px",
43332
+ fontFamily: "'Times New Roman', serif",
43333
+ fontSize: "16px",
43334
+ letterSpacing: "0.04em",
43196
43335
  color: "#8A8A8A",
43197
43336
  overflow: "hidden",
43198
43337
  textOverflow: "ellipsis",
@@ -43340,7 +43479,6 @@ function DetailAccordion({
43340
43479
  }) });
43341
43480
  }
43342
43481
  const AXIS_LOCK_PX = 8;
43343
- const ROTATE_STEP_PX = 50;
43344
43482
  function ZoomModal({
43345
43483
  frameUrls,
43346
43484
  selectedFrameIndex,
@@ -43386,15 +43524,7 @@ function ZoomModal({
43386
43524
  if (mode === "scroll" && scrollArea) {
43387
43525
  scrollArea.scrollTop = startScrollTop - deltaY;
43388
43526
  } else if (mode === "rotate") {
43389
- const rotateDelta = move.clientX - lastRotateX;
43390
- if (Math.abs(rotateDelta) >= ROTATE_STEP_PX) {
43391
- if (rotateDelta > 0) {
43392
- rotateRight();
43393
- } else {
43394
- rotateLeft();
43395
- }
43396
- lastRotateX = move.clientX;
43397
- }
43527
+ lastRotateX += applyDragSteps(move.clientX - lastRotateX, rotateLeft, rotateRight);
43398
43528
  }
43399
43529
  };
43400
43530
  const onUp = () => {
@@ -43440,7 +43570,11 @@ function ZoomModal({
43440
43570
  transform: "translateY(-50%)",
43441
43571
  display: "flex",
43442
43572
  cursor: "pointer",
43443
- zIndex: 1
43573
+ zIndex: 1,
43574
+ // Rapid clicks shouldn't initiate a text-selection drag into the
43575
+ // close-button glyph or any other overlay text.
43576
+ userSelect: "none",
43577
+ WebkitUserSelect: "none"
43444
43578
  },
43445
43579
  chevronLeft: {
43446
43580
  left: "8px"
@@ -43486,11 +43620,11 @@ function ZoomModal({
43486
43620
  /* @__PURE__ */ jsx$1("div", { css: /* @__PURE__ */ css$1({
43487
43621
  ...css2.chevron,
43488
43622
  ...css2.chevronLeft
43489
- }, "", ""), onClick: rotateLeft, children: /* @__PURE__ */ jsx$1(SvgChevronLeft, { css: css2.chevronIcon }) }),
43623
+ }, "", ""), onMouseDown: (e) => e.preventDefault(), onClick: rotateLeft, children: /* @__PURE__ */ jsx$1(SvgChevronLeft, { css: css2.chevronIcon }) }),
43490
43624
  /* @__PURE__ */ jsx$1("div", { css: /* @__PURE__ */ css$1({
43491
43625
  ...css2.chevron,
43492
43626
  ...css2.chevronRight
43493
- }, "", ""), onClick: rotateRight, children: /* @__PURE__ */ jsx$1(SvgChevronRight, { css: css2.chevronIcon }) }),
43627
+ }, "", ""), onMouseDown: (e) => e.preventDefault(), onClick: rotateRight, children: /* @__PURE__ */ jsx$1(SvgChevronRight, { css: css2.chevronIcon }) }),
43494
43628
  /* @__PURE__ */ jsx$1("button", { css: css2.close, onClick: onClose, "aria-label": "Close zoom", children: "×" })
43495
43629
  ] });
43496
43630
  }
@@ -43510,6 +43644,7 @@ function DesktopLayout$1({
43510
43644
  forceUntuck,
43511
43645
  canTuck,
43512
43646
  frameUrls,
43647
+ autoRotateTrigger,
43513
43648
  onSelectItem,
43514
43649
  onRemoveItem,
43515
43650
  onOpenAccordionItem,
@@ -43611,7 +43746,7 @@ function DesktopLayout$1({
43611
43746
  return /* @__PURE__ */ jsxs("div", { ref: containerRef, css: css2.container, style: {
43612
43747
  gridTemplateColumns
43613
43748
  }, children: [
43614
- /* @__PURE__ */ jsx$1("div", { css: css2.avatarColumn, onMouseEnter: () => setAvatarHovered(true), onMouseLeave: () => setAvatarHovered(false), children: /* @__PURE__ */ jsx$1(AvatarPane, { hasSelection, frameUrls, controls, selectedFrameIndex, setSelectedFrameIndex }) }),
43749
+ /* @__PURE__ */ jsx$1("div", { css: css2.avatarColumn, onMouseEnter: () => setAvatarHovered(true), onMouseLeave: () => setAvatarHovered(false), children: /* @__PURE__ */ jsx$1(AvatarPane, { hasSelection, frameUrls, controls, selectedFrameIndex, setSelectedFrameIndex, autoRotateTrigger }) }),
43615
43750
  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,
43616
43751
  /* @__PURE__ */ jsxs("div", { css: css2.railsColumn, children: [
43617
43752
  /* @__PURE__ */ jsxs("span", { css: css2.signOutWrapper, onClick: onSignOut, children: [
@@ -43738,6 +43873,7 @@ function MobileLayout$1({
43738
43873
  forceUntuck,
43739
43874
  canTuck,
43740
43875
  frameUrls,
43876
+ autoRotateTrigger,
43741
43877
  sheetSnap,
43742
43878
  sheetTouchStart,
43743
43879
  onSelectItem,
@@ -43754,7 +43890,7 @@ function MobileLayout$1({
43754
43890
  if (mode === "browse") {
43755
43891
  return /* @__PURE__ */ jsx$1(BrowseView, { resolved, availabilityByExternalId, selectedCount: selectedItems.length, onSelectItem, onRemoveItem, onTryItOn });
43756
43892
  }
43757
- return /* @__PURE__ */ jsx$1(TryOnView, { selectedItems, openAccordionItemId, detailMode, forceUntuck, canTuck, frameUrls, sheetSnap, sheetTouchStart, onBackToBrowse, onOpenAccordionItem, onChangeDetailMode, onChangeSize, onChangeColor, onAddToCart, onToggleUntuck });
43893
+ return /* @__PURE__ */ jsx$1(TryOnView, { selectedItems, openAccordionItemId, detailMode, forceUntuck, canTuck, frameUrls, autoRotateTrigger, sheetSnap, sheetTouchStart, onBackToBrowse, onOpenAccordionItem, onChangeDetailMode, onChangeSize, onChangeColor, onAddToCart, onToggleUntuck });
43758
43894
  }
43759
43895
  function BrowseView({
43760
43896
  resolved,
@@ -43848,6 +43984,7 @@ function TryOnView({
43848
43984
  forceUntuck,
43849
43985
  canTuck,
43850
43986
  frameUrls,
43987
+ autoRotateTrigger,
43851
43988
  sheetSnap,
43852
43989
  sheetTouchStart,
43853
43990
  onBackToBrowse,
@@ -43948,7 +44085,7 @@ function TryOnView({
43948
44085
  }
43949
44086
  }));
43950
44087
  return /* @__PURE__ */ jsxs("div", { css: css2.container, children: [
43951
- /* @__PURE__ */ jsx$1(AvatarPane, { hasSelection: selectedItems.length > 0, frameUrls, mobileFullscreen: true, controls: /* @__PURE__ */ jsx$1(MobileTuckControl, { canTuck, forceUntuck, onToggleUntuck }) }),
44088
+ /* @__PURE__ */ jsx$1(AvatarPane, { hasSelection: selectedItems.length > 0, frameUrls, autoRotateTrigger, mobileFullscreen: true, controls: /* @__PURE__ */ jsx$1(MobileTuckControl, { canTuck, forceUntuck, onToggleUntuck }) }),
43952
44089
  /* @__PURE__ */ jsx$1(Button, { variant: "base", css: css2.backButton, onClick: onBackToBrowse, "aria-label": "Back to browse", children: /* @__PURE__ */ jsx$1(SvgIconLeftArrow, { css: css2.backArrow }) }),
43953
44090
  /* @__PURE__ */ jsx$1("div", { css: css2.sheetOuter, style: sheetStyle, children: /* @__PURE__ */ jsxs("div", { ref: innerRef, css: css2.sheetInner, style: sheetStyle, children: [
43954
44091
  /* @__PURE__ */ jsxs("div", { css: css2.sheetHandleRow, onTouchStart: sheetTouchStart, children: [
@@ -44093,9 +44230,11 @@ function FittingRoomOverlay({
44093
44230
  const closeOverlay = useMainStore((state) => state.closeOverlay);
44094
44231
  const openOverlay = useMainStore((state) => state.openOverlay);
44095
44232
  const updateFittingRoomItem = useMainStore((state) => state.updateFittingRoomItem);
44233
+ const removeFromFittingRoom = useMainStore((state) => state.removeFromFittingRoom);
44096
44234
  const resolved = useResolvedFittingRoom();
44097
44235
  const [topOffset, setTopOffset] = reactExports.useState(0);
44098
44236
  const [selectedExternalIds, setSelectedExternalIds] = reactExports.useState(() => /* @__PURE__ */ new Set());
44237
+ const [autoRotateTrigger, setAutoRotateTrigger] = reactExports.useState(void 0);
44099
44238
  const [openAccordionItemId, setOpenAccordionItemId] = reactExports.useState(null);
44100
44239
  const [detailMode, setDetailMode] = reactExports.useState("compact");
44101
44240
  const [forceUntuck, setForceUntuck] = reactExports.useState(false);
@@ -44202,6 +44341,7 @@ function FittingRoomOverlay({
44202
44341
  nextSelected.add(externalId);
44203
44342
  ensureSizeForItem(item);
44204
44343
  setLastAddedExternalId(externalId);
44344
+ setAutoRotateTrigger((n) => (n ?? 0) + 1);
44205
44345
  if (!isMobileLayout) {
44206
44346
  setOpenAccordionItemId(externalId);
44207
44347
  setDetailMode("compact");
@@ -44294,7 +44434,8 @@ function FittingRoomOverlay({
44294
44434
  if (openAccordionItemId === externalId) {
44295
44435
  setOpenAccordionItemId(null);
44296
44436
  }
44297
- }, [openAccordionItemId]);
44437
+ removeFromFittingRoom(externalId);
44438
+ }, [openAccordionItemId, removeFromFittingRoom]);
44298
44439
  const handleTryItOn = reactExports.useCallback(() => {
44299
44440
  setMobileMode("try-on");
44300
44441
  setSheetSnap("collapsed");
@@ -44312,7 +44453,8 @@ function FittingRoomOverlay({
44312
44453
  if (preselectAppliedRef.current || !preselectExternalId) {
44313
44454
  return;
44314
44455
  }
44315
- if (!resolved.items.some((i) => i.externalId === preselectExternalId)) {
44456
+ const item = resolved.items.find((i) => i.externalId === preselectExternalId);
44457
+ if (!item || !item.merchantProduct || !item.loadedProduct) {
44316
44458
  return;
44317
44459
  }
44318
44460
  preselectAppliedRef.current = true;
@@ -44439,7 +44581,7 @@ function FittingRoomOverlay({
44439
44581
  /* @__PURE__ */ jsx$1(TextT, { variant: "base", css: css2.emptyTagline, t: "landing.description" }),
44440
44582
  /* @__PURE__ */ jsx$1("div", { css: css2.emptyShopNow, children: /* @__PURE__ */ jsx$1(ButtonT, { variant: "primary", t: "fitting_room.shop_now", onClick: handleShopNow }) }),
44441
44583
  userIsLoggedIn ? /* @__PURE__ */ jsx$1(LinkT, { variant: "underline", css: css2.emptySignOut, t: "fitting_room.sign_out", onClick: handleSignOut }) : null
44442
- ] }) }) : 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 }),
44584
+ ] }) }) : 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 }) : /* @__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 }),
44443
44585
  vtoError ? /* @__PURE__ */ jsx$1(Snackbar, { messageKey: "fitting_room.vto_error", onDismiss: clearVtoError }) : null
44444
44586
  ] }) });
44445
44587
  }
@@ -44629,7 +44771,7 @@ function GetAppOverlay({
44629
44771
  openOverlay(OverlayName.SIGN_IN, {
44630
44772
  returnToOverlay
44631
44773
  });
44632
- }, [returnToOverlay]);
44774
+ }, [openOverlay, returnToOverlay]);
44633
44775
  const handleGetAppAppleClick = reactExports.useCallback(() => {
44634
44776
  const url = getStaticData().config.links.appAppleStoreUrl;
44635
44777
  window.open(url, "_blank");
@@ -44696,12 +44838,12 @@ function LandingOverlay({
44696
44838
  openOverlay(OverlayName.GET_APP, {
44697
44839
  returnToOverlay
44698
44840
  });
44699
- }, [returnToOverlay]);
44841
+ }, [openOverlay, returnToOverlay]);
44700
44842
  const handleSignInClick = reactExports.useCallback(() => {
44701
44843
  openOverlay(OverlayName.SIGN_IN, {
44702
44844
  returnToOverlay
44703
44845
  });
44704
- }, [returnToOverlay]);
44846
+ }, [openOverlay, returnToOverlay]);
44705
44847
  return /* @__PURE__ */ jsxs(ContentModal, { onRequestClose: closeOverlay, title: /* @__PURE__ */ jsx$1(TextT, { variant: "brand", css: css2.titleText, t: "try_it_on" }), children: [
44706
44848
  /* @__PURE__ */ jsx$1("div", { css: css2.headerText, children: /* @__PURE__ */ jsx$1(TextT, { variant: "brand", css: css2.headerText, t: t("landing.header") }) }),
44707
44849
  /* @__PURE__ */ jsx$1("div", { css: css2.description, children: /* @__PURE__ */ jsx$1(TextT, { variant: "base", t: t("landing.description") }) }),
@@ -44841,12 +44983,12 @@ function SignInOverlay({
44841
44983
  openOverlay(OverlayName.FORGOT_PASSWORD, {
44842
44984
  returnToOverlay
44843
44985
  });
44844
- }, [returnToOverlay]);
44986
+ }, [openOverlay, returnToOverlay]);
44845
44987
  const handleGetAppClick = reactExports.useCallback(() => {
44846
44988
  openOverlay(OverlayName.GET_APP, {
44847
44989
  returnToOverlay
44848
44990
  });
44849
- }, [returnToOverlay]);
44991
+ }, [openOverlay, returnToOverlay]);
44850
44992
  return /* @__PURE__ */ jsx$1(ContentModal, { onRequestClose: closeOverlay, title: /* @__PURE__ */ jsx$1(TfrTitle, {}), children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, css: css2.form, children: [
44851
44993
  /* @__PURE__ */ jsx$1("div", { css: css2.emailContainer, children: /* @__PURE__ */ jsx$1("input", { name: "email", type: "email", placeholder: t("sign-in.email"), required: true, css: /* @__PURE__ */ css$1({
44852
44994
  ...css2.input,
@@ -45166,6 +45308,8 @@ function FitChart({
45166
45308
  const AVATAR_IMAGE_ASPECT_RATIO = 2 / 3;
45167
45309
  const AVATAR_GUTTER_HEIGHT_PX = 100;
45168
45310
  const CONTENT_AREA_WIDTH_PX = 550;
45311
+ const SHOW_ROTATION_SLIDER = false;
45312
+ const DESKTOP_AVATAR_GUTTER_HEIGHT_PX = 0;
45169
45313
  const logger$5 = getLogger("overlays/quick-view");
45170
45314
  function QuickViewOverlay() {
45171
45315
  const userIsLoggedIn = useMainStore((state) => state.userIsLoggedIn);
@@ -45175,6 +45319,8 @@ function QuickViewOverlay() {
45175
45319
  const deviceLayout = useMainStore((state) => state.deviceLayout);
45176
45320
  const openOverlay = useMainStore((state) => state.openOverlay);
45177
45321
  const closeOverlay = useMainStore((state) => state.closeOverlay);
45322
+ const updateFittingRoomItem = useMainStore((state) => state.updateFittingRoomItem);
45323
+ const fittingRoomItems = useMainStore((state) => state.fittingRoom);
45178
45324
  const [vtoProductData, setVtoProductData] = reactExports.useState(null);
45179
45325
  const [selectedSizeLabel, setSelectedSizeLabel] = reactExports.useState(null);
45180
45326
  const [selectedColorLabel, setSelectedColorLabel] = reactExports.useState(null);
@@ -45234,8 +45380,8 @@ function QuickViewOverlay() {
45234
45380
  color: selectedColor
45235
45381
  } = await currentProduct.getSelectedOptions();
45236
45382
  const styleCategoryIndex = await loadStyleCategoryIndex();
45237
- const styleCategoryGroup = styleCategoryIndex.groupForCategory(storeProduct.style.style_category_name);
45238
- const styleCategoryLabel = styleCategoryGroup?.label ?? null;
45383
+ const styleCategoryRecord = styleCategoryIndex.byName(storeProduct.style.style_category_name);
45384
+ const styleCategoryLabel = styleCategoryRecord?.label_singular ?? styleCategoryRecord?.label ?? null;
45239
45385
  const sizeRecommendationRecord = storeProduct.sizeFitRecommendation;
45240
45386
  {
45241
45387
  const recommendedSizeId = sizeRecommendationRecord.recommended_size.id || null;
@@ -45408,7 +45554,28 @@ function QuickViewOverlay() {
45408
45554
  error
45409
45555
  });
45410
45556
  });
45411
- }, [closeOverlay, openOverlay]);
45557
+ }, [closeOverlay]);
45558
+ const handleChangeColor = reactExports.useCallback((newColorLabel) => {
45559
+ setSelectedColorLabel(newColorLabel);
45560
+ const currentProduct = getStaticData().currentProduct;
45561
+ if (!currentProduct) {
45562
+ return;
45563
+ }
45564
+ const inRoom = fittingRoomItems.some((item) => item.externalId === currentProduct.externalId);
45565
+ if (!inRoom || !vtoProductData || !selectedSizeLabel) {
45566
+ return;
45567
+ }
45568
+ const sizeRec = vtoProductData.sizes.find((s) => s.sizeLabel === selectedSizeLabel);
45569
+ const csa = sizeRec?.colors.find((c) => c.colorLabel === newColorLabel);
45570
+ if (!csa) {
45571
+ return;
45572
+ }
45573
+ updateFittingRoomItem(currentProduct.externalId, {
45574
+ colorwaySizeAssetId: csa.colorwaySizeAssetId,
45575
+ size: selectedSizeLabel,
45576
+ color: csa.colorLabel
45577
+ });
45578
+ }, [fittingRoomItems, selectedSizeLabel, updateFittingRoomItem, vtoProductData]);
45412
45579
  const handleAddToCartClick = reactExports.useCallback(async () => {
45413
45580
  try {
45414
45581
  if (!selectedSizeLabel) {
@@ -45430,7 +45597,7 @@ function QuickViewOverlay() {
45430
45597
  error
45431
45598
  });
45432
45599
  }
45433
- }, [selectedColorLabel, selectedSizeLabel]);
45600
+ }, [closeOverlay, selectedColorLabel, selectedSizeLabel]);
45434
45601
  if (vtoProductData === false) {
45435
45602
  return /* @__PURE__ */ jsx$1(SidecarModalFrame, { onRequestClose: closeOverlay, children: /* @__PURE__ */ jsx$1(NoFitLayout, { onClose: closeOverlay, onSignOut: handleSignOutClick }) });
45436
45603
  }
@@ -45444,7 +45611,7 @@ function QuickViewOverlay() {
45444
45611
  Layout = DesktopLayout;
45445
45612
  }
45446
45613
  return /* @__PURE__ */ jsxs(SidecarModalFrame, { onRequestClose: closeOverlay, contentStyle: modalStyle, children: [
45447
- /* @__PURE__ */ jsx$1(Layout, { loadedProductData: vtoProductData, selectedColorSizeRecord, availableColorLabels, selectedColorLabel, selectedSizeLabel, frameUrls, setModalStyle, onClose: closeOverlay, onChangeColor: setSelectedColorLabel, onChangeSize: setSelectedSizeLabel, onAddToCart: handleAddToCartClick, onSignOut: handleSignOutClick }),
45614
+ /* @__PURE__ */ jsx$1(Layout, { loadedProductData: vtoProductData, selectedColorSizeRecord, availableColorLabels, selectedColorLabel, selectedSizeLabel, frameUrls, setModalStyle, onClose: closeOverlay, onChangeColor: handleChangeColor, onChangeSize: setSelectedSizeLabel, onAddToCart: handleAddToCartClick, onSignOut: handleSignOutClick }),
45448
45615
  vtoError ? /* @__PURE__ */ jsx$1(Snackbar, { messageKey: "quick-view.vto_error", onDismiss: clearVtoError }) : null
45449
45616
  ] });
45450
45617
  }
@@ -45682,7 +45849,10 @@ function MobileLayout({
45682
45849
  }
45683
45850
  function MobileContentCollapsed({
45684
45851
  loadedProductData,
45852
+ availableColorLabels,
45853
+ selectedColorLabel,
45685
45854
  selectedSizeLabel,
45855
+ onChangeColor,
45686
45856
  onChangeSize
45687
45857
  }) {
45688
45858
  const css2 = useCss((_theme) => ({
@@ -45692,17 +45862,24 @@ function MobileContentCollapsed({
45692
45862
  selectSizeLabelText: {},
45693
45863
  sizeSelectorContainer: {
45694
45864
  marginTop: "8px"
45865
+ },
45866
+ colorSelectorContainer: {
45867
+ marginTop: "8px"
45695
45868
  }
45696
45869
  }));
45697
45870
  return /* @__PURE__ */ jsxs(Fragment, { children: [
45698
45871
  /* @__PURE__ */ jsx$1("div", { css: css2.selectSizeLabelContainer, children: /* @__PURE__ */ jsx$1(TextT, { variant: "base", css: css2.selectSizeLabelText, t: "size-rec.select_size" }) }),
45699
- /* @__PURE__ */ jsx$1("div", { css: css2.sizeSelectorContainer, children: /* @__PURE__ */ jsx$1(SizeSelector, { loadedProductData, selectedSizeLabel, onChangeSize }) })
45872
+ /* @__PURE__ */ jsx$1("div", { css: css2.sizeSelectorContainer, children: /* @__PURE__ */ jsx$1(SizeSelector, { loadedProductData, selectedSizeLabel, onChangeSize }) }),
45873
+ /* @__PURE__ */ jsx$1("div", { css: css2.colorSelectorContainer, children: /* @__PURE__ */ jsx$1(ColorSelector, { availableColorLabels, selectedColorLabel, onChangeColor }) })
45700
45874
  ] });
45701
45875
  }
45702
45876
  function MobileContentExpanded({
45703
45877
  loadedProductData,
45878
+ availableColorLabels,
45879
+ selectedColorLabel,
45704
45880
  selectedSizeLabel,
45705
45881
  onChangeContentView,
45882
+ onChangeColor,
45706
45883
  onChangeSize,
45707
45884
  onAddToCart
45708
45885
  }) {
@@ -45714,6 +45891,9 @@ function MobileContentExpanded({
45714
45891
  sizeSelectorContainer: {
45715
45892
  marginTop: "8px"
45716
45893
  },
45894
+ colorSelectorContainer: {
45895
+ marginTop: "8px"
45896
+ },
45717
45897
  itemFitTextContainer: {
45718
45898
  marginTop: "8px"
45719
45899
  },
@@ -45743,6 +45923,7 @@ function MobileContentExpanded({
45743
45923
  return /* @__PURE__ */ jsxs(Fragment, { children: [
45744
45924
  /* @__PURE__ */ jsx$1("div", { css: css2.selectSizeLabelContainer, children: /* @__PURE__ */ jsx$1(TextT, { variant: "base", css: css2.selectSizeLabelText, t: "size-rec.select_size" }) }),
45745
45925
  /* @__PURE__ */ jsx$1("div", { css: css2.sizeSelectorContainer, children: /* @__PURE__ */ jsx$1(SizeSelector, { loadedProductData, selectedSizeLabel, onChangeSize }) }),
45926
+ /* @__PURE__ */ jsx$1("div", { css: css2.colorSelectorContainer, children: /* @__PURE__ */ jsx$1(ColorSelector, { availableColorLabels, selectedColorLabel, onChangeColor }) }),
45746
45927
  /* @__PURE__ */ jsx$1("div", { css: css2.itemFitTextContainer, children: /* @__PURE__ */ jsx$1(ItemFitText, { loadedProductData }) }),
45747
45928
  /* @__PURE__ */ jsx$1("div", { css: css2.itemFitDetailsContainer, children: /* @__PURE__ */ jsx$1(ItemFitDetails, { loadedProductData, selectedSizeLabel }) }),
45748
45929
  /* @__PURE__ */ jsxs("div", { css: css2.buttonContainer, children: [
@@ -45895,7 +46076,9 @@ function DesktopLayout({
45895
46076
  },
45896
46077
  productNameContainer: {},
45897
46078
  productNameText: {
45898
- fontSize: "32px"
46079
+ fontFamily: "'Inter', sans-serif",
46080
+ fontSize: "24px",
46081
+ fontWeight: 300
45899
46082
  },
45900
46083
  priceContainer: {
45901
46084
  marginTop: "8px"
@@ -45929,17 +46112,27 @@ function DesktopLayout({
45929
46112
  marginTop: "8px",
45930
46113
  lineHeight: "normal"
45931
46114
  },
45932
- itemFitText: {},
46115
+ itemFitText: {
46116
+ fontFamily: "'Inter', sans-serif",
46117
+ fontWeight: 300
46118
+ },
45933
46119
  selectSizeLabelContainer: {
45934
46120
  lineHeight: "normal"
45935
46121
  },
45936
- selectSizeLabelText: {},
46122
+ selectSizeLabelText: {
46123
+ fontFamily: "'Inter', sans-serif",
46124
+ fontWeight: 300
46125
+ },
45937
46126
  sizeSelectorContainer: {
45938
46127
  marginTop: "24px"
45939
46128
  },
45940
46129
  itemFitDetailsContainer: {
45941
46130
  marginTop: "24px",
45942
- width: "100%"
46131
+ width: "100%",
46132
+ // Cascades into <ItemFitDetails>; the bold recommended-size line above
46133
+ // and the size-selector buttons stay at their own weights.
46134
+ fontFamily: "'Inter', sans-serif",
46135
+ fontWeight: 300
45943
46136
  },
45944
46137
  fitChartContainer: {
45945
46138
  marginTop: "16px"
@@ -46004,6 +46197,7 @@ function Avatar({
46004
46197
  });
46005
46198
  const [selectedFrameIndex, setSelectedFrameIndex] = reactExports.useState(null);
46006
46199
  const [zoomOpen, setZoomOpen] = reactExports.useState(false);
46200
+ const cancelAutoRotate = useAutoRotate(1, frameUrls, selectedFrameIndex, setSelectedFrameIndex);
46007
46201
  const css2 = useCss((theme) => ({
46008
46202
  topContainer: {
46009
46203
  flex: "none",
@@ -46013,8 +46207,11 @@ function Avatar({
46013
46207
  },
46014
46208
  zoomPill: {
46015
46209
  position: "absolute",
46016
- // Bottom-right of the avatar image, clear of the slider gutter below it.
46017
- bottom: `${AVATAR_GUTTER_HEIGHT_PX + 16}px`,
46210
+ // Bottom-right of the avatar image, 16px above the image's bottom edge.
46211
+ // When the rotation slider is on, that's also `clear of the gutter` —
46212
+ // when the slider is hidden, the gutter collapses to 0 and the 16px
46213
+ // offset alone keeps the pill inset from the image's true bottom.
46214
+ bottom: `${DESKTOP_AVATAR_GUTTER_HEIGHT_PX + 16}px`,
46018
46215
  right: "16px",
46019
46216
  display: "inline-flex",
46020
46217
  alignItems: "center",
@@ -46028,6 +46225,10 @@ function Avatar({
46028
46225
  letterSpacing: "0.5px",
46029
46226
  textTransform: "uppercase",
46030
46227
  cursor: "pointer",
46228
+ // Rapid clicks on the rotation chevrons can otherwise spill a triple-
46229
+ // click selection into this label.
46230
+ userSelect: "none",
46231
+ WebkitUserSelect: "none",
46031
46232
  zIndex: 2
46032
46233
  },
46033
46234
  zoomPillIcon: {
@@ -46104,7 +46305,7 @@ function Avatar({
46104
46305
  }
46105
46306
  } else {
46106
46307
  const screenHeightPx = window.innerHeight;
46107
- const bottomContainerHeightPx = AVATAR_GUTTER_HEIGHT_PX;
46308
+ const bottomContainerHeightPx = DESKTOP_AVATAR_GUTTER_HEIGHT_PX;
46108
46309
  const imageHeightPx = screenHeightPx - bottomContainerHeightPx;
46109
46310
  const imageWidthPx = Math.floor(imageHeightPx * AVATAR_IMAGE_ASPECT_RATIO);
46110
46311
  const modalWidthPx = imageWidthPx + CONTENT_AREA_WIDTH_PX;
@@ -46144,16 +46345,16 @@ function Avatar({
46144
46345
  return () => {
46145
46346
  window.removeEventListener("resize", refreshLayoutData);
46146
46347
  };
46147
- }, [isMobileLayout]);
46348
+ }, [isMobileLayout, setModalStyle]);
46148
46349
  const isReady = !!frameUrls && selectedFrameIndex != null;
46149
46350
  return /* @__PURE__ */ jsxs("div", { css: css2.topContainer, style: layoutData.topContainerStyle, children: [
46150
- /* @__PURE__ */ jsx$1(AvatarFrameViewer, { frameUrls, selectedFrameIndex, setSelectedFrameIndex, imageContainerStyle: layoutData.imageContainerStyle, imageStyle: layoutData.imageStyle, loadingT: "quick-view.avatar_loading" }),
46351
+ /* @__PURE__ */ jsx$1(AvatarFrameViewer, { frameUrls, selectedFrameIndex, setSelectedFrameIndex, imageContainerStyle: layoutData.imageContainerStyle, imageStyle: layoutData.imageStyle, loadingT: "quick-view.avatar_loading", onUserInteract: cancelAutoRotate }),
46151
46352
  isReady && !isMobileLayout ? /* @__PURE__ */ jsxs(Button, { variant: "base", css: css2.zoomPill, onClick: () => setZoomOpen(true), children: [
46152
46353
  /* @__PURE__ */ jsx$1(SvgIconZoom, { css: css2.zoomPillIcon }),
46153
46354
  /* @__PURE__ */ jsx$1(TextT, { variant: "base", t: "quick-view.zoom_in" })
46154
46355
  ] }) : null,
46155
46356
  zoomOpen && frameUrls && frameUrls.length > 0 ? /* @__PURE__ */ jsx$1(ZoomModal, { frameUrls, selectedFrameIndex, setSelectedFrameIndex, onClose: () => setZoomOpen(false) }) : null,
46156
- frameUrls && selectedFrameIndex != null ? /* @__PURE__ */ jsx$1("div", { css: css2.bottomContainer, style: layoutData.bottomContainerStyle, children: isMobileLayout ? /* @__PURE__ */ jsx$1(Fragment, { children: " " }) : /* @__PURE__ */ jsxs(Fragment, { children: [
46357
+ 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: [
46157
46358
  /* @__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 }),
46158
46359
  /* @__PURE__ */ jsx$1(TextT, { variant: "base", t: "quick-view.slide_to_rotate", css: css2.sliderText })
46159
46360
  ] }) }) : null
@@ -46173,10 +46374,14 @@ function ProductSummaryRow({
46173
46374
  },
46174
46375
  labelContainer: {},
46175
46376
  labelText: {
46377
+ fontFamily: "'Times New Roman', serif",
46378
+ fontSize: "16px",
46176
46379
  color: "#1A1A1A"
46177
46380
  },
46178
46381
  nameContainer: {},
46179
46382
  nameText: {
46383
+ fontFamily: "'Times New Roman', serif",
46384
+ fontSize: "16px",
46180
46385
  color: "#9F9F9F"
46181
46386
  }
46182
46387
  }));
@@ -46677,28 +46882,41 @@ const useMainStore = create((set) => ({
46677
46882
  }
46678
46883
  })),
46679
46884
  // Fitting room:
46885
+ //
46886
+ // Each mutation reads the latest localStorage state before applying the
46887
+ // change, so two tabs adding different products at the same time merge
46888
+ // instead of last-write-wins. The in-memory Zustand value is just a
46889
+ // cache of "what was in localStorage the last time we touched it".
46890
+ // Cross-tab UI freshness (Tab B's open fitting-room sees Tab A's add
46891
+ // without a mutation of its own) is handled by the `storage` event
46892
+ // listener registered in fitting-room-storage.ts::_init.
46680
46893
  fittingRoom: [],
46681
- addToFittingRoom: (item) => set((prevState) => {
46682
- const filtered = prevState.fittingRoom.filter((existing) => existing.externalId !== item.externalId);
46683
- const next2 = [...filtered, item];
46684
- writeFittingRoom(getStaticData().brandId, next2);
46894
+ addToFittingRoom: (item) => set(() => {
46895
+ const brandId = getStaticData().brandId;
46896
+ const fresh = readFittingRoom(brandId);
46897
+ const next2 = [...fresh.filter((existing) => existing.externalId !== item.externalId), item];
46898
+ writeFittingRoom(brandId, next2);
46685
46899
  return {
46686
46900
  fittingRoom: next2
46687
46901
  };
46688
46902
  }),
46689
- removeFromFittingRoom: (externalId) => set((prevState) => {
46690
- const next2 = prevState.fittingRoom.filter((existing) => existing.externalId !== externalId);
46691
- writeFittingRoom(getStaticData().brandId, next2);
46903
+ removeFromFittingRoom: (externalId) => set(() => {
46904
+ const brandId = getStaticData().brandId;
46905
+ const fresh = readFittingRoom(brandId);
46906
+ const next2 = fresh.filter((existing) => existing.externalId !== externalId);
46907
+ writeFittingRoom(brandId, next2);
46692
46908
  return {
46693
46909
  fittingRoom: next2
46694
46910
  };
46695
46911
  }),
46696
- updateFittingRoomItem: (externalId, patch) => set((prevState) => {
46697
- const next2 = prevState.fittingRoom.map((existing) => existing.externalId === externalId ? {
46912
+ updateFittingRoomItem: (externalId, patch) => set(() => {
46913
+ const brandId = getStaticData().brandId;
46914
+ const fresh = readFittingRoom(brandId);
46915
+ const next2 = fresh.map((existing) => existing.externalId === externalId ? {
46698
46916
  ...existing,
46699
46917
  ...patch
46700
46918
  } : existing);
46701
- writeFittingRoom(getStaticData().brandId, next2);
46919
+ writeFittingRoom(brandId, next2);
46702
46920
  return {
46703
46921
  fittingRoom: next2
46704
46922
  };
@@ -46765,9 +46983,9 @@ const SHARED_CONFIG = {
46765
46983
  appGooglePlayUrl: "https://play.google.com/store/apps/details?id=com.thefittingroom.marketplace"
46766
46984
  },
46767
46985
  build: {
46768
- version: `${"5.0.25"}`,
46769
- commitHash: `${"101d833"}`,
46770
- date: `${"2026-05-22T00:57:54.147Z"}`
46986
+ version: `${"5.0.27"}`,
46987
+ commitHash: `${"9e3e717"}`,
46988
+ date: `${"2026-05-24T21:00:26.048Z"}`
46771
46989
  }
46772
46990
  };
46773
46991
  const CONFIGS = {
@@ -46891,7 +47109,7 @@ const CONFIGS = {
46891
47109
  const getConfig = (envName) => {
46892
47110
  return CONFIGS[envName];
46893
47111
  };
46894
- const css = "\n@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');\n/* Inter — the SDK's own typeface, matching the Figma designs. Loaded here so\n overlay text renders in Inter regardless of the host page's fonts. */\nbody.tfr-modal-open {\n overflow: hidden;\n position: fixed;\n width: 100%;\n}\n";
47112
+ const css = "\n@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');\n/* Inter — the SDK's own typeface, matching the Figma designs. Loaded here so\n overlay text renders in Inter regardless of the host page's fonts. The\n variable-axis URL serves every integer weight from 100 to 900 in one file\n so any `font-weight: <n>` value renders accurately without falling back to\n the nearest pre-loaded weight. */\nbody.tfr-modal-open {\n overflow: hidden;\n position: fixed;\n width: 100%;\n}\n";
46895
47113
  class TfrWidgetElement extends HTMLElement {
46896
47114
  connectedCallback() {
46897
47115
  const attributes = this.getAttributeNames().reduce((attrs, name2) => {
@@ -46987,10 +47205,12 @@ async function logout() {
46987
47205
  }
46988
47206
  const TFR = {
46989
47207
  init,
46990
- logout
47208
+ logout,
47209
+ syncCurrentProductSelection
46991
47210
  };
46992
47211
  export {
46993
47212
  TFR as default,
46994
47213
  init,
46995
- logout
47214
+ logout,
47215
+ syncCurrentProductSelection
46996
47216
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thefittingroom/shop-ui",
3
- "version": "5.0.25",
3
+ "version": "5.0.27",
4
4
  "description": "the fitting room UI library",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",