@deneb-ui/ui 2.0.126 → 2.0.127

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -637,6 +637,12 @@ export function PromoSection() {
637
637
 
638
638
  DENEB UI commerce components natively support both **Single Fixed Prices** and **Dynamic Price Ranges**, as well as apparel clothing options (color swatches with image swapping and size chips).
639
639
 
640
+ Fivora uses one canonical catalog contract: `ProductCard` is the commerce
641
+ renderer, `ServiceCard` is the service/quote renderer, and `UniversalCard` is
642
+ reserved for other generic entity listings. Keep `imageUrl` as the primary
643
+ image and `additionalImages` as the ordered gallery; DENEB normalizes them to
644
+ `images`/`gallery` for detail and quick-view components.
645
+
640
646
  Keep `price`, `compareAtPrice`, `minPrice`, `maxPrice`, and variant prices as
641
647
  JSON numbers. Keep `currency` or a currency symbol as separate text. A formatted
642
648
  `priceRange` string is display-only; numeric filters and sorting must use the
@@ -653,7 +659,8 @@ import { ProductCard } from "@deneb-ui/ui";
653
659
  price: 3500,
654
660
  compareAtPrice: 4500,
655
661
  currency: "LKR",
656
- imageUrl: "/images/shirt.jpg"
662
+ imageUrl: "/images/shirt.jpg",
663
+ additionalImages: ["/images/shirt-back.jpg", "/images/shirt-detail.jpg"]
657
664
  }}
658
665
  itemPath="products[0]"
659
666
  />
@@ -673,6 +680,7 @@ import { ProductCard } from "@deneb-ui/ui";
673
680
  isPriceRange: true,
674
681
  currency: "LKR",
675
682
  imageUrl: "/images/tunic-navy.jpg",
683
+ additionalImages: ["/images/tunic-back.jpg", "/images/tunic-detail.jpg"],
676
684
  colors: [
677
685
  { name: "Navy Blue", hex: "#1e3a8a", imageUrl: "/images/tunic-navy.jpg" },
678
686
  { name: "Olive Green", hex: "#3f6212", imageUrl: "/images/tunic-olive.jpg" },
@@ -29,6 +29,7 @@ export interface ProductItem {
29
29
  productImage?: string;
30
30
  gallery?: string[];
31
31
  images?: string[];
32
+ additionalImages?: string[];
32
33
  badge?: string;
33
34
  tag?: string;
34
35
  rating?: number | string;
@@ -9,6 +9,7 @@ const EditableImage_1 = require("./EditableImage");
9
9
  const useCart_1 = require("./cart/useCart");
10
10
  const productOptions_1 = require("./utils/productOptions");
11
11
  const PlatformProductDetail_1 = require("./PlatformProductDetail");
12
+ const catalogMedia_1 = require("./utils/catalogMedia");
12
13
  const COLOR_HEX_MAP = {
13
14
  black: '#111827',
14
15
  white: '#ffffff',
@@ -166,14 +167,7 @@ function EditableProductCard({ itemPath, product: productProp, item: itemProp, c
166
167
  const badge = String(product?.badge || product?.tag || '');
167
168
  const hasBadgeField = Boolean(product && ('badge' in product || 'tag' in product));
168
169
  const fallbackImage = imageFallback || 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?auto=format&fit=crop&w=800&q=80';
169
- const rawImage = product?.imageUrl ||
170
- product?.image ||
171
- product?.photo ||
172
- product?.thumbnail ||
173
- product?.productImage ||
174
- (Array.isArray(product?.gallery) && product.gallery[0]) ||
175
- (Array.isArray(product?.images) && product.images[0]);
176
- const baseImageUrl = String(rawImage || fallbackImage);
170
+ const baseImageUrl = (0, catalogMedia_1.resolveCatalogPrimaryImage)(product, fallbackImage);
177
171
  // Active image: clicked color image -> resolved variant image -> product base image
178
172
  const currentImage = colorImage || resolvedPrice.variantImage || baseImageUrl;
179
173
  const imageKey = 'image' in (product || {}) && !('imageUrl' in (product || {}) && product.imageUrl)
@@ -16,6 +16,7 @@ export interface ProductDetailItem {
16
16
  image?: string;
17
17
  gallery?: string[];
18
18
  images?: string[];
19
+ additionalImages?: string[];
19
20
  addToSelectionLabel?: string;
20
21
  specsTitle?: string;
21
22
  specs?: Array<{
@@ -9,6 +9,7 @@ const react_1 = require("react");
9
9
  const utils_1 = require("./utils");
10
10
  const productOptions_1 = require("./utils/productOptions");
11
11
  const urls_1 = require("./utils/urls");
12
+ const catalogMedia_1 = require("./utils/catalogMedia");
12
13
  /**
13
14
  * EditableProductDetail is an elite, fully calibrated Single Product View component
14
15
  * engineered for high-conversion commerce and 100% compliant with Fivora Visual Editing.
@@ -28,14 +29,11 @@ function EditableProductDetail({ sectionPath = 'product', product, sizes, colors
28
29
  const productRecord = (product || {});
29
30
  const productPhoto = productRecord.photo;
30
31
  const productThumbnail = productRecord.thumbnail;
31
- const validImages = (0, react_1.useMemo)(() => {
32
- const rawList = (product.gallery && product.gallery.length > 0)
33
- ? product.gallery
34
- : (product.images && product.images.length > 0)
35
- ? product.images
36
- : [product.featuredImage || product.imageUrl || product.image || productPhoto || productThumbnail || '/products/vanta-aero-x.jpg'];
37
- return rawList.filter(Boolean);
38
- }, [product.gallery, product.images, product.featuredImage, product.imageUrl, product.image, productPhoto, productThumbnail]);
32
+ const validImages = (0, react_1.useMemo)(() => (0, catalogMedia_1.resolveCatalogImages)({
33
+ ...product,
34
+ photo: productPhoto,
35
+ thumbnail: productThumbnail,
36
+ }, '/products/vanta-aero-x.jpg'), [product, productPhoto, productThumbnail]);
39
37
  // 2. Resolve Options (Sizes, Volumes, Weights, Counts)
40
38
  const resolved = (0, productOptions_1.resolveProductOptions)(product);
41
39
  const effectiveOptions = (0, react_1.useMemo)(() => {
@@ -10,6 +10,9 @@ export interface ServiceItem {
10
10
  description?: string;
11
11
  imageUrl?: string;
12
12
  image?: string;
13
+ gallery?: string[];
14
+ images?: string[];
15
+ additionalImages?: string[];
13
16
  features?: unknown[];
14
17
  [key: string]: unknown;
15
18
  }
@@ -4,6 +4,7 @@ exports.EditableServiceCard = EditableServiceCard;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const EditableText_1 = require("./EditableText");
6
6
  const EditableImage_1 = require("./EditableImage");
7
+ const catalogMedia_1 = require("./utils/catalogMedia");
7
8
  /**
8
9
  * EditableServiceCard standardizes service presentation across Home and Services pages.
9
10
  * Supports service name, description, image, and repeatable features checklist.
@@ -28,7 +29,8 @@ function EditableServiceCard({ itemPath, service, imageFallback = '/placeholder.
28
29
  ? String(rawPriceCandidate)
29
30
  : 'LKR ' + String(rawPriceCandidate)
30
31
  : '';
31
- const imageUrl = String(service?.imageUrl || service?.image || '');
32
+ const serviceImages = (0, catalogMedia_1.resolveCatalogImages)(service);
33
+ const imageUrl = serviceImages[0] || '';
32
34
  const features = Array.isArray(service?.features) ? service.features : [];
33
35
  const mergedStyle = {
34
36
  textAlign: align,
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * EditableUniversalCard — DENEB UI Framework
3
3
  *
4
- * Universal, multi-industry listing card for all commerce and business templates:
4
+ * Universal, multi-industry listing renderer for non-catalog entities. Fivora
5
+ * products and services should use EditableProductCard/EditableServiceCard so
6
+ * their backend catalog behavior is preserved:
5
7
  * - 🚗 Automotive & Equipment: Make/model, year & condition, mileage, fuel, gearbox, price
6
8
  * - 📚 Bookshops & Publishers: Cover photo, book title, author, published year, pages, price
7
9
  * - 🏋️ Gyms & Fitness: Membership tier, duration, meal plan, 24/7 access, trainer, price
8
- * - 🍽️ Restaurants & Menus: Dish photo, title, dietary tags, ingredients, prep time, price
10
+ * - 🍽️ Editorial Menus: Dish photo, title, dietary tags, ingredients, prep time, price
9
11
  * - 🏠 Real Estate & Rentals: Property image, location, beds, baths, sqft, price
10
- * - 💼 Services & Agencies: Service title, duration, deliverable checklist, pricing
12
+ * - 💼 Agency Portfolios: Offering title, duration, deliverable checklist, pricing
11
13
  *
12
14
  * Designed with strict editability architecture:
13
15
  * 1. Automatically resolves fields from `useSiteData()` via `itemPath` or explicit field paths.
@@ -9,6 +9,7 @@ const react_1 = require("react");
9
9
  const EditableProductDetail_1 = require("./EditableProductDetail");
10
10
  const SiteDataProvider_1 = require("./SiteDataProvider");
11
11
  const utils_1 = require("./utils");
12
+ const catalogMedia_1 = require("./utils/catalogMedia");
12
13
  const DEFAULT_QUERY_PARAM = 'id';
13
14
  const DEFAULT_DETAIL_ROUTE = '/products/detail/';
14
15
  const DEFAULT_RETRY_DELAYS = [0, 750, 2_000];
@@ -93,13 +94,7 @@ function normalizeProductForDetail(product, siteData) {
93
94
  (typeof mergedProduct.thumbnail === 'string' && mergedProduct.thumbnail) ||
94
95
  undefined;
95
96
  const price = mergedProduct.price ?? mergedProduct.cost ?? mergedProduct.amount ?? mergedProduct.productPrice;
96
- const images = Array.isArray(mergedProduct.gallery)
97
- ? mergedProduct.gallery
98
- : Array.isArray(mergedProduct.images)
99
- ? mergedProduct.images
100
- : Array.isArray(mergedProduct.additionalImages)
101
- ? mergedProduct.additionalImages.filter((value) => typeof value === 'string' && Boolean(value.trim()))
102
- : undefined;
97
+ const images = (0, catalogMedia_1.resolveCatalogImages)(mergedProduct);
103
98
  const compareAtPrice = mergedProduct.compareAtPrice ?? mergedProduct.originalPrice;
104
99
  const shop = (0, SiteDataProvider_1.isRecord)(siteData.shop) ? siteData.shop : {};
105
100
  const merchant = (0, SiteDataProvider_1.isRecord)(siteData.merchant) ? siteData.merchant : {};
@@ -122,7 +117,7 @@ function normalizeProductForDetail(product, siteData) {
122
117
  ? { featuredImage: image, imageUrl: image, image }
123
118
  : {}),
124
119
  ...(price !== undefined ? { price } : {}),
125
- ...(images ? { gallery: images, images } : {}),
120
+ ...(images.length > 0 ? { gallery: images, images } : {}),
126
121
  ...(compareAtPrice !== undefined
127
122
  ? { compareAtPrice, originalPrice: compareAtPrice }
128
123
  : {}),
@@ -9,6 +9,8 @@ export interface ProductQuickViewItem {
9
9
  description?: string;
10
10
  imageUrl?: string;
11
11
  gallery?: string[];
12
+ images?: string[];
13
+ additionalImages?: string[];
12
14
  inStock?: boolean;
13
15
  isAvailable?: boolean;
14
16
  rating?: number;
@@ -5,6 +5,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const utils_1 = require("./utils");
7
7
  const PlatformProductDetail_1 = require("./PlatformProductDetail");
8
+ const catalogMedia_1 = require("./utils/catalogMedia");
8
9
  /**
9
10
  * DENEB UI — Product Quick View Modal
10
11
  *
@@ -20,7 +21,7 @@ function ProductQuickView({ product, isOpen, onClose, onAddToCart, className = '
20
21
  (0, react_1.useEffect)(() => {
21
22
  if (product) {
22
23
  setQuantity(1);
23
- setSelectedImage(product.imageUrl || (product.gallery && product.gallery[0]) || '');
24
+ setSelectedImage((0, catalogMedia_1.resolveCatalogImages)(product)[0] || '');
24
25
  }
25
26
  }, [product]);
26
27
  // Handle ESC key to close
@@ -42,7 +43,7 @@ function ProductQuickView({ product, isOpen, onClose, onAddToCart, className = '
42
43
  if (!isOpen || !product) {
43
44
  return null;
44
45
  }
45
- const images = product.gallery && product.gallery.length > 0 ? product.gallery : [product.imageUrl || ''];
46
+ const images = (0, catalogMedia_1.resolveCatalogImages)(product);
46
47
  const currency = product.currency || '$';
47
48
  const isAvailable = product.isAvailable !== false && product.inStock !== false;
48
49
  const handleAddToCart = () => {
@@ -11,6 +11,7 @@ export declare const LEGACY_PREVIEW_READY_MESSAGE: string;
11
11
  export declare const DENEB_PREVIEW_FOCUS_MESSAGE = "DENEB_PREVIEW_FOCUS_PAGE";
12
12
  export declare const PREVIEW_FOCUS_MESSAGE = "FIVORA_PREVIEW_FOCUS_PAGE";
13
13
  export declare const LEGACY_PREVIEW_FOCUS_MESSAGE: string;
14
+ export declare const DENEB_REFRESH_LIVE_CATALOG_EVENT = "deneb:refresh-live-catalog";
14
15
  export declare const PREVIEW_FIELD_ATTRIBUTE = "data-preview-field-path";
15
16
  export { STYLE_PATCH_MESSAGE, DENEB_STYLE_PATCH_MESSAGE } from '@deneb-ui/core';
16
17
  export type GenericRecord = Record<string, any>;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  'use client';
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.DUMMY_PREVIEW_SHOP_NAME = exports.DUMMY_PREVIEW_ADDRESS = exports.DUMMY_PREVIEW_EMAIL = exports.DUMMY_PREVIEW_WHATSAPP = exports.DUMMY_PREVIEW_PHONE = exports.DenebDataContext = exports.useDenebData = exports.DenebDataProvider = exports.resolveAsset = exports.withBasePath = exports.SiteDataContext = exports.DENEB_STYLE_PATCH_MESSAGE = exports.STYLE_PATCH_MESSAGE = exports.PREVIEW_FIELD_ATTRIBUTE = exports.LEGACY_PREVIEW_FOCUS_MESSAGE = exports.PREVIEW_FOCUS_MESSAGE = exports.DENEB_PREVIEW_FOCUS_MESSAGE = exports.LEGACY_PREVIEW_READY_MESSAGE = exports.PREVIEW_READY_MESSAGE = exports.DENEB_PREVIEW_READY_MESSAGE = exports.LEGACY_PREVIEW_DATA_MESSAGE = exports.PREVIEW_DATA_MESSAGE = exports.DENEB_PREVIEW_DATA_MESSAGE = void 0;
4
+ exports.DUMMY_PREVIEW_SHOP_NAME = exports.DUMMY_PREVIEW_ADDRESS = exports.DUMMY_PREVIEW_EMAIL = exports.DUMMY_PREVIEW_WHATSAPP = exports.DUMMY_PREVIEW_PHONE = exports.DenebDataContext = exports.useDenebData = exports.DenebDataProvider = exports.resolveAsset = exports.withBasePath = exports.SiteDataContext = exports.DENEB_STYLE_PATCH_MESSAGE = exports.STYLE_PATCH_MESSAGE = exports.PREVIEW_FIELD_ATTRIBUTE = exports.DENEB_REFRESH_LIVE_CATALOG_EVENT = exports.LEGACY_PREVIEW_FOCUS_MESSAGE = exports.PREVIEW_FOCUS_MESSAGE = exports.DENEB_PREVIEW_FOCUS_MESSAGE = exports.LEGACY_PREVIEW_READY_MESSAGE = exports.PREVIEW_READY_MESSAGE = exports.DENEB_PREVIEW_READY_MESSAGE = exports.LEGACY_PREVIEW_DATA_MESSAGE = exports.PREVIEW_DATA_MESSAGE = exports.DENEB_PREVIEW_DATA_MESSAGE = void 0;
5
5
  exports.isRecord = isRecord;
6
6
  exports.isDarkColor = isDarkColor;
7
7
  exports.syncThemeToDocument = syncThemeToDocument;
@@ -43,6 +43,7 @@ const ThemeStyles_1 = require("./ThemeStyles");
43
43
  const utils_1 = require("./utils");
44
44
  Object.defineProperty(exports, "withBasePath", { enumerable: true, get: function () { return utils_1.withBasePath; } });
45
45
  Object.defineProperty(exports, "resolveAsset", { enumerable: true, get: function () { return utils_1.resolveAsset; } });
46
+ const catalogMedia_1 = require("./utils/catalogMedia");
46
47
  exports.DENEB_PREVIEW_DATA_MESSAGE = 'DENEB_PREVIEW_SITE_DATA';
47
48
  exports.PREVIEW_DATA_MESSAGE = 'FIVORA_PREVIEW_SITE_DATA';
48
49
  const PREVIOUS_PREVIEW_PREFIX = `${['MARKET', 'PLACE'].join('')}_PREVIEW_`;
@@ -55,6 +56,7 @@ exports.LEGACY_PREVIEW_READY_MESSAGE = previousPreviewMessage('READY');
55
56
  exports.DENEB_PREVIEW_FOCUS_MESSAGE = 'DENEB_PREVIEW_FOCUS_PAGE';
56
57
  exports.PREVIEW_FOCUS_MESSAGE = 'FIVORA_PREVIEW_FOCUS_PAGE';
57
58
  exports.LEGACY_PREVIEW_FOCUS_MESSAGE = previousPreviewMessage('FOCUS_PAGE');
59
+ exports.DENEB_REFRESH_LIVE_CATALOG_EVENT = 'deneb:refresh-live-catalog';
58
60
  exports.PREVIEW_FIELD_ATTRIBUTE = 'data-preview-field-path';
59
61
  var core_2 = require("@deneb-ui/core");
60
62
  Object.defineProperty(exports, "STYLE_PATCH_MESSAGE", { enumerable: true, get: function () { return core_2.STYLE_PATCH_MESSAGE; } });
@@ -366,6 +368,9 @@ function SiteDataProvider({ children, initialSiteData, fallbackSiteData, liveCat
366
368
  }
367
369
  return {
368
370
  ...current,
371
+ liveCatalogUpdatedAt: typeof live.updatedAt === 'string'
372
+ ? live.updatedAt
373
+ : new Date().toISOString(),
369
374
  shop: nextShop,
370
375
  merchant: nextMerchant,
371
376
  reviews: nextReviews ?? current.reviews,
@@ -388,6 +393,36 @@ function SiteDataProvider({ children, initialSiteData, fallbackSiteData, liveCat
388
393
  ...(currentContent?.store && typeof currentContent.store === 'object' && nextProducts !== undefined
389
394
  ? { store: { ...currentContent.store, products: nextProducts } }
390
395
  : {}),
396
+ ...(currentContent?.servicesPage &&
397
+ typeof currentContent.servicesPage === 'object' &&
398
+ nextServices !== undefined
399
+ ? {
400
+ servicesPage: {
401
+ ...currentContent.servicesPage,
402
+ services: nextServices,
403
+ },
404
+ }
405
+ : {}),
406
+ ...(currentContent?.company &&
407
+ typeof currentContent.company === 'object' &&
408
+ nextServices !== undefined
409
+ ? {
410
+ company: {
411
+ ...currentContent.company,
412
+ services: nextServices,
413
+ },
414
+ }
415
+ : {}),
416
+ ...(currentContent?.business &&
417
+ typeof currentContent.business === 'object' &&
418
+ nextServices !== undefined
419
+ ? {
420
+ business: {
421
+ ...currentContent.business,
422
+ services: nextServices,
423
+ },
424
+ }
425
+ : {}),
391
426
  ...(currentHome || Object.keys(nextHome).length > 0
392
427
  ? {
393
428
  home: {
@@ -458,9 +493,19 @@ function SiteDataProvider({ children, initialSiteData, fallbackSiteData, liveCat
458
493
  void loadLiveCatalog();
459
494
  }
460
495
  };
496
+ const refreshNow = () => {
497
+ if (document.visibilityState !== 'visible')
498
+ return;
499
+ if (retryTimer !== null)
500
+ window.clearTimeout(retryTimer);
501
+ retryTimer = null;
502
+ void loadLiveCatalog();
503
+ };
461
504
  void loadLiveCatalog();
462
505
  window.addEventListener("focus", refreshIfStale);
463
506
  window.addEventListener("online", refreshIfStale);
507
+ window.addEventListener('pageshow', refreshIfStale);
508
+ window.addEventListener(exports.DENEB_REFRESH_LIVE_CATALOG_EVENT, refreshNow);
464
509
  document.addEventListener("visibilitychange", refreshIfStale);
465
510
  return () => {
466
511
  stopped = true;
@@ -469,6 +514,8 @@ function SiteDataProvider({ children, initialSiteData, fallbackSiteData, liveCat
469
514
  window.clearTimeout(retryTimer);
470
515
  window.removeEventListener("focus", refreshIfStale);
471
516
  window.removeEventListener("online", refreshIfStale);
517
+ window.removeEventListener('pageshow', refreshIfStale);
518
+ window.removeEventListener(exports.DENEB_REFRESH_LIVE_CATALOG_EVENT, refreshNow);
472
519
  document.removeEventListener("visibilitychange", refreshIfStale);
473
520
  };
474
521
  }, [liveCatalogEndpoint, siteSlug, initialSiteData]);
@@ -875,7 +922,9 @@ function normalizeProductItem(p) {
875
922
  p.src ??
876
923
  p.productImage ??
877
924
  (Array.isArray(p.gallery) ? p.gallery[0] : undefined) ??
878
- (Array.isArray(p.images) ? p.images[0] : undefined);
925
+ (Array.isArray(p.images) ? p.images[0] : undefined) ??
926
+ (Array.isArray(p.additionalImages) ? p.additionalImages[0] : undefined);
927
+ const catalogImages = (0, catalogMedia_1.resolveCatalogImages)(p);
879
928
  const badgeCandidate = p.badge ?? p.tag ?? p.label ?? p.badgeText ?? p.chip;
880
929
  const descCandidate = p.description ?? p.desc ?? p.details ?? p.shortDescription ?? p.subtitle;
881
930
  const explicitMin = p.minPrice ?? customData.minPrice;
@@ -918,6 +967,13 @@ function normalizeProductItem(p) {
918
967
  productImage: imageCandidate,
919
968
  }
920
969
  : {}),
970
+ ...(catalogImages.length > 0
971
+ ? {
972
+ gallery: catalogImages,
973
+ images: catalogImages,
974
+ additionalImages: catalogImages.slice(1),
975
+ }
976
+ : {}),
921
977
  ...(badgeCandidate !== undefined
922
978
  ? {
923
979
  badge: badgeCandidate,
@@ -943,7 +999,15 @@ function normalizeServiceItem(s) {
943
999
  const nameCandidate = s.name ?? s.title ?? s.serviceName ?? s.heading;
944
1000
  const titleCandidate = s.title ?? s.name ?? s.serviceName ?? s.heading;
945
1001
  const descCandidate = s.description ?? s.desc ?? s.details ?? s.shortDescription;
946
- const imageCandidate = s.imageUrl ?? s.image ?? s.photo ?? s.thumbnail ?? s.picture;
1002
+ const imageCandidate = s.imageUrl ??
1003
+ s.image ??
1004
+ s.photo ??
1005
+ s.thumbnail ??
1006
+ s.picture ??
1007
+ (Array.isArray(s.gallery) ? s.gallery[0] : undefined) ??
1008
+ (Array.isArray(s.images) ? s.images[0] : undefined) ??
1009
+ (Array.isArray(s.additionalImages) ? s.additionalImages[0] : undefined);
1010
+ const catalogImages = (0, catalogMedia_1.resolveCatalogImages)(s);
947
1011
  return {
948
1012
  ...customData,
949
1013
  ...s,
@@ -951,6 +1015,13 @@ function normalizeServiceItem(s) {
951
1015
  title: titleCandidate,
952
1016
  ...(descCandidate !== undefined ? { description: descCandidate, desc: descCandidate, details: descCandidate } : {}),
953
1017
  ...(imageCandidate !== undefined ? { imageUrl: imageCandidate, image: imageCandidate, photo: imageCandidate } : {}),
1018
+ ...(catalogImages.length > 0
1019
+ ? {
1020
+ gallery: catalogImages,
1021
+ images: catalogImages,
1022
+ additionalImages: catalogImages.slice(1),
1023
+ }
1024
+ : {}),
954
1025
  };
955
1026
  }
956
1027
  function useProducts(fallback = []) {
@@ -958,6 +1029,13 @@ function useProducts(fallback = []) {
958
1029
  const content = isRecord(siteData?.content) ? siteData.content : null;
959
1030
  if (!content)
960
1031
  return fallback;
1032
+ // Once the public catalog endpoint has answered, its top-level collection is
1033
+ // authoritative even when it is empty. This prevents deleted products from
1034
+ // falling back to stale products embedded in the generated site bundle.
1035
+ if (typeof siteData.liveCatalogUpdatedAt === 'string' &&
1036
+ Array.isArray(content.products)) {
1037
+ return content.products.map(normalizeProductItem);
1038
+ }
961
1039
  let rawList = null;
962
1040
  const candidates = [
963
1041
  content.products,
@@ -990,6 +1068,12 @@ function useServices(fallback = []) {
990
1068
  const content = isRecord(siteData?.content) ? siteData.content : null;
991
1069
  if (!content)
992
1070
  return fallback;
1071
+ // An empty live response is meaningful: all services were removed. Do not
1072
+ // resurrect build-time services from a nested legacy collection or fallback.
1073
+ if (typeof siteData.liveCatalogUpdatedAt === 'string' &&
1074
+ Array.isArray(content.services)) {
1075
+ return content.services.map(normalizeServiceItem);
1076
+ }
993
1077
  const candidates = [
994
1078
  content.services,
995
1079
  content.servicesPage?.services,
@@ -0,0 +1,18 @@
1
+ export interface CatalogMediaItem {
2
+ featuredImage?: unknown;
3
+ imageUrl?: unknown;
4
+ image?: unknown;
5
+ photo?: unknown;
6
+ thumbnail?: unknown;
7
+ productImage?: unknown;
8
+ gallery?: unknown;
9
+ images?: unknown;
10
+ additionalImages?: unknown;
11
+ [key: string]: unknown;
12
+ }
13
+ /**
14
+ * Resolves Fivora's catalog media contract while retaining legacy template
15
+ * aliases. The primary image stays first and duplicate URLs are removed.
16
+ */
17
+ export declare function resolveCatalogImages(item?: CatalogMediaItem | null, fallbackImage?: string): string[];
18
+ export declare function resolveCatalogPrimaryImage(item?: CatalogMediaItem | null, fallbackImage?: string): string;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveCatalogImages = resolveCatalogImages;
4
+ exports.resolveCatalogPrimaryImage = resolveCatalogPrimaryImage;
5
+ function appendImage(images, seen, candidate) {
6
+ if (typeof candidate !== 'string')
7
+ return;
8
+ const normalized = candidate.trim();
9
+ if (!normalized || seen.has(normalized))
10
+ return;
11
+ seen.add(normalized);
12
+ images.push(normalized);
13
+ }
14
+ function appendImageList(images, seen, candidate) {
15
+ if (!Array.isArray(candidate))
16
+ return;
17
+ candidate.forEach((value) => appendImage(images, seen, value));
18
+ }
19
+ /**
20
+ * Resolves Fivora's catalog media contract while retaining legacy template
21
+ * aliases. The primary image stays first and duplicate URLs are removed.
22
+ */
23
+ function resolveCatalogImages(item, fallbackImage) {
24
+ const images = [];
25
+ const seen = new Set();
26
+ const source = item || {};
27
+ appendImage(images, seen, source.featuredImage);
28
+ appendImage(images, seen, source.imageUrl);
29
+ appendImage(images, seen, source.image);
30
+ appendImage(images, seen, source.photo);
31
+ appendImage(images, seen, source.thumbnail);
32
+ appendImage(images, seen, source.productImage);
33
+ appendImageList(images, seen, source.gallery);
34
+ appendImageList(images, seen, source.images);
35
+ appendImageList(images, seen, source.additionalImages);
36
+ if (images.length === 0) {
37
+ appendImage(images, seen, fallbackImage);
38
+ }
39
+ return images;
40
+ }
41
+ function resolveCatalogPrimaryImage(item, fallbackImage) {
42
+ return resolveCatalogImages(item, fallbackImage)[0] || '';
43
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './urls';
2
2
  export * from './responsive';
3
3
  export * from './productOptions';
4
+ export * from './catalogMedia';
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./urls"), exports);
18
18
  __exportStar(require("./responsive"), exports);
19
19
  __exportStar(require("./productOptions"), exports);
20
+ __exportStar(require("./catalogMedia"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/ui",
3
- "version": "2.0.126",
3
+ "version": "2.0.127",
4
4
  "description": "Visual-first React component library for editable commerce storefronts. Built for Next.js and Fivora.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -50,7 +50,7 @@
50
50
  ],
51
51
  "license": "MIT",
52
52
  "dependencies": {
53
- "@deneb-ui/core": "^2.0.126"
53
+ "@deneb-ui/core": "^2.0.127"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "react": "^18.0.0 || ^19.0.0",