@deneb-ui/ui 2.0.126 → 2.0.128

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,12 +29,19 @@ 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;
35
36
  reviewsCount?: number | string;
36
37
  isNew?: boolean;
37
38
  isBestSeller?: boolean;
39
+ isAvailable?: boolean;
40
+ inStock?: boolean;
41
+ available?: boolean;
42
+ isActive?: boolean;
43
+ active?: boolean;
44
+ status?: string;
38
45
  whatsappNumber?: string;
39
46
  whatsappButtonText?: string;
40
47
  addToCartButtonText?: 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)
@@ -457,7 +451,7 @@ function EditableProductCard({ itemPath, product: productProp, item: itemProp, c
457
451
  fontSize: '0.8rem',
458
452
  fontWeight: 500,
459
453
  color: 'var(--muted-text, #64748b)',
460
- } })) : null] })) : !effectiveShowPrice ? ((0, jsx_runtime_1.jsx)("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: (0, jsx_runtime_1.jsx)("span", { style: {
454
+ } })) : null] })) : (!effectiveShowPrice || !hasPrice) ? ((0, jsx_runtime_1.jsx)("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: (0, jsx_runtime_1.jsx)("span", { style: {
461
455
  display: 'inline-flex',
462
456
  alignItems: 'center',
463
457
  padding: '4px 10px',
@@ -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)(() => {
@@ -4,13 +4,30 @@ export interface ServiceItem {
4
4
  name?: string;
5
5
  title?: string;
6
6
  price?: string | number;
7
+ cost?: string | number;
8
+ amount?: string | number;
9
+ rate?: string | number;
10
+ serviceFee?: string | number;
11
+ servicePrice?: string | number;
12
+ compareAtPrice?: string | number;
13
+ originalPrice?: string | number;
7
14
  priceLabel?: string;
8
15
  showPrice?: boolean;
16
+ currency?: string;
9
17
  customData?: Record<string, unknown>;
10
18
  description?: string;
11
19
  imageUrl?: string;
12
20
  image?: string;
21
+ gallery?: string[];
22
+ images?: string[];
23
+ additionalImages?: string[];
13
24
  features?: unknown[];
25
+ badge?: string;
26
+ tag?: string;
27
+ isActive?: boolean;
28
+ active?: boolean;
29
+ isAvailable?: boolean;
30
+ available?: boolean;
14
31
  [key: string]: unknown;
15
32
  }
16
33
  export interface EditableServiceCardProps extends React.HTMLAttributes<HTMLElement> {
@@ -35,9 +52,17 @@ export interface EditableServiceCardProps extends React.HTMLAttributes<HTMLEleme
35
52
  */
36
53
  align?: 'left' | 'center' | 'right';
37
54
  showPrice?: boolean;
55
+ /**
56
+ * Whether to show active/inactive status badge (default: true).
57
+ */
58
+ showStatusBadge?: boolean;
59
+ /**
60
+ * Currency code override (e.g. "LKR", "USD"). If omitted, auto-resolves from live shop profile.
61
+ */
62
+ currency?: string;
38
63
  }
39
64
  /**
40
65
  * EditableServiceCard standardizes service presentation across Home and Services pages.
41
66
  * Supports service name, description, image, and repeatable features checklist.
42
67
  */
43
- export declare function EditableServiceCard({ itemPath, service, imageFallback, as: Component, align, showPrice, className, style, children, ...props }: EditableServiceCardProps): React.JSX.Element;
68
+ export declare function EditableServiceCard({ itemPath, service, imageFallback, as: Component, align, showPrice, showStatusBadge, currency: currencyOverride, className, style, children, ...props }: EditableServiceCardProps): React.JSX.Element;
@@ -4,11 +4,18 @@ 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");
8
+ const SiteDataProvider_1 = require("./SiteDataProvider");
7
9
  /**
8
10
  * EditableServiceCard standardizes service presentation across Home and Services pages.
9
11
  * Supports service name, description, image, and repeatable features checklist.
10
12
  */
11
- function EditableServiceCard({ itemPath, service, imageFallback = '/placeholder.svg', as: Component = 'article', align = 'left', showPrice = true, className = '', style, children, ...props }) {
13
+ function EditableServiceCard({ itemPath, service, imageFallback = '/placeholder.svg', as: Component = 'article', align = 'left', showPrice = true, showStatusBadge = true, currency: currencyOverride, className = '', style, children, ...props }) {
14
+ const isServiceActive = service?.isActive !== false &&
15
+ service?.active !== false &&
16
+ service?.isAvailable !== false &&
17
+ service?.available !== false;
18
+ const badgeCandidate = service?.badge || service?.tag;
12
19
  const name = String(service?.name || service?.title || '');
13
20
  const description = String(service?.description || '');
14
21
  const itemShowPrice = service.showPrice !== undefined
@@ -21,37 +28,95 @@ function EditableServiceCard({ itemPath, service, imageFallback = '/placeholder.
21
28
  const hasRawPrice = rawPriceCandidate !== undefined && rawPriceCandidate !== null && String(rawPriceCandidate).trim() !== '';
22
29
  const priceLabel = service?.priceLabel ? String(service.priceLabel) : '';
23
30
  const hasPrice = effectiveShowPrice && (hasRawPrice || Boolean(priceLabel));
31
+ const liveCurrency = (0, SiteDataProvider_1.useCurrency)();
32
+ const currency = currencyOverride || service?.currency || liveCurrency;
24
33
  const formattedPrice = hasRawPrice
25
34
  ? typeof rawPriceCandidate === 'number'
26
- ? 'LKR ' + rawPriceCandidate.toLocaleString()
27
- : String(rawPriceCandidate).includes('LKR')
35
+ ? currency + ' ' + rawPriceCandidate.toLocaleString()
36
+ : String(rawPriceCandidate).match(/^[A-Z]{3}\s/) || String(rawPriceCandidate).includes(currency)
28
37
  ? String(rawPriceCandidate)
29
- : 'LKR ' + String(rawPriceCandidate)
38
+ : currency + ' ' + String(rawPriceCandidate)
30
39
  : '';
31
- const imageUrl = String(service?.imageUrl || service?.image || '');
40
+ const rawCompareCandidate = service?.compareAtPrice || service?.originalPrice;
41
+ const hasComparePrice = effectiveShowPrice &&
42
+ rawCompareCandidate !== undefined &&
43
+ rawCompareCandidate !== null &&
44
+ String(rawCompareCandidate).trim() !== '' &&
45
+ Number(rawCompareCandidate) > Number(rawPriceCandidate);
46
+ const formattedComparePrice = hasComparePrice
47
+ ? typeof rawCompareCandidate === 'number'
48
+ ? currency + ' ' + rawCompareCandidate.toLocaleString()
49
+ : String(rawCompareCandidate).match(/^[A-Z]{3}\s/) || String(rawCompareCandidate).includes(currency)
50
+ ? String(rawCompareCandidate)
51
+ : currency + ' ' + String(rawCompareCandidate)
52
+ : '';
53
+ const serviceImages = (0, catalogMedia_1.resolveCatalogImages)(service);
54
+ const imageUrl = serviceImages[0] || '';
32
55
  const features = Array.isArray(service?.features) ? service.features : [];
33
56
  const mergedStyle = {
34
57
  textAlign: align,
58
+ opacity: !isServiceActive ? 0.72 : 1,
35
59
  ...style,
36
60
  };
37
- return ((0, jsx_runtime_1.jsxs)(Component, { "data-preview-item-path": itemPath, className: `editable-service-card ${className}`.trim(), style: mergedStyle, ...props, children: [(imageUrl || service?.imageUrl !== undefined || service?.image !== undefined) && ((0, jsx_runtime_1.jsx)("div", { className: "service-card-image-wrap", children: (0, jsx_runtime_1.jsx)(EditableImage_1.EditableImage, { id: `${itemPath}.imageUrl`, "data-preview-field-path": `${itemPath}.imageUrl`, src: imageUrl, fallbackSrc: imageFallback, alt: name, className: "service-card-image" }) })), (0, jsx_runtime_1.jsxs)("div", { className: "service-card-body", children: [(0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "h2", id: `${itemPath}.name`, "data-preview-field-path": `${itemPath}.name`, defaultValue: name, className: "service-card-title" }), (0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "p", id: `${itemPath}.description`, "data-preview-field-path": `${itemPath}.description`, defaultValue: description, className: "service-card-description" }), hasPrice ? ((0, jsx_runtime_1.jsxs)("div", { className: "service-card-price-wrap", style: { margin: '0.5rem 0', display: 'flex', alignItems: 'baseline', gap: '0.375rem', flexWrap: 'wrap' }, children: [hasRawPrice && ((0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "span", id: `${itemPath}.price`, "data-preview-field-path": `${itemPath}.price`, defaultValue: formattedPrice, className: "service-card-price", style: {
61
+ const badgeElement = (badgeCandidate || (!isServiceActive && showStatusBadge)) ? ((0, jsx_runtime_1.jsx)("div", { className: "service-card-badge-wrap", style: {
62
+ position: 'absolute',
63
+ top: '0.75rem',
64
+ left: '0.75rem',
65
+ zIndex: 3,
66
+ display: 'flex',
67
+ gap: '0.375rem',
68
+ pointerEvents: 'none',
69
+ }, children: !isServiceActive && showStatusBadge ? ((0, jsx_runtime_1.jsxs)("span", { className: "service-card-status-badge inactive", style: {
70
+ display: 'inline-flex',
71
+ alignItems: 'center',
72
+ gap: '0.25rem',
73
+ padding: '0.25rem 0.625rem',
74
+ borderRadius: '9999px',
75
+ fontSize: '0.6875rem',
76
+ fontWeight: 700,
77
+ textTransform: 'uppercase',
78
+ letterSpacing: '0.05em',
79
+ backgroundColor: 'rgba(239, 68, 68, 0.95)',
80
+ color: '#ffffff',
81
+ boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
82
+ }, children: [(0, jsx_runtime_1.jsx)("span", { style: { width: 6, height: 6, borderRadius: '50%', backgroundColor: 'currentColor' } }), "Unavailable"] })) : badgeCandidate ? ((0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "span", id: `${itemPath}.badge`, "data-preview-field-path": `${itemPath}.badge`, defaultValue: String(badgeCandidate), className: "service-card-badge", style: {
83
+ display: 'inline-flex',
84
+ alignItems: 'center',
85
+ padding: '0.25rem 0.625rem',
86
+ borderRadius: '9999px',
87
+ fontSize: '0.6875rem',
88
+ fontWeight: 700,
89
+ textTransform: 'uppercase',
90
+ letterSpacing: '0.05em',
91
+ backgroundColor: 'var(--brand-color, #2563eb)',
92
+ color: '#ffffff',
93
+ boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
94
+ pointerEvents: 'auto',
95
+ } })) : null })) : null;
96
+ return ((0, jsx_runtime_1.jsxs)(Component, { "data-preview-item-path": itemPath, className: `editable-service-card ${!isServiceActive ? 'is-inactive is-unavailable' : ''} ${className}`.trim(), style: mergedStyle, ...props, children: [(imageUrl || service?.imageUrl !== undefined || service?.image !== undefined) ? ((0, jsx_runtime_1.jsxs)("div", { className: "service-card-image-wrap", style: { position: 'relative' }, children: [badgeElement, (0, jsx_runtime_1.jsx)(EditableImage_1.EditableImage, { id: `${itemPath}.imageUrl`, "data-preview-field-path": `${itemPath}.imageUrl`, src: imageUrl, fallbackSrc: imageFallback, alt: name, className: "service-card-image" })] })) : badgeElement ? ((0, jsx_runtime_1.jsx)("div", { style: { position: 'relative', width: '100%', height: '1.75rem', marginBottom: '0.5rem' }, children: badgeElement })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "service-card-body", children: [(0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "h2", id: `${itemPath}.name`, "data-preview-field-path": `${itemPath}.name`, defaultValue: name, className: "service-card-title" }), (0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "p", id: `${itemPath}.description`, "data-preview-field-path": `${itemPath}.description`, defaultValue: description, className: "service-card-description" }), hasPrice ? ((0, jsx_runtime_1.jsxs)("div", { className: "service-card-price-wrap", style: { margin: '0.5rem 0', display: 'flex', alignItems: 'baseline', gap: '0.5rem', flexWrap: 'wrap' }, children: [hasRawPrice && ((0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "span", id: `${itemPath}.price`, "data-preview-field-path": `${itemPath}.price`, defaultValue: formattedPrice, className: "service-card-price", style: {
38
97
  fontSize: '1.125rem',
39
98
  fontWeight: 700,
40
99
  color: 'var(--brand-color, #2563eb)',
41
- } })), priceLabel && ((0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "span", id: `${itemPath}.priceLabel`, "data-preview-field-path": `${itemPath}.priceLabel`, defaultValue: priceLabel, className: "service-card-price-label", style: {
100
+ } })), hasComparePrice && ((0, jsx_runtime_1.jsx)("span", { className: "service-card-compare-price", style: {
101
+ fontSize: '0.875rem',
102
+ fontWeight: 500,
103
+ color: 'var(--muted-text, #94a3b8)',
104
+ textDecoration: 'line-through',
105
+ }, children: formattedComparePrice })), priceLabel && ((0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "span", id: `${itemPath}.priceLabel`, "data-preview-field-path": `${itemPath}.priceLabel`, defaultValue: priceLabel, className: "service-card-price-label", style: {
42
106
  fontSize: '0.8125rem',
43
107
  fontWeight: 500,
44
108
  color: 'var(--muted-text, #94a3b8)',
45
- } }))] })) : !effectiveShowPrice ? ((0, jsx_runtime_1.jsx)("div", { className: "service-card-price-wrap", style: { margin: '0.5rem 0' }, children: (0, jsx_runtime_1.jsx)("span", { style: {
109
+ } }))] })) : (!effectiveShowPrice || (!hasRawPrice && !priceLabel)) ? ((0, jsx_runtime_1.jsx)("div", { className: "service-card-price-wrap", style: { margin: '0.5rem 0' }, children: (0, jsx_runtime_1.jsx)("span", { style: {
46
110
  display: 'inline-flex',
47
111
  alignItems: 'center',
48
- padding: '3px 8px',
49
- borderRadius: '6px',
50
- fontSize: '0.75rem',
112
+ padding: '4px 10px',
113
+ borderRadius: '8px',
114
+ fontSize: '0.8125rem',
51
115
  fontWeight: 600,
52
116
  backgroundColor: 'rgba(255, 255, 255, 0.08)',
53
117
  color: 'var(--muted-text, #94a3b8)',
54
118
  border: '1px solid rgba(255, 255, 255, 0.12)',
119
+ letterSpacing: '0.01em',
55
120
  }, children: "Quote on Request" }) })) : null, (features.length > 0 || Array.isArray(service?.features)) && ((0, jsx_runtime_1.jsx)("ul", { "data-preview-list-path": `${itemPath}.features`, className: "service-card-features", children: features.map((feature, featureIndex) => {
56
121
  const featureText = typeof feature === 'string'
57
122
  ? feature
@@ -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 = () => {
@@ -112,14 +112,24 @@ export declare function formatWeeklyHoursToString(value: unknown): string;
112
112
  export declare function normalizeProductItem(p: unknown): ProductItem;
113
113
  /**
114
114
  * Universal Service Normalizer
115
+ * Ensures every service object has both canonical and alias keys simultaneously populated
116
+ * with clean numbers and resolved image URLs, matching the product normalizer's robustness.
115
117
  */
116
118
  export declare function normalizeServiceItem(s: unknown): ServiceItem;
117
- export declare function useProducts(fallback?: ProductItem[]): ProductItem[];
119
+ export interface UseProductsOptions {
120
+ inStockOnly?: boolean;
121
+ activeOnly?: boolean;
122
+ }
123
+ export declare function useProducts(fallback?: ProductItem[], options?: UseProductsOptions): ProductItem[];
124
+ export interface UseServicesOptions {
125
+ activeOnly?: boolean;
126
+ }
118
127
  /**
119
128
  * Hook to retrieve services cleanly from SiteData, supporting both
120
129
  * top-level content.services and nested content.home.services.
130
+ * Supports optional { activeOnly: true } filtering to mirror product availability.
121
131
  */
122
- export declare function useServices(fallback?: ServiceItem[]): ServiceItem[];
132
+ export declare function useServices(fallback?: ServiceItem[], options?: UseServicesOptions): ServiceItem[];
123
133
  /**
124
134
  * Hook to access official Fivora backend API endpoints (catalogUrl, contactUrl, analyticsUrl).
125
135
  */
@@ -178,3 +188,31 @@ export interface SyncedBranding {
178
188
  * Hierarchy: Visual Editor Edits (content.common) -> Live Merchant Shop Profile -> Template Fallback.
179
189
  */
180
190
  export declare function useSyncedBranding(fallback?: Partial<SyncedBranding>): SyncedBranding;
191
+ /**
192
+ * Hook to retrieve the live store currency code (e.g. "LKR", "USD", "EUR").
193
+ * Auto-hydrates from the merchant portal's shop profile.
194
+ * Hierarchy: shop.currency → content.common.currency → merchant.currency → fallback.
195
+ */
196
+ export declare function useCurrency(fallback?: string): string;
197
+ export interface SocialLinksMap {
198
+ facebook: string;
199
+ instagram: string;
200
+ tiktok: string;
201
+ youtube: string;
202
+ linkedin: string;
203
+ twitter: string;
204
+ website: string;
205
+ [key: string]: string;
206
+ }
207
+ /**
208
+ * Hook to retrieve the live social links map from the merchant profile.
209
+ * Auto-hydrates from the shop owner's Fivora Portal social configuration.
210
+ * Returns a map like { facebook: "https://...", instagram: "https://...", ... }.
211
+ */
212
+ export declare function useSocial(fallback?: Partial<SocialLinksMap>): SocialLinksMap;
213
+ /**
214
+ * Hook to retrieve the live shop front / cover / banner image URL.
215
+ * Auto-hydrates from the merchant portal's shop profile.
216
+ * Hierarchy: shop.coverImage → shop.shopImage → shop.bannerImage → shop.heroImage → content.common.shopImage → fallback.
217
+ */
218
+ export declare function useShopImage(fallback?: string): string;
@@ -32,6 +32,9 @@ exports.isPreviewValidation = isPreviewValidation;
32
32
  exports.useCommon = useCommon;
33
33
  exports.useContact = useContact;
34
34
  exports.useSyncedBranding = useSyncedBranding;
35
+ exports.useCurrency = useCurrency;
36
+ exports.useSocial = useSocial;
37
+ exports.useShopImage = useShopImage;
35
38
  const jsx_runtime_1 = require("react/jsx-runtime");
36
39
  const useCart_1 = require("./cart/useCart");
37
40
  const react_1 = require("react");
@@ -43,6 +46,7 @@ const ThemeStyles_1 = require("./ThemeStyles");
43
46
  const utils_1 = require("./utils");
44
47
  Object.defineProperty(exports, "withBasePath", { enumerable: true, get: function () { return utils_1.withBasePath; } });
45
48
  Object.defineProperty(exports, "resolveAsset", { enumerable: true, get: function () { return utils_1.resolveAsset; } });
49
+ const catalogMedia_1 = require("./utils/catalogMedia");
46
50
  exports.DENEB_PREVIEW_DATA_MESSAGE = 'DENEB_PREVIEW_SITE_DATA';
47
51
  exports.PREVIEW_DATA_MESSAGE = 'FIVORA_PREVIEW_SITE_DATA';
48
52
  const PREVIOUS_PREVIEW_PREFIX = `${['MARKET', 'PLACE'].join('')}_PREVIEW_`;
@@ -875,13 +879,36 @@ function normalizeProductItem(p) {
875
879
  p.src ??
876
880
  p.productImage ??
877
881
  (Array.isArray(p.gallery) ? p.gallery[0] : undefined) ??
878
- (Array.isArray(p.images) ? p.images[0] : undefined);
882
+ (Array.isArray(p.images) ? p.images[0] : undefined) ??
883
+ (Array.isArray(p.additionalImages) ? p.additionalImages[0] : undefined);
884
+ const catalogImages = (0, catalogMedia_1.resolveCatalogImages)(p);
879
885
  const badgeCandidate = p.badge ?? p.tag ?? p.label ?? p.badgeText ?? p.chip;
880
886
  const descCandidate = p.description ?? p.desc ?? p.details ?? p.shortDescription ?? p.subtitle;
881
887
  const explicitMin = p.minPrice ?? customData.minPrice;
882
888
  const explicitMax = p.maxPrice ?? customData.maxPrice;
883
889
  const explicitRange = p.priceRange ?? customData.priceRange;
884
890
  const isRange = Boolean(p.isPriceRange || customData.isPriceRange || (explicitMax && explicitMin));
891
+ const isAvailableCandidate = p.isAvailable !== undefined
892
+ ? Boolean(p.isAvailable)
893
+ : p.available !== undefined
894
+ ? Boolean(p.available)
895
+ : p.inStock !== undefined
896
+ ? Boolean(p.inStock)
897
+ : customData.isAvailable !== undefined
898
+ ? Boolean(customData.isAvailable)
899
+ : true;
900
+ const isActiveCandidate = p.isActive !== undefined
901
+ ? Boolean(p.isActive)
902
+ : p.active !== undefined
903
+ ? Boolean(p.active)
904
+ : customData.isActive !== undefined
905
+ ? Boolean(customData.isActive)
906
+ : true;
907
+ const showPriceCandidate = p.showPrice !== undefined
908
+ ? Boolean(p.showPrice)
909
+ : customData.showPrice !== undefined
910
+ ? Boolean(customData.showPrice)
911
+ : true;
885
912
  return {
886
913
  ...customData,
887
914
  ...p,
@@ -889,6 +916,12 @@ function normalizeProductItem(p) {
889
916
  title: titleCandidate,
890
917
  productName: (titleCandidate ?? nameCandidate),
891
918
  itemTitle: (titleCandidate ?? nameCandidate),
919
+ isAvailable: isAvailableCandidate,
920
+ inStock: isAvailableCandidate,
921
+ available: isAvailableCandidate,
922
+ isActive: isActiveCandidate,
923
+ active: isActiveCandidate,
924
+ showPrice: showPriceCandidate,
892
925
  minPrice: explicitMin !== undefined ? explicitMin : undefined,
893
926
  priceMin: (explicitMin ?? p.priceMin) !== undefined ? (explicitMin ?? p.priceMin) : undefined,
894
927
  maxPrice: explicitMax !== undefined ? explicitMax : undefined,
@@ -918,6 +951,13 @@ function normalizeProductItem(p) {
918
951
  productImage: imageCandidate,
919
952
  }
920
953
  : {}),
954
+ ...(catalogImages.length > 0
955
+ ? {
956
+ gallery: catalogImages,
957
+ images: catalogImages,
958
+ additionalImages: catalogImages.slice(1),
959
+ }
960
+ : {}),
921
961
  ...(badgeCandidate !== undefined
922
962
  ? {
923
963
  badge: badgeCandidate,
@@ -935,6 +975,8 @@ function normalizeProductItem(p) {
935
975
  }
936
976
  /**
937
977
  * Universal Service Normalizer
978
+ * Ensures every service object has both canonical and alias keys simultaneously populated
979
+ * with clean numbers and resolved image URLs, matching the product normalizer's robustness.
938
980
  */
939
981
  function normalizeServiceItem(s) {
940
982
  if (!isRecord(s))
@@ -943,17 +985,126 @@ function normalizeServiceItem(s) {
943
985
  const nameCandidate = s.name ?? s.title ?? s.serviceName ?? s.heading;
944
986
  const titleCandidate = s.title ?? s.name ?? s.serviceName ?? s.heading;
945
987
  const descCandidate = s.description ?? s.desc ?? s.details ?? s.shortDescription;
946
- const imageCandidate = s.imageUrl ?? s.image ?? s.photo ?? s.thumbnail ?? s.picture;
988
+ const imageCandidate = s.imageUrl ??
989
+ s.image ??
990
+ s.photo ??
991
+ s.thumbnail ??
992
+ s.picture ??
993
+ (Array.isArray(s.gallery) ? s.gallery[0] : undefined) ??
994
+ (Array.isArray(s.images) ? s.images[0] : undefined) ??
995
+ (Array.isArray(s.additionalImages) ? s.additionalImages[0] : undefined);
996
+ const catalogImages = (0, catalogMedia_1.resolveCatalogImages)(s);
997
+ // Price normalization — parse string prices to clean numbers, support aliases
998
+ const rawPrice = s.price !== undefined
999
+ ? s.price
1000
+ : s.cost !== undefined
1001
+ ? s.cost
1002
+ : s.amount !== undefined
1003
+ ? s.amount
1004
+ : s.rate !== undefined
1005
+ ? s.rate
1006
+ : s.serviceFee !== undefined
1007
+ ? s.serviceFee
1008
+ : s.servicePrice !== undefined
1009
+ ? s.servicePrice
1010
+ : customData.price !== undefined
1011
+ ? customData.price
1012
+ : undefined;
1013
+ let numPrice = rawPrice;
1014
+ if (typeof rawPrice === "string" && rawPrice.trim()) {
1015
+ const isRangeStr = rawPrice.includes("-") || rawPrice.includes("–") || rawPrice.includes("—") || /\bto\b/i.test(rawPrice);
1016
+ if (!isRangeStr) {
1017
+ const match = rawPrice.match(/-?\d+(?:,\d{3})*(?:\.\d+)?|-?\d+(?:\.\d+)?/);
1018
+ if (match) {
1019
+ const parsed = parseFloat(match[0].replace(/,/g, ""));
1020
+ if (!isNaN(parsed) && Number.isFinite(parsed)) {
1021
+ numPrice = parsed;
1022
+ }
1023
+ }
1024
+ }
1025
+ }
1026
+ // Compare/original price normalization for services
1027
+ const rawCompare = s.compareAtPrice !== undefined
1028
+ ? s.compareAtPrice
1029
+ : s.originalPrice !== undefined
1030
+ ? s.originalPrice
1031
+ : s.oldPrice !== undefined
1032
+ ? s.oldPrice
1033
+ : s.regularPrice !== undefined
1034
+ ? s.regularPrice
1035
+ : customData.compareAtPrice !== undefined
1036
+ ? customData.compareAtPrice
1037
+ : undefined;
1038
+ let numCompare = rawCompare;
1039
+ if (typeof rawCompare === "string" && rawCompare.trim()) {
1040
+ const match = rawCompare.match(/-?\d+(?:,\d{3})*(?:\.\d+)?|-?\d+(?:\.\d+)?/);
1041
+ if (match) {
1042
+ const parsed = parseFloat(match[0].replace(/,/g, ""));
1043
+ if (!isNaN(parsed) && Number.isFinite(parsed)) {
1044
+ numCompare = parsed;
1045
+ }
1046
+ }
1047
+ }
1048
+ const priceLabelCandidate = s.priceLabel ?? customData.priceLabel;
1049
+ const isActiveCandidate = s.isActive !== undefined
1050
+ ? Boolean(s.isActive)
1051
+ : s.active !== undefined
1052
+ ? Boolean(s.active)
1053
+ : s.isAvailable !== undefined
1054
+ ? Boolean(s.isAvailable)
1055
+ : s.available !== undefined
1056
+ ? Boolean(s.available)
1057
+ : customData.isActive !== undefined
1058
+ ? Boolean(customData.isActive)
1059
+ : customData.isAvailable !== undefined
1060
+ ? Boolean(customData.isAvailable)
1061
+ : true;
1062
+ const showPriceCandidate = s.showPrice !== undefined
1063
+ ? Boolean(s.showPrice)
1064
+ : customData.showPrice !== undefined
1065
+ ? Boolean(customData.showPrice)
1066
+ : true;
1067
+ const badgeCandidate = s.badge ?? s.tag ?? s.label ?? customData.badge ?? customData.tag;
947
1068
  return {
948
1069
  ...customData,
949
1070
  ...s,
950
1071
  name: nameCandidate,
951
1072
  title: titleCandidate,
1073
+ isActive: isActiveCandidate,
1074
+ active: isActiveCandidate,
1075
+ isAvailable: isActiveCandidate,
1076
+ available: isActiveCandidate,
1077
+ showPrice: showPriceCandidate,
952
1078
  ...(descCandidate !== undefined ? { description: descCandidate, desc: descCandidate, details: descCandidate } : {}),
953
1079
  ...(imageCandidate !== undefined ? { imageUrl: imageCandidate, image: imageCandidate, photo: imageCandidate } : {}),
1080
+ ...(badgeCandidate !== undefined ? { badge: String(badgeCandidate), tag: String(badgeCandidate) } : {}),
1081
+ ...(catalogImages.length > 0
1082
+ ? {
1083
+ gallery: catalogImages,
1084
+ images: catalogImages,
1085
+ additionalImages: catalogImages.slice(1),
1086
+ }
1087
+ : {}),
1088
+ ...(numPrice !== undefined
1089
+ ? {
1090
+ price: numPrice,
1091
+ cost: numPrice,
1092
+ amount: numPrice,
1093
+ rate: numPrice,
1094
+ serviceFee: numPrice,
1095
+ servicePrice: numPrice,
1096
+ }
1097
+ : {}),
1098
+ ...(numCompare !== undefined
1099
+ ? {
1100
+ compareAtPrice: numCompare,
1101
+ originalPrice: numCompare,
1102
+ }
1103
+ : {}),
1104
+ ...(priceLabelCandidate !== undefined ? { priceLabel: priceLabelCandidate } : {}),
954
1105
  };
955
1106
  }
956
- function useProducts(fallback = []) {
1107
+ function useProducts(fallback = [], options) {
957
1108
  const siteData = useSiteData();
958
1109
  const content = isRecord(siteData?.content) ? siteData.content : null;
959
1110
  if (!content)
@@ -979,17 +1130,26 @@ function useProducts(fallback = []) {
979
1130
  }
980
1131
  if (!rawList)
981
1132
  return fallback;
982
- return rawList.map(normalizeProductItem);
1133
+ let list = rawList.map(normalizeProductItem);
1134
+ if (options?.activeOnly) {
1135
+ list = list.filter((p) => p.isActive !== false && p.status !== 'ARCHIVED' && p.status !== 'DRAFT');
1136
+ }
1137
+ if (options?.inStockOnly) {
1138
+ list = list.filter((p) => p.isAvailable !== false && p.inStock !== false);
1139
+ }
1140
+ return list;
983
1141
  }
984
1142
  /**
985
1143
  * Hook to retrieve services cleanly from SiteData, supporting both
986
1144
  * top-level content.services and nested content.home.services.
1145
+ * Supports optional { activeOnly: true } filtering to mirror product availability.
987
1146
  */
988
- function useServices(fallback = []) {
1147
+ function useServices(fallback = [], options) {
989
1148
  const siteData = useSiteData();
990
1149
  const content = isRecord(siteData?.content) ? siteData.content : null;
991
1150
  if (!content)
992
1151
  return fallback;
1152
+ let rawList = null;
993
1153
  const candidates = [
994
1154
  content.services,
995
1155
  content.servicesPage?.services,
@@ -1000,10 +1160,15 @@ function useServices(fallback = []) {
1000
1160
  ];
1001
1161
  for (const candidate of candidates) {
1002
1162
  if (Array.isArray(candidate) && candidate.length > 0) {
1003
- return candidate.map(normalizeServiceItem);
1163
+ rawList = candidate;
1164
+ break;
1004
1165
  }
1005
1166
  }
1006
- return fallback;
1167
+ const list = rawList ? rawList.map(normalizeServiceItem) : fallback;
1168
+ if (options?.activeOnly) {
1169
+ return list.filter((s) => s.isActive !== false && s.isAvailable !== false);
1170
+ }
1171
+ return list;
1007
1172
  }
1008
1173
  /**
1009
1174
  * Hook to access official Fivora backend API endpoints (catalogUrl, contactUrl, analyticsUrl).
@@ -1230,3 +1395,79 @@ function useSyncedBranding(fallback = {}) {
1230
1395
  faviconUrl,
1231
1396
  };
1232
1397
  }
1398
+ /**
1399
+ * Hook to retrieve the live store currency code (e.g. "LKR", "USD", "EUR").
1400
+ * Auto-hydrates from the merchant portal's shop profile.
1401
+ * Hierarchy: shop.currency → content.common.currency → merchant.currency → fallback.
1402
+ */
1403
+ function useCurrency(fallback = 'LKR') {
1404
+ const siteData = useSiteData();
1405
+ const content = isRecord(siteData?.content) ? siteData.content : null;
1406
+ const common = isRecord(content?.common) ? content.common : {};
1407
+ const shop = useShop();
1408
+ return ((typeof shop.currency === 'string' && shop.currency.trim()) ||
1409
+ (typeof common.currency === 'string' && common.currency.trim()) ||
1410
+ (typeof shop.currencyCode === 'string' && shop.currencyCode.trim()) ||
1411
+ (typeof common.currencyCode === 'string' && common.currencyCode.trim()) ||
1412
+ fallback);
1413
+ }
1414
+ /**
1415
+ * Hook to retrieve the live social links map from the merchant profile.
1416
+ * Auto-hydrates from the shop owner's Fivora Portal social configuration.
1417
+ * Returns a map like { facebook: "https://...", instagram: "https://...", ... }.
1418
+ */
1419
+ function useSocial(fallback = {}) {
1420
+ const siteData = useSiteData();
1421
+ const content = isRecord(siteData?.content) ? siteData.content : null;
1422
+ const common = isRecord(content?.common) ? content.common : {};
1423
+ const shop = useShop();
1424
+ const shopSocial = isRecord(shop.socialLinks) ? shop.socialLinks : (isRecord(shop.social) ? shop.social : {});
1425
+ const commonSocial = isRecord(common.socialLinks) ? common.socialLinks : (isRecord(common.social) ? common.social : {});
1426
+ const extractString = (key) => {
1427
+ const candidates = [
1428
+ shopSocial[key],
1429
+ commonSocial[key],
1430
+ shop[key],
1431
+ common[key],
1432
+ fallback[key],
1433
+ ];
1434
+ for (const candidate of candidates) {
1435
+ if (typeof candidate === 'string' && candidate.trim()) {
1436
+ return candidate.trim();
1437
+ }
1438
+ }
1439
+ return '';
1440
+ };
1441
+ return {
1442
+ facebook: extractString('facebook') || extractString('facebookUrl'),
1443
+ instagram: extractString('instagram') || extractString('instagramUrl'),
1444
+ tiktok: extractString('tiktok') || extractString('tiktokUrl'),
1445
+ youtube: extractString('youtube') || extractString('youtubeUrl'),
1446
+ linkedin: extractString('linkedin') || extractString('linkedinUrl'),
1447
+ twitter: extractString('twitter') || extractString('twitterUrl') || extractString('x'),
1448
+ website: extractString('website') || extractString('websiteUrl'),
1449
+ ...Object.fromEntries(Object.entries({ ...fallback, ...commonSocial, ...shopSocial })
1450
+ .filter(([, v]) => typeof v === 'string' && v.trim())
1451
+ .map(([k, v]) => [k, String(v).trim()])),
1452
+ };
1453
+ }
1454
+ /**
1455
+ * Hook to retrieve the live shop front / cover / banner image URL.
1456
+ * Auto-hydrates from the merchant portal's shop profile.
1457
+ * Hierarchy: shop.coverImage → shop.shopImage → shop.bannerImage → shop.heroImage → content.common.shopImage → fallback.
1458
+ */
1459
+ function useShopImage(fallback = '') {
1460
+ const siteData = useSiteData();
1461
+ const content = isRecord(siteData?.content) ? siteData.content : null;
1462
+ const common = isRecord(content?.common) ? content.common : {};
1463
+ const shop = useShop();
1464
+ return ((typeof shop.coverImage === 'string' && shop.coverImage.trim()) ||
1465
+ (typeof shop.shopImage === 'string' && shop.shopImage.trim()) ||
1466
+ (typeof shop.bannerImage === 'string' && shop.bannerImage.trim()) ||
1467
+ (typeof shop.heroImage === 'string' && shop.heroImage.trim()) ||
1468
+ (typeof shop.shopFrontImage === 'string' && shop.shopFrontImage.trim()) ||
1469
+ (typeof common.shopImage === 'string' && common.shopImage.trim()) ||
1470
+ (typeof common.coverImage === 'string' && common.coverImage.trim()) ||
1471
+ (typeof common.bannerImage === 'string' && common.bannerImage.trim()) ||
1472
+ fallback);
1473
+ }
@@ -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.128",
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.128"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "react": "^18.0.0 || ^19.0.0",