@thefittingroom/shop-ui 5.0.29 → 5.0.31

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 +174 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -42073,14 +42073,38 @@ function pairCompatible(a, b, group) {
42073
42073
  }
42074
42074
  return aIncl.includes(bName) || bIncl.includes(aName);
42075
42075
  }
42076
+ function getSameCategoryConflicts(item, selectedExternalIds, resolved) {
42077
+ if (!item.styleCategory) {
42078
+ return [];
42079
+ }
42080
+ const itemName = catName(item.styleCategory);
42081
+ const out = [];
42082
+ for (const sel of resolved.items) {
42083
+ if (sel.externalId === item.externalId) {
42084
+ continue;
42085
+ }
42086
+ if (!selectedExternalIds.has(sel.externalId)) {
42087
+ continue;
42088
+ }
42089
+ if (!sel.styleCategory) {
42090
+ continue;
42091
+ }
42092
+ if (catName(sel.styleCategory) === itemName) {
42093
+ out.push(sel.externalId);
42094
+ }
42095
+ }
42096
+ return out;
42097
+ }
42076
42098
  function computeAvailability(item, selectedExternalIds, resolved) {
42077
42099
  if (selectedExternalIds.has(item.externalId)) {
42078
42100
  return "selected";
42079
42101
  }
42080
- if (selectedExternalIds.size >= MAX_OUTFIT_ITEMS) {
42102
+ if (!item.styleCategory) {
42081
42103
  return "disabled";
42082
42104
  }
42083
- if (!item.styleCategory) {
42105
+ const sameCategoryEvictions = new Set(getSameCategoryConflicts(item, selectedExternalIds, resolved));
42106
+ const effectiveSize = selectedExternalIds.size - sameCategoryEvictions.size + 1;
42107
+ if (effectiveSize > MAX_OUTFIT_ITEMS) {
42084
42108
  return "disabled";
42085
42109
  }
42086
42110
  const itemCat = item.styleCategory;
@@ -42088,6 +42112,9 @@ function computeAvailability(item, selectedExternalIds, resolved) {
42088
42112
  if (!selectedExternalIds.has(sel.externalId)) {
42089
42113
  continue;
42090
42114
  }
42115
+ if (sameCategoryEvictions.has(sel.externalId)) {
42116
+ continue;
42117
+ }
42091
42118
  if (!sel.styleCategory) {
42092
42119
  continue;
42093
42120
  }
@@ -42680,11 +42707,77 @@ function Chevron({
42680
42707
  transform: `rotate(${ROTATION_DEG[direction]}deg)`
42681
42708
  }, children: /* @__PURE__ */ jsx$1("path", { d: "M6 9L12 15L18 9", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) });
42682
42709
  }
42710
+ function ColorSwatchRow({
42711
+ colors,
42712
+ selectedLabel,
42713
+ onSelect
42714
+ }) {
42715
+ const css2 = useCss((theme) => ({
42716
+ row: {
42717
+ display: "flex",
42718
+ flexWrap: "wrap",
42719
+ gap: "6px"
42720
+ },
42721
+ swatch: {
42722
+ width: "24px",
42723
+ height: "24px",
42724
+ borderRadius: "50%",
42725
+ padding: 0,
42726
+ border: "1px solid rgba(0, 0, 0, 0.15)",
42727
+ backgroundColor: "#FFFFFF",
42728
+ cursor: "pointer",
42729
+ overflow: "hidden",
42730
+ display: "inline-flex",
42731
+ alignItems: "center",
42732
+ justifyContent: "center",
42733
+ // Reset native button chrome so the circle renders crisply.
42734
+ appearance: "none",
42735
+ WebkitAppearance: "none"
42736
+ },
42737
+ swatchSelected: {
42738
+ outline: `2px solid ${theme.color_fg_text}`,
42739
+ outlineOffset: "1px",
42740
+ borderColor: "transparent"
42741
+ },
42742
+ swatchImage: {
42743
+ width: "100%",
42744
+ height: "100%",
42745
+ objectFit: "cover"
42746
+ },
42747
+ // Tiny text label, used only as a last-resort fallback when neither an
42748
+ // image nor a hex is available. Truncates so a long colour name
42749
+ // doesn't break the swatch shape.
42750
+ swatchTextLabel: {
42751
+ fontSize: "9px",
42752
+ lineHeight: 1,
42753
+ color: theme.color_fg_text,
42754
+ padding: "0 2px",
42755
+ overflow: "hidden",
42756
+ textOverflow: "ellipsis",
42757
+ whiteSpace: "nowrap",
42758
+ maxWidth: "100%"
42759
+ }
42760
+ }));
42761
+ if (colors.length < 2) {
42762
+ return null;
42763
+ }
42764
+ return /* @__PURE__ */ jsx$1("div", { css: css2.row, children: colors.map((c) => {
42765
+ const isSelected = c.label === selectedLabel;
42766
+ const fillStyle = c.imageUrl ? void 0 : c.hex ? {
42767
+ backgroundColor: c.hex
42768
+ } : void 0;
42769
+ return /* @__PURE__ */ jsx$1(Button, { variant: "base", css: isSelected ? {
42770
+ ...css2.swatch,
42771
+ ...css2.swatchSelected
42772
+ } : css2.swatch, style: fillStyle, onClick: () => onSelect(c.label), "aria-label": `Pick colour ${c.label}`, "aria-pressed": isSelected, children: c.imageUrl ? /* @__PURE__ */ jsx$1("img", { src: c.imageUrl, alt: "", css: css2.swatchImage }) : c.hex ? null : /* @__PURE__ */ jsx$1("span", { css: css2.swatchTextLabel, children: c.label.slice(0, 3) }) }, c.label);
42773
+ }) });
42774
+ }
42683
42775
  function ProductCard({
42684
42776
  item,
42685
42777
  availability,
42686
42778
  onClick,
42687
- onRemove
42779
+ onRemove,
42780
+ onChangeColor
42688
42781
  }) {
42689
42782
  const css2 = useCss((theme) => ({
42690
42783
  container: {
@@ -42758,6 +42851,24 @@ function ProductCard({
42758
42851
  removeIcon: {
42759
42852
  width: "12px",
42760
42853
  height: "12px"
42854
+ },
42855
+ // Selected badge — mirrors the X button at the top-right, green-filled
42856
+ // circle at top-left with an inline white checkmark. Only rendered when
42857
+ // availability === 'selected'. Decorative: pointer-events: none so the
42858
+ // shopper still taps the card body underneath to toggle off.
42859
+ selectedBadge: {
42860
+ position: "absolute",
42861
+ top: "4px",
42862
+ left: "4px",
42863
+ width: "24px",
42864
+ height: "24px",
42865
+ borderRadius: "12px",
42866
+ backgroundColor: "#22C55E",
42867
+ display: "flex",
42868
+ alignItems: "center",
42869
+ justifyContent: "center",
42870
+ pointerEvents: "none",
42871
+ zIndex: 1
42761
42872
  }
42762
42873
  }));
42763
42874
  const disabled = availability === "disabled";
@@ -42782,6 +42893,29 @@ function ProductCard({
42782
42893
  const selectedVariant = item.merchantProduct?.variants.find((v) => v.color === effectiveColor && (!item.storage.size || v.size === item.storage.size));
42783
42894
  const imageUrl = selectedVariant?.imageUrl ?? item.merchantProduct?.imageUrl ?? null;
42784
42895
  const price = selectedVariant?.priceFormatted ?? item.merchantProduct?.variants[0]?.priceFormatted ?? null;
42896
+ const swatchColors = reactExports.useMemo(() => {
42897
+ const variants = item.merchantProduct?.variants;
42898
+ if (!variants) {
42899
+ return [];
42900
+ }
42901
+ const seen = /* @__PURE__ */ new Set();
42902
+ const out = [];
42903
+ for (const v of variants) {
42904
+ if (!v.color || seen.has(v.color)) {
42905
+ continue;
42906
+ }
42907
+ seen.add(v.color);
42908
+ out.push({
42909
+ label: v.color,
42910
+ imageUrl: v.swatchImageUrl ?? null,
42911
+ hex: v.swatchHex ?? null
42912
+ });
42913
+ }
42914
+ return out;
42915
+ }, [item.merchantProduct?.variants]);
42916
+ const handleSwatchSelect = (label) => {
42917
+ onChangeColor?.(item.externalId, label);
42918
+ };
42785
42919
  return /* @__PURE__ */ jsxs("div", { css: /* @__PURE__ */ css$1({
42786
42920
  ...css2.container,
42787
42921
  ...selected && css2.containerSelected,
@@ -42791,6 +42925,7 @@ function ProductCard({
42791
42925
  e.stopPropagation();
42792
42926
  onRemove();
42793
42927
  }, "aria-label": "Remove from fitting room", children: /* @__PURE__ */ jsx$1(SvgCloseIcon, { css: css2.removeIcon }) }),
42928
+ selected ? /* @__PURE__ */ jsx$1("div", { css: css2.selectedBadge, "aria-hidden": "true", children: /* @__PURE__ */ jsx$1("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx$1("path", { d: "M3 8.5L6.5 12L13 4.5", stroke: "#FFFFFF", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) : null,
42794
42929
  /* @__PURE__ */ jsxs(Button, { variant: "base", css: /* @__PURE__ */ css$1({
42795
42930
  ...css2.cardBody,
42796
42931
  ...disabled && css2.cardBodyDisabled
@@ -42798,14 +42933,17 @@ function ProductCard({
42798
42933
  /* @__PURE__ */ jsx$1("div", { css: css2.imageContainer, children: imageUrl ? /* @__PURE__ */ jsx$1("img", { src: imageUrl, css: css2.image, alt: name2 }) : null }),
42799
42934
  /* @__PURE__ */ jsx$1(Text, { variant: "base", css: css2.nameText, children: name2 }),
42800
42935
  price ? /* @__PURE__ */ jsx$1(Text, { variant: "base", css: css2.priceText, children: price }) : null
42801
- ] })
42936
+ ] }),
42937
+ onChangeColor ? /* @__PURE__ */ jsx$1(ColorSwatchRow, { colors: swatchColors, selectedLabel: effectiveColor, onSelect: handleSwatchSelect }) : null
42802
42938
  ] });
42803
42939
  }
42804
42940
  function CardRail({
42805
42941
  group,
42806
42942
  availabilityByExternalId,
42807
42943
  onSelectItem,
42808
- onRemoveItem
42944
+ onRemoveItem,
42945
+ onChangeColor,
42946
+ sortSelectedFirst
42809
42947
  }) {
42810
42948
  const [collapsed, setCollapsed] = reactExports.useState(false);
42811
42949
  const scrollRef = reactExports.useRef(null);
@@ -42909,7 +43047,12 @@ function CardRail({
42909
43047
  right: 0
42910
43048
  }
42911
43049
  }));
42912
- const cards = group.items.map((item) => /* @__PURE__ */ jsx$1(ProductCard, { item, availability: availabilityByExternalId[item.externalId] ?? "disabled", onClick: () => onSelectItem(item.externalId), onRemove: () => onRemoveItem(item.externalId) }, item.externalId));
43050
+ const orderedItems = sortSelectedFirst ? [...group.items].sort((a, b) => {
43051
+ const aSel = availabilityByExternalId[a.externalId] === "selected" ? 0 : 1;
43052
+ const bSel = availabilityByExternalId[b.externalId] === "selected" ? 0 : 1;
43053
+ return aSel - bSel;
43054
+ }) : group.items;
43055
+ const cards = orderedItems.map((item) => /* @__PURE__ */ jsx$1(ProductCard, { item, availability: availabilityByExternalId[item.externalId] ?? "disabled", onClick: () => onSelectItem(item.externalId), onRemove: () => onRemoveItem(item.externalId), onChangeColor }, item.externalId));
42913
43056
  return /* @__PURE__ */ jsxs("div", { css: css2.container, children: [
42914
43057
  /* @__PURE__ */ jsxs(Button, { variant: "base", css: css2.header, onClick: () => setCollapsed((c) => !c), children: [
42915
43058
  /* @__PURE__ */ jsx$1(Text, { variant: "base", css: css2.headerLabel, children: group.group.label }),
@@ -43776,7 +43919,7 @@ function DesktopLayout$1({
43776
43919
  ...css2.utilityLink,
43777
43920
  ...css2.clearAllWrapper
43778
43921
  }, "", ""), 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)),
43922
+ resolved.groups.map((group) => /* @__PURE__ */ jsx$1(CardRail, { group, availabilityByExternalId, onSelectItem, onRemoveItem, onChangeColor, sortSelectedFirst: true }, group.group.name)),
43780
43923
  /* @__PURE__ */ jsxs("span", { css: /* @__PURE__ */ css$1({
43781
43924
  ...css2.utilityLink,
43782
43925
  ...css2.signOutWrapper
@@ -43920,7 +44063,7 @@ function MobileLayout$1({
43920
44063
  onClearAll
43921
44064
  }) {
43922
44065
  if (mode === "browse") {
43923
- return /* @__PURE__ */ jsx$1(BrowseView, { resolved, availabilityByExternalId, selectedCount: selectedItems.length, onSelectItem, onRemoveItem, onTryItOn, onSignOut, onClearAll });
44066
+ return /* @__PURE__ */ jsx$1(BrowseView, { resolved, availabilityByExternalId, selectedCount: selectedItems.length, onSelectItem, onRemoveItem, onChangeColor, onTryItOn, onSignOut, onClearAll });
43924
44067
  }
43925
44068
  return /* @__PURE__ */ jsx$1(TryOnView, { selectedItems, openAccordionItemId, detailMode, forceUntuck, canTuck, frameUrls, autoRotateTrigger, sheetSnap, sheetTouchStart, onBackToBrowse, onOpenAccordionItem, onChangeDetailMode, onChangeSize, onChangeColor, onAddToCart, onToggleUntuck });
43926
44069
  }
@@ -43930,6 +44073,7 @@ function BrowseView({
43930
44073
  selectedCount,
43931
44074
  onSelectItem,
43932
44075
  onRemoveItem,
44076
+ onChangeColor,
43933
44077
  onTryItOn,
43934
44078
  onSignOut,
43935
44079
  onClearAll
@@ -44056,7 +44200,7 @@ function BrowseView({
44056
44200
  }
44057
44201
  }));
44058
44202
  return /* @__PURE__ */ jsxs("div", { css: css2.container, children: [
44059
- !resolved.isLoading && resolved.groups.length > 0 ? /* @__PURE__ */ jsx$1(SectionNav, { sections, activeName: activeSectionName, onSelect: scrollToSection }) : null,
44203
+ !resolved.isLoading && resolved.groups.length > 1 ? /* @__PURE__ */ jsx$1(SectionNav, { sections, activeName: activeSectionName, onSelect: scrollToSection }) : null,
44060
44204
  /* @__PURE__ */ jsxs("div", { ref: railsAreaRef, css: css2.railsArea, onScroll: recomputeActiveSection, children: [
44061
44205
  resolved.groups.map((group) => /* @__PURE__ */ jsx$1("div", { ref: (el) => {
44062
44206
  if (el) {
@@ -44064,7 +44208,7 @@ function BrowseView({
44064
44208
  } else {
44065
44209
  sectionRefs.current.delete(group.group.name);
44066
44210
  }
44067
- }, children: /* @__PURE__ */ jsx$1(CardRail, { group, availabilityByExternalId, onSelectItem, onRemoveItem }) }, group.group.name)),
44211
+ }, children: /* @__PURE__ */ jsx$1(CardRail, { group, availabilityByExternalId, onSelectItem, onRemoveItem, onChangeColor }) }, group.group.name)),
44068
44212
  /* @__PURE__ */ jsxs("span", { css: css2.signOutWrapper, onClick: onSignOut, children: [
44069
44213
  /* @__PURE__ */ jsx$1(SvgTfrIcon, { css: css2.signOutIcon }),
44070
44214
  /* @__PURE__ */ jsx$1(LinkT, { variant: "underline", css: css2.signOutText, t: "fitting_room.sign_out" })
@@ -44439,6 +44583,12 @@ function FittingRoomOverlay({
44439
44583
  setOpenAccordionItemId(null);
44440
44584
  }
44441
44585
  } else {
44586
+ for (const evictedId of getSameCategoryConflicts(item, selectedExternalIds, resolved)) {
44587
+ nextSelected.delete(evictedId);
44588
+ if (openAccordionItemId === evictedId) {
44589
+ setOpenAccordionItemId(null);
44590
+ }
44591
+ }
44442
44592
  nextSelected.add(externalId);
44443
44593
  ensureSizeForItem(item);
44444
44594
  setLastAddedExternalId(externalId);
@@ -44471,20 +44621,24 @@ function FittingRoomOverlay({
44471
44621
  }, [resolved.items, updateFittingRoomItem]);
44472
44622
  const handleChangeColor = reactExports.useCallback((externalId, colorLabel) => {
44473
44623
  const item = resolved.items.find((i) => i.externalId === externalId);
44474
- if (!item || !item.storage.size) {
44624
+ if (!item) {
44475
44625
  return;
44476
44626
  }
44477
44627
  const productData = buildVtoProductDataFromResolved(item);
44478
44628
  if (!productData) {
44479
44629
  return;
44480
44630
  }
44481
- const csa = findCsaByLabel(productData, item.storage.size, colorLabel);
44631
+ const effectiveSize = item.storage.size ?? productData.recommendedSizeLabel;
44632
+ if (!effectiveSize) {
44633
+ return;
44634
+ }
44635
+ const csa = findCsaByLabel(productData, effectiveSize, colorLabel);
44482
44636
  if (!csa) {
44483
44637
  return;
44484
44638
  }
44485
44639
  updateFittingRoomItem(externalId, {
44486
44640
  colorwaySizeAssetId: csa.colorwaySizeAssetId,
44487
- size: item.storage.size,
44641
+ size: effectiveSize,
44488
44642
  color: csa.colorLabel
44489
44643
  });
44490
44644
  }, [resolved.items, updateFittingRoomItem]);
@@ -44576,13 +44730,16 @@ function FittingRoomOverlay({
44576
44730
  if (outfit.items.length === 0) {
44577
44731
  return;
44578
44732
  }
44733
+ if (isMobileLayout && mobileMode === "browse") {
44734
+ return;
44735
+ }
44579
44736
  requestVtoComposition(toWireItems(outfit.items), true);
44580
44737
  if (getStaticData().config.features.vtoPrefetch) {
44581
44738
  for (const alt of outfit.alternates) {
44582
44739
  requestVtoComposition(toWireItems(alt), false);
44583
44740
  }
44584
44741
  }
44585
- }, [userIsLoggedIn, userHasAvatar, outfit, requestVtoComposition]);
44742
+ }, [userIsLoggedIn, userHasAvatar, isMobileLayout, mobileMode, outfit, requestVtoComposition]);
44586
44743
  const frameUrls = reactExports.useMemo(() => {
44587
44744
  if (outfit.items.length === 0) {
44588
44745
  const bareFrames = userProfile?.avatar_frames;
@@ -47103,9 +47260,9 @@ const SHARED_CONFIG = {
47103
47260
  appGooglePlayUrl: "https://play.google.com/store/apps/details?id=com.thefittingroom.marketplace"
47104
47261
  },
47105
47262
  build: {
47106
- version: `${"5.0.29"}`,
47107
- commitHash: `${"538bbd3"}`,
47108
- date: `${"2026-05-25T01:10:21.610Z"}`
47263
+ version: `${"5.0.31"}`,
47264
+ commitHash: `${"992c5c6"}`,
47265
+ date: `${"2026-06-07T14:43:50.848Z"}`
47109
47266
  }
47110
47267
  };
47111
47268
  const CONFIGS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thefittingroom/shop-ui",
3
- "version": "5.0.29",
3
+ "version": "5.0.31",
4
4
  "description": "the fitting room UI library",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",