@osimart/create-storefront 1.0.21 → 1.0.22

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.
@@ -115,6 +115,8 @@ const DEFAULTS = {
115
115
 
116
116
  // cart
117
117
  HEADER_CART_COUNT: "BADGE",
118
+ HEADER_CART_BADGE_BG: null,
119
+ HEADER_CART_BADGE_TEXT: null,
118
120
 
119
121
  // chrome
120
122
  HEADER_SHOW_ACCOUNT: true,
@@ -453,6 +455,8 @@ const DEFAULTS = {
453
455
  CART_DRAWER_EMPTY_BTN_STYLE: "THEME",
454
456
  CART_DRAWER_RECS_ADD_STYLE: "THEME",
455
457
  CART_DRAWER_QTY_SIZE: 30,
458
+ CART_DRAWER_SHOW_REMOVE: true,
459
+ CART_DRAWER_REMOVE_COLOR: null,
456
460
  CART_DRAWER_RECS_CARD_BACKGROUND: null,
457
461
  CART_DRAWER_RECS_PRICE_POSITION: "BELOW",
458
462
  CART_DRAWER_RECS_NAME_LINES: 2,
@@ -438,9 +438,21 @@ export function useContentTree(section, options = {}) {
438
438
  if (bleed) {
439
439
  for (const [k, val] of Object.entries(paddingOf(v))) box[k.replace("--cb-p", "--card-p")] = val;
440
440
  }
441
+ // Quick buy's colour swatches follow the card's Swatches block.
442
+ const findSw = (list) => {
443
+ for (const c of list ?? []) {
444
+ if (c.type === "CARD_SWATCHES") return c;
445
+ const hit = findSw(c.children);
446
+ if (hit) return hit;
447
+ }
448
+ return null;
449
+ };
450
+ const swatchRadius = findSw(children)?.swRadius;
451
+ if (swatchRadius) kids = kids.map((c) => (c.type === "CARD_IMAGE" ? { ...c, swatchRadius } : c));
441
452
  return {
442
453
  ...base,
443
454
  ...flexOf(v),
455
+ swatchRadius,
444
456
  children: kids,
445
457
  sizeClasses: ["w-fill"],
446
458
  sizeVars: {},
@@ -579,9 +591,15 @@ export function useContentTree(section, options = {}) {
579
591
  box: boxOf(v, { background: false }),
580
592
  max: num(v.MAX, 5),
581
593
  square: upper(v.SHAPE, "CIRCLE") === "SQUARE",
594
+ // Own radius (0 = sharp) over the shape's; the card's quick buy uses it too.
595
+ swRadius: has(v.RADIUS) ? `${num(v.RADIUS, 0)}px` : upper(v.SHAPE, "CIRCLE") === "SQUARE" ? "2px" : undefined,
582
596
  // COLOR: the colour option only; ALL: every option (sizes as text).
583
597
  show: upper(v.SHOW, "ALL"),
584
- swatchVars: { "--cb-sw": `${num(v.SIZE, 16)}px`, "--cb-sw-gap": `${num(v.GAP, 6)}px` },
598
+ swatchVars: {
599
+ "--cb-sw": `${num(v.SIZE, 16)}px`,
600
+ "--cb-sw-gap": `${num(v.GAP, 6)}px`,
601
+ ...(has(v.RADIUS) ? { "--cb-sw-r": `${num(v.RADIUS, 0)}px` } : {}),
602
+ },
585
603
  };
586
604
 
587
605
  case "CARD_BUTTON":
@@ -753,8 +771,16 @@ export function useContentTree(section, options = {}) {
753
771
  return {
754
772
  ...base,
755
773
  ...sizeOf(v, "WIDTH", "WIDTH_MOBILE", "FILL"),
756
- box: boxOf(v, { background: false }),
774
+ // Background too: their Text-look settings carry BG_COLOR (Title,
775
+ // Description, Breadcrumbs…); blocks without it just leave it unset.
776
+ box: boxOf(v),
757
777
  ...(textDefault ? textLook(v, textDefault) : {}),
778
+ // Text parts always fill their row (no Width setting), so the text box
779
+ // — capped by MAX_WIDTH — would sit at the start even with the section
780
+ // centred. ContentNode lines it up instead: the block's own Alignment,
781
+ // else its group's (the inherited --cb-col-align).
782
+ textPart: !!textDefault,
783
+ selfVars: textDefault ? compact({ "--cb-self-align": COL_ALIGN[upper(v.ALIGNMENT, "")] }) : {},
758
784
  component: markRaw(component),
759
785
  };
760
786
  }
@@ -1,3 +1,4 @@
1
+ import { isColorType } from "#osi/composables/useProductVariants";
1
2
  /**
2
3
  * PRODUCT_LIST's items — products in the one shape the card template reads.
3
4
  *
@@ -147,7 +148,8 @@ export function useProductItems(section, products, options = {}) {
147
148
  return Number(product.remaining_stock) <= 0;
148
149
  }
149
150
 
150
- const isColorName = (name) => ["color", "colour"].includes(String(name).toLowerCase());
151
+ // By the option's display_type (the name only for older rows).
152
+ const isColorOption = (type) => isColorType(type);
151
153
 
152
154
  /**
153
155
  * Every option (Color, Size, …) with the values that have a variant behind
@@ -163,10 +165,10 @@ export function useProductItems(section, products, options = {}) {
163
165
  const value = (variant.values ?? []).find((v) => v.type === type.id);
164
166
  if (!value || seen.has(value.id)) continue;
165
167
  seen.add(value.id);
166
- const color = isColorName(type.name) ? value.hex_code || "" : "";
168
+ const color = isColorOption(type) ? value.hex_code || "" : "";
167
169
  values.push({ key: value.id, name: value.name || value.value || "", color });
168
170
  }
169
- return { key: type.id, name: type.name, isColor: isColorName(type.name), values };
171
+ return { key: type.id, name: type.name, isColor: isColorOption(type), values };
170
172
  })
171
173
  .filter((option) => option.values.length);
172
174
  }
@@ -17,6 +17,37 @@
17
17
  * @param {object|Ref|(() => object)} product pass a getter — the card is
18
18
  * rendered in a v-for and the product prop changes identity on re-fetch.
19
19
  */
20
+ /**
21
+ * Is this option (a vary_by type / a filter) drawn as colour swatches?
22
+ * Its display_type decides ("COLOR" — case doesn't matter). Only a row from an
23
+ * older API with no display_type at all falls back to the name (Color /
24
+ * Colour), so a type named "Color" set to buttons shows buttons.
25
+ */
26
+ export function isColorType(type) {
27
+ const display = type?.display_type;
28
+ if (display !== undefined && display !== null && display !== "") {
29
+ return String(display).toLowerCase() === "color";
30
+ }
31
+ return ["color", "colour"].includes(String(type?.name ?? "").toLowerCase());
32
+ }
33
+
34
+ /**
35
+ * Sold out for a picked variant — the product page's rule: the variant's own
36
+ * stock decides once one is picked (one that doesn't track inventory is always
37
+ * available); before that, the product's status, then its stock.
38
+ */
39
+ export function isVariantSoldOut(product, variant = null) {
40
+ const out = (n) => n !== null && n !== undefined && Number(n) <= 0;
41
+ if (variant?.id) {
42
+ if (!variant.track_inventory) return false;
43
+ return out(variant.remaining_stock);
44
+ }
45
+ const status = product?.status;
46
+ if (status === "In Stock" || status === "On Demand") return false;
47
+ if (status === "Out of Stock") return true;
48
+ return out(product?.remaining_stock);
49
+ }
50
+
20
51
  export function useProductVariants(product) {
21
52
  const p = computed(() => toValue(product) ?? {});
22
53
 
@@ -32,7 +63,7 @@ export function useProductVariants(product) {
32
63
  );
33
64
 
34
65
  const colorType = computed(
35
- () => variantTypes.value.find((type) => type.name === "Color") ?? null
66
+ () => variantTypes.value.find((type) => isColorType(type)) ?? null
36
67
  );
37
68
 
38
69
  /** Distinct colour values that actually have a variant behind them. */
@@ -114,6 +114,7 @@ export default function useLogic() {
114
114
  if (s.CART_DRAWER_RECS_CARD_BACKGROUND) style["--recs-card-bg"] = s.CART_DRAWER_RECS_CARD_BACKGROUND;
115
115
  if (s.CART_DRAWER_RECS_PRICE_COLOR) style["--recs-price-color"] = s.CART_DRAWER_RECS_PRICE_COLOR;
116
116
  if (s.CART_DRAWER_RECS_LINE_COLOR) style["--recs-line"] = s.CART_DRAWER_RECS_LINE_COLOR;
117
+ if (s.CART_DRAWER_REMOVE_COLOR) style["--cart-remove-color"] = s.CART_DRAWER_REMOVE_COLOR;
117
118
  // Quantity buttons (inside each cart line): picked up by .osi-qty-btn below.
118
119
  const q = buttonVars("CART_DRAWER_QTY");
119
120
  for (const [k, v] of Object.entries(q)) style[k.replace("--btn-", "--qty-")] = v;
@@ -137,6 +138,8 @@ export default function useLogic() {
137
138
  const increase = (item) => edit(increaseItem, item);
138
139
  const decrease = (item) => edit(decreaseItem, item);
139
140
  const remove = (item) => edit(removeItem, item);
141
+ // Unset (older themes) counts as shown.
142
+ const showRemove = computed(() => settings.value.CART_DRAWER_SHOW_REMOVE !== false);
140
143
 
141
144
  function close() {
142
145
  toggleCartSidebar(false);
@@ -261,5 +264,5 @@ export default function useLogic() {
261
264
  // rely on a page's onMounted to load the cart.
262
265
  onMounted(fetchCart);
263
266
 
264
- return { settings, t, TEXT_DEFAULTS, txt, scheme, isCartSidebarVisible, toggleCartSidebar, cartItems, topLevelItems, childrenOf, cartTotal, count, fetchCart, showApiError, mediaUrl, productUrl, handleAddToCart, store, currency, money, px, has, VARIANTS, buttonVars, button, checkoutBtn, emptyBtn, addBtn, cardStyle, RATIOS, rootStyle, isUpdating, edit, increase, decrease, remove, close, go, threshold, shipping, recs, recsLoading, recsKey, pickedIds, limit, loadRecs, inCart, shownRecs, recsLayout, pricePos, adding, addRec, priceOf, analytics };
267
+ return { settings, t, TEXT_DEFAULTS, txt, scheme, isCartSidebarVisible, toggleCartSidebar, cartItems, topLevelItems, childrenOf, cartTotal, count, fetchCart, showApiError, mediaUrl, productUrl, handleAddToCart, store, currency, money, px, has, VARIANTS, buttonVars, button, checkoutBtn, emptyBtn, addBtn, cardStyle, RATIOS, rootStyle, isUpdating, edit, increase, decrease, remove, showRemove, close, go, threshold, shipping, recs, recsLoading, recsKey, pickedIds, limit, loadRecs, inCart, shownRecs, recsLayout, pricePos, adding, addRec, priceOf, analytics };
265
268
  }
@@ -1,3 +1,4 @@
1
+ import { isColorType } from "#osi/composables/useProductVariants";
1
2
  import { useProductDetail } from "#osi/composables/useProductDetail";
2
3
 
3
4
  export default function useLogic({ props }) {
@@ -30,7 +31,10 @@ export default function useLogic({ props }) {
30
31
  const pickerStyle = computed(() =>
31
32
  compact({
32
33
  "--sw-size": px(s.value.PICKER_SWATCH_SIZE) ?? "32px",
33
- "--sw-radius": { SQUARE: "2px", ROUNDED: "8px" }[s.value.PICKER_SWATCH_SHAPE] ?? "999px",
34
+ // Own radius first (0 = sharp), else the shape's.
35
+ "--sw-radius":
36
+ px(s.value.PICKER_SWATCH_RADIUS) ??
37
+ ({ SQUARE: "2px", ROUNDED: "8px" }[s.value.PICKER_SWATCH_SHAPE] ?? "999px"),
34
38
  "--sw-ring": s.value.PICKER_SWATCH_RING_COLOR,
35
39
  "--pk-gap": px(s.value.PICKER_GAP) ?? "8px",
36
40
  "--pk-option-gap": px(s.value.PICKER_OPTION_GAP) ?? "16px",
@@ -53,7 +57,8 @@ export default function useLogic({ props }) {
53
57
  })
54
58
  );
55
59
 
56
- const isColor = (type) => ["color", "colour"].includes(String(type?.name ?? "").toLowerCase());
60
+ // Swatches by the option's display_type (the name only for older rows).
61
+ const isColor = (type) => isColorType(type);
57
62
 
58
63
  /** How one option type is drawn. */
59
64
  function modeFor(type) {
@@ -16,6 +16,7 @@
16
16
  *
17
17
  * Every click inside stops at the panel: the card around it is a link.
18
18
  */
19
+ import { isColorType, isVariantSoldOut } from "#osi/composables/useProductVariants";
19
20
 
20
21
 
21
22
  export default function useLogic({ props, emit }) {
@@ -86,9 +87,13 @@ export default function useLogic({ props, emit }) {
86
87
 
87
88
  /* ------------------------------------------------------------------- details */
88
89
 
89
- const isColor = (type) => ["color", "colour"].includes(String(type.name).toLowerCase());
90
+ // Swatches by the option's display_type, like the product page.
91
+ const isColor = (type) => isColorType(type);
90
92
  const selected = computed(() => variants.selectedVariant.value);
91
- const soldOut = computed(() => variants.stockFor(selected.value));
93
+ // The product page's rule: a picked combination that's out of stock is sold
94
+ // out even when the product as a whole is "In Stock".
95
+ const soldOut = computed(() => isVariantSoldOut(props.product, selected.value));
96
+ const comingSoon = computed(() => !!props.product?.coming_soon);
92
97
  // The picked option has no price: shown as unavailable, can't be added.
93
98
  const noPrice = computed(() => !!selected.value && !(Number(selected.value.price) > 0));
94
99
  const currency = computed(
@@ -178,6 +183,7 @@ export default function useLogic({ props, emit }) {
178
183
  quickShop.flashError("That combination isn't available.");
179
184
  return;
180
185
  }
186
+ if (soldOut.value || comingSoon.value) return;
181
187
  await quickShop.addVariant(selected.value, qty.value);
182
188
  }
183
189
 
@@ -185,10 +191,12 @@ export default function useLogic({ props, emit }) {
185
191
  quickShop.errorMessage.value ||
186
192
  (quickShop.adding.value
187
193
  ? "Adding…"
188
- : soldOut.value
189
- ? "Sold out"
190
- : "Add to cart") // no price: same label, the button is disabled
194
+ : comingSoon.value
195
+ ? "Coming soon"
196
+ : soldOut.value
197
+ ? "Out of stock"
198
+ : "Add to cart") // no price: same label, the button is disabled
191
199
  );
192
200
 
193
- return { isMobile, cartType, store, toggleCartSidebar, mediaUrl, router, variants, quickShop, shown, locked, afterClose, inline, popup, close, onAfterLeave, onKey, closeBtn, onAfterEnter, isColor, selected, soldOut, noPrice, currency, money, price, pickedName, images, track, slide, onScroll, goTo, description, descEl, descLong, descOpen, measureDesc, step, qty, dec, inc, add, ctaLabel };
201
+ return { isMobile, cartType, store, toggleCartSidebar, mediaUrl, router, variants, quickShop, shown, locked, afterClose, inline, popup, close, onAfterLeave, onKey, closeBtn, onAfterEnter, isColor, selected, soldOut, comingSoon, noPrice, currency, money, price, pickedName, images, track, slide, onScroll, goTo, description, descEl, descLong, descOpen, measureDesc, step, qty, dec, inc, add, ctaLabel };
194
202
  }
@@ -144,6 +144,8 @@ export default function useLogic() {
144
144
  style["--osi-header-icon"] = s.HEADER_ICON_COLOR || "var(--osi-text)";
145
145
  if (s.HEADER_ICON_BG) style["--osi-header-icon-bg"] = s.HEADER_ICON_BG;
146
146
  if (s.HEADER_ICON_BORDER) style["--osi-header-icon-border"] = s.HEADER_ICON_BORDER;
147
+ if (s.HEADER_CART_BADGE_BG) style["--osi-header-badge-bg"] = s.HEADER_CART_BADGE_BG;
148
+ if (s.HEADER_CART_BADGE_TEXT) style["--osi-header-badge-text"] = s.HEADER_CART_BADGE_TEXT;
147
149
  style["--osi-header-pad-y"] = `${s.HEADER_PADDING_Y}px`;
148
150
  // Side padding lives on the header itself, not on the inner box — see the
149
151
  // note on .osi-header-inner below. 16 is COMMON_STYLE_SETTINGS' PADDING_X
@@ -332,6 +332,8 @@ export default function useLogic({ props }) {
332
332
 
333
333
  // filters
334
334
  "--pf-swatch": px(num(s.value.SWATCH_SIZE, 28)),
335
+ // Empty: the shape's radius (circle / square).
336
+ "--pf-swatch-r": px(s.value.SWATCH_RADIUS),
335
337
 
336
338
  // grid
337
339
  "--plp-cols": effectiveColumns.value,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osimart/create-storefront",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "Osimart storefront platform — logic, platform pages and CLI; extended by every theme repo",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",