@deneb-ui/ui 2.0.127 → 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.
@@ -36,6 +36,12 @@ export interface ProductItem {
36
36
  reviewsCount?: number | string;
37
37
  isNew?: boolean;
38
38
  isBestSeller?: boolean;
39
+ isAvailable?: boolean;
40
+ inStock?: boolean;
41
+ available?: boolean;
42
+ isActive?: boolean;
43
+ active?: boolean;
44
+ status?: string;
39
45
  whatsappNumber?: string;
40
46
  whatsappButtonText?: string;
41
47
  addToCartButtonText?: string;
@@ -451,7 +451,7 @@ function EditableProductCard({ itemPath, product: productProp, item: itemProp, c
451
451
  fontSize: '0.8rem',
452
452
  fontWeight: 500,
453
453
  color: 'var(--muted-text, #64748b)',
454
- } })) : 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: {
455
455
  display: 'inline-flex',
456
456
  alignItems: 'center',
457
457
  padding: '4px 10px',
@@ -4,8 +4,16 @@ 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;
@@ -14,6 +22,12 @@ export interface ServiceItem {
14
22
  images?: string[];
15
23
  additionalImages?: string[];
16
24
  features?: unknown[];
25
+ badge?: string;
26
+ tag?: string;
27
+ isActive?: boolean;
28
+ active?: boolean;
29
+ isAvailable?: boolean;
30
+ available?: boolean;
17
31
  [key: string]: unknown;
18
32
  }
19
33
  export interface EditableServiceCardProps extends React.HTMLAttributes<HTMLElement> {
@@ -38,9 +52,17 @@ export interface EditableServiceCardProps extends React.HTMLAttributes<HTMLEleme
38
52
  */
39
53
  align?: 'left' | 'center' | 'right';
40
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;
41
63
  }
42
64
  /**
43
65
  * EditableServiceCard standardizes service presentation across Home and Services pages.
44
66
  * Supports service name, description, image, and repeatable features checklist.
45
67
  */
46
- 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;
@@ -5,11 +5,17 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const EditableText_1 = require("./EditableText");
6
6
  const EditableImage_1 = require("./EditableImage");
7
7
  const catalogMedia_1 = require("./utils/catalogMedia");
8
+ const SiteDataProvider_1 = require("./SiteDataProvider");
8
9
  /**
9
10
  * EditableServiceCard standardizes service presentation across Home and Services pages.
10
11
  * Supports service name, description, image, and repeatable features checklist.
11
12
  */
12
- 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;
13
19
  const name = String(service?.name || service?.title || '');
14
20
  const description = String(service?.description || '');
15
21
  const itemShowPrice = service.showPrice !== undefined
@@ -22,38 +28,95 @@ function EditableServiceCard({ itemPath, service, imageFallback = '/placeholder.
22
28
  const hasRawPrice = rawPriceCandidate !== undefined && rawPriceCandidate !== null && String(rawPriceCandidate).trim() !== '';
23
29
  const priceLabel = service?.priceLabel ? String(service.priceLabel) : '';
24
30
  const hasPrice = effectiveShowPrice && (hasRawPrice || Boolean(priceLabel));
31
+ const liveCurrency = (0, SiteDataProvider_1.useCurrency)();
32
+ const currency = currencyOverride || service?.currency || liveCurrency;
25
33
  const formattedPrice = hasRawPrice
26
34
  ? typeof rawPriceCandidate === 'number'
27
- ? 'LKR ' + rawPriceCandidate.toLocaleString()
28
- : String(rawPriceCandidate).includes('LKR')
35
+ ? currency + ' ' + rawPriceCandidate.toLocaleString()
36
+ : String(rawPriceCandidate).match(/^[A-Z]{3}\s/) || String(rawPriceCandidate).includes(currency)
29
37
  ? String(rawPriceCandidate)
30
- : 'LKR ' + String(rawPriceCandidate)
38
+ : currency + ' ' + String(rawPriceCandidate)
39
+ : '';
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)
31
52
  : '';
32
53
  const serviceImages = (0, catalogMedia_1.resolveCatalogImages)(service);
33
54
  const imageUrl = serviceImages[0] || '';
34
55
  const features = Array.isArray(service?.features) ? service.features : [];
35
56
  const mergedStyle = {
36
57
  textAlign: align,
58
+ opacity: !isServiceActive ? 0.72 : 1,
37
59
  ...style,
38
60
  };
39
- 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: {
40
97
  fontSize: '1.125rem',
41
98
  fontWeight: 700,
42
99
  color: 'var(--brand-color, #2563eb)',
43
- } })), 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: {
44
106
  fontSize: '0.8125rem',
45
107
  fontWeight: 500,
46
108
  color: 'var(--muted-text, #94a3b8)',
47
- } }))] })) : !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: {
48
110
  display: 'inline-flex',
49
111
  alignItems: 'center',
50
- padding: '3px 8px',
51
- borderRadius: '6px',
52
- fontSize: '0.75rem',
112
+ padding: '4px 10px',
113
+ borderRadius: '8px',
114
+ fontSize: '0.8125rem',
53
115
  fontWeight: 600,
54
116
  backgroundColor: 'rgba(255, 255, 255, 0.08)',
55
117
  color: 'var(--muted-text, #94a3b8)',
56
118
  border: '1px solid rgba(255, 255, 255, 0.12)',
119
+ letterSpacing: '0.01em',
57
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) => {
58
121
  const featureText = typeof feature === 'string'
59
122
  ? feature
@@ -11,7 +11,6 @@ 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";
15
14
  export declare const PREVIEW_FIELD_ATTRIBUTE = "data-preview-field-path";
16
15
  export { STYLE_PATCH_MESSAGE, DENEB_STYLE_PATCH_MESSAGE } from '@deneb-ui/core';
17
16
  export type GenericRecord = Record<string, any>;
@@ -113,14 +112,24 @@ export declare function formatWeeklyHoursToString(value: unknown): string;
113
112
  export declare function normalizeProductItem(p: unknown): ProductItem;
114
113
  /**
115
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.
116
117
  */
117
118
  export declare function normalizeServiceItem(s: unknown): ServiceItem;
118
- 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
+ }
119
127
  /**
120
128
  * Hook to retrieve services cleanly from SiteData, supporting both
121
129
  * top-level content.services and nested content.home.services.
130
+ * Supports optional { activeOnly: true } filtering to mirror product availability.
122
131
  */
123
- export declare function useServices(fallback?: ServiceItem[]): ServiceItem[];
132
+ export declare function useServices(fallback?: ServiceItem[], options?: UseServicesOptions): ServiceItem[];
124
133
  /**
125
134
  * Hook to access official Fivora backend API endpoints (catalogUrl, contactUrl, analyticsUrl).
126
135
  */
@@ -179,3 +188,31 @@ export interface SyncedBranding {
179
188
  * Hierarchy: Visual Editor Edits (content.common) -> Live Merchant Shop Profile -> Template Fallback.
180
189
  */
181
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;
@@ -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.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;
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;
5
5
  exports.isRecord = isRecord;
6
6
  exports.isDarkColor = isDarkColor;
7
7
  exports.syncThemeToDocument = syncThemeToDocument;
@@ -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");
@@ -56,7 +59,6 @@ exports.LEGACY_PREVIEW_READY_MESSAGE = previousPreviewMessage('READY');
56
59
  exports.DENEB_PREVIEW_FOCUS_MESSAGE = 'DENEB_PREVIEW_FOCUS_PAGE';
57
60
  exports.PREVIEW_FOCUS_MESSAGE = 'FIVORA_PREVIEW_FOCUS_PAGE';
58
61
  exports.LEGACY_PREVIEW_FOCUS_MESSAGE = previousPreviewMessage('FOCUS_PAGE');
59
- exports.DENEB_REFRESH_LIVE_CATALOG_EVENT = 'deneb:refresh-live-catalog';
60
62
  exports.PREVIEW_FIELD_ATTRIBUTE = 'data-preview-field-path';
61
63
  var core_2 = require("@deneb-ui/core");
62
64
  Object.defineProperty(exports, "STYLE_PATCH_MESSAGE", { enumerable: true, get: function () { return core_2.STYLE_PATCH_MESSAGE; } });
@@ -368,9 +370,6 @@ function SiteDataProvider({ children, initialSiteData, fallbackSiteData, liveCat
368
370
  }
369
371
  return {
370
372
  ...current,
371
- liveCatalogUpdatedAt: typeof live.updatedAt === 'string'
372
- ? live.updatedAt
373
- : new Date().toISOString(),
374
373
  shop: nextShop,
375
374
  merchant: nextMerchant,
376
375
  reviews: nextReviews ?? current.reviews,
@@ -393,36 +392,6 @@ function SiteDataProvider({ children, initialSiteData, fallbackSiteData, liveCat
393
392
  ...(currentContent?.store && typeof currentContent.store === 'object' && nextProducts !== undefined
394
393
  ? { store: { ...currentContent.store, products: nextProducts } }
395
394
  : {}),
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
- : {}),
426
395
  ...(currentHome || Object.keys(nextHome).length > 0
427
396
  ? {
428
397
  home: {
@@ -493,19 +462,9 @@ function SiteDataProvider({ children, initialSiteData, fallbackSiteData, liveCat
493
462
  void loadLiveCatalog();
494
463
  }
495
464
  };
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
- };
504
465
  void loadLiveCatalog();
505
466
  window.addEventListener("focus", refreshIfStale);
506
467
  window.addEventListener("online", refreshIfStale);
507
- window.addEventListener('pageshow', refreshIfStale);
508
- window.addEventListener(exports.DENEB_REFRESH_LIVE_CATALOG_EVENT, refreshNow);
509
468
  document.addEventListener("visibilitychange", refreshIfStale);
510
469
  return () => {
511
470
  stopped = true;
@@ -514,8 +473,6 @@ function SiteDataProvider({ children, initialSiteData, fallbackSiteData, liveCat
514
473
  window.clearTimeout(retryTimer);
515
474
  window.removeEventListener("focus", refreshIfStale);
516
475
  window.removeEventListener("online", refreshIfStale);
517
- window.removeEventListener('pageshow', refreshIfStale);
518
- window.removeEventListener(exports.DENEB_REFRESH_LIVE_CATALOG_EVENT, refreshNow);
519
476
  document.removeEventListener("visibilitychange", refreshIfStale);
520
477
  };
521
478
  }, [liveCatalogEndpoint, siteSlug, initialSiteData]);
@@ -931,6 +888,27 @@ function normalizeProductItem(p) {
931
888
  const explicitMax = p.maxPrice ?? customData.maxPrice;
932
889
  const explicitRange = p.priceRange ?? customData.priceRange;
933
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;
934
912
  return {
935
913
  ...customData,
936
914
  ...p,
@@ -938,6 +916,12 @@ function normalizeProductItem(p) {
938
916
  title: titleCandidate,
939
917
  productName: (titleCandidate ?? nameCandidate),
940
918
  itemTitle: (titleCandidate ?? nameCandidate),
919
+ isAvailable: isAvailableCandidate,
920
+ inStock: isAvailableCandidate,
921
+ available: isAvailableCandidate,
922
+ isActive: isActiveCandidate,
923
+ active: isActiveCandidate,
924
+ showPrice: showPriceCandidate,
941
925
  minPrice: explicitMin !== undefined ? explicitMin : undefined,
942
926
  priceMin: (explicitMin ?? p.priceMin) !== undefined ? (explicitMin ?? p.priceMin) : undefined,
943
927
  maxPrice: explicitMax !== undefined ? explicitMax : undefined,
@@ -991,6 +975,8 @@ function normalizeProductItem(p) {
991
975
  }
992
976
  /**
993
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.
994
980
  */
995
981
  function normalizeServiceItem(s) {
996
982
  if (!isRecord(s))
@@ -1008,13 +994,90 @@ function normalizeServiceItem(s) {
1008
994
  (Array.isArray(s.images) ? s.images[0] : undefined) ??
1009
995
  (Array.isArray(s.additionalImages) ? s.additionalImages[0] : undefined);
1010
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;
1011
1068
  return {
1012
1069
  ...customData,
1013
1070
  ...s,
1014
1071
  name: nameCandidate,
1015
1072
  title: titleCandidate,
1073
+ isActive: isActiveCandidate,
1074
+ active: isActiveCandidate,
1075
+ isAvailable: isActiveCandidate,
1076
+ available: isActiveCandidate,
1077
+ showPrice: showPriceCandidate,
1016
1078
  ...(descCandidate !== undefined ? { description: descCandidate, desc: descCandidate, details: descCandidate } : {}),
1017
1079
  ...(imageCandidate !== undefined ? { imageUrl: imageCandidate, image: imageCandidate, photo: imageCandidate } : {}),
1080
+ ...(badgeCandidate !== undefined ? { badge: String(badgeCandidate), tag: String(badgeCandidate) } : {}),
1018
1081
  ...(catalogImages.length > 0
1019
1082
  ? {
1020
1083
  gallery: catalogImages,
@@ -1022,20 +1085,30 @@ function normalizeServiceItem(s) {
1022
1085
  additionalImages: catalogImages.slice(1),
1023
1086
  }
1024
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 } : {}),
1025
1105
  };
1026
1106
  }
1027
- function useProducts(fallback = []) {
1107
+ function useProducts(fallback = [], options) {
1028
1108
  const siteData = useSiteData();
1029
1109
  const content = isRecord(siteData?.content) ? siteData.content : null;
1030
1110
  if (!content)
1031
1111
  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
- }
1039
1112
  let rawList = null;
1040
1113
  const candidates = [
1041
1114
  content.products,
@@ -1057,23 +1130,26 @@ function useProducts(fallback = []) {
1057
1130
  }
1058
1131
  if (!rawList)
1059
1132
  return fallback;
1060
- 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;
1061
1141
  }
1062
1142
  /**
1063
1143
  * Hook to retrieve services cleanly from SiteData, supporting both
1064
1144
  * top-level content.services and nested content.home.services.
1145
+ * Supports optional { activeOnly: true } filtering to mirror product availability.
1065
1146
  */
1066
- function useServices(fallback = []) {
1147
+ function useServices(fallback = [], options) {
1067
1148
  const siteData = useSiteData();
1068
1149
  const content = isRecord(siteData?.content) ? siteData.content : null;
1069
1150
  if (!content)
1070
1151
  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
- }
1152
+ let rawList = null;
1077
1153
  const candidates = [
1078
1154
  content.services,
1079
1155
  content.servicesPage?.services,
@@ -1084,10 +1160,15 @@ function useServices(fallback = []) {
1084
1160
  ];
1085
1161
  for (const candidate of candidates) {
1086
1162
  if (Array.isArray(candidate) && candidate.length > 0) {
1087
- return candidate.map(normalizeServiceItem);
1163
+ rawList = candidate;
1164
+ break;
1088
1165
  }
1089
1166
  }
1090
- 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;
1091
1172
  }
1092
1173
  /**
1093
1174
  * Hook to access official Fivora backend API endpoints (catalogUrl, contactUrl, analyticsUrl).
@@ -1314,3 +1395,79 @@ function useSyncedBranding(fallback = {}) {
1314
1395
  faviconUrl,
1315
1396
  };
1316
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/ui",
3
- "version": "2.0.127",
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.127"
53
+ "@deneb-ui/core": "^2.0.128"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "react": "^18.0.0 || ^19.0.0",