@deneb-ui/ui 2.0.125 → 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" },
@@ -735,6 +743,63 @@ export function FullStorefrontDemo() {
735
743
 
736
744
  ---
737
745
 
746
+ ## EditableBox container styles
747
+
748
+ Use `stylePath` to give a container its own card style target without treating
749
+ its child text as a single content field. Contact starter cards use
750
+ `contact.informationCard` and `contact.formCard` in both templates. For example,
751
+ `styles['contact.informationCard'] = { backgroundColor: '#f0fdf4', paddingTop: 0,
752
+ paddingBottom: 24, paddingLeft: 24, paddingRight: 24, marginTop: 8, gap: 16 }`.
753
+ These are root `styles` entries, not new content fields. Unedited boxes retain
754
+ their existing props and class-based appearance. Saved styles override defaults;
755
+ legacy content-suffix spacing remains supported. Gap affects a box's direct
756
+ flex/grid children, not a nested form's internal spacing.
757
+
758
+ In Local Lab, select the card's empty background/padding area to target its
759
+ container. Text remains independently editable. Test background, each padding
760
+ edge (including zero), margins, and flex/grid gap, then save/reload. The bridge
761
+ forwards card style metadata; actual Inspector controls depend on the connected
762
+ Fivora editor. Run `node scripts/box-editing.test.cjs` after building core/UI.
763
+
764
+ ## Footer headings and contact icons
765
+
766
+ Built-in `EditableIcon` glyphs with a `fieldPath` now use an HTML span as their
767
+ editing boundary, matching the existing custom-icon branches. The SVG remains
768
+ inside it with its original size and SVG attributes; the field and icon-picker
769
+ markers occur only on the wrapper. This lets HTML-based editor selection resolve
770
+ clicks on SVG paths to the icon field rather than a neighboring text field.
771
+ Icons without a `fieldPath` continue to render a bare SVG.
772
+
773
+ `Footer` reuses `EditableText` for `common.footerExploreHeading` and
774
+ `common.footerContactHeading`, including text style targets. It reuses
775
+ `EditableIcon` for `common.footerEmailIcon`, `common.footerPhoneIcon`, and
776
+ `common.footerLocationIcon`. These are content-relative paths under
777
+ `content.common`; default icon names are `mail`, `phone`, and `map-pin`.
778
+ The starter schemas declare the icons as text with `control: 'icon-picker'`.
779
+ Contact icons appear alongside their corresponding nonempty contact values.
780
+ Missing fields retain the original headings/icons; blank headings remain
781
+ editable through placeholders. Verify heading text/typography and icon selection
782
+ in the Lab, then save/reload. Contact values and destinations are independent
783
+ of icon selection.
784
+
785
+ ## Footer newsletter description
786
+
787
+ The newsletter also exposes `content.common.newsletterSubscribeLabel` and
788
+ `content.common.newsletterEmailPlaceholder`. The button label reuses
789
+ `EditableText`; the email input uses the existing input-placeholder field binding
790
+ pattern. The visitor's entered email remains separate from the editable
791
+ placeholder. Both starter schemas declare these as text fields. Original copy
792
+ is the fallback when fields are missing; blank placeholder values are preserved.
793
+ Verify edits and save/reload in Local Lab, plus newsletter submission in Preview.
794
+
795
+ `Footer` / `EditableFooter` reuses `EditableText` for
796
+ `content.common.newsletterDescription`. Its field and text style target are
797
+ `common.newsletterDescription`; saved typography lives at
798
+ `styles['common.newsletterDescription']`. Missing data uses the original copy;
799
+ an empty string keeps an editable placeholder. Both starter templates declare
800
+ the field as a textarea. With newsletters enabled, edit the paragraph in the
801
+ Lab, save, and reload to verify persistence.
802
+
738
803
  ## Authors
739
804
 
740
805
  Created and maintained by **[Chamika Gayashan](https://github.com/chamikathereal)** and **[Induranga Kawishwara](https://github.com/Induranga-kawishwara)**.
@@ -5,6 +5,8 @@ export type BoxSpacing = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | (st
5
5
  export type BoxAspectRatio = '1/1' | '16/9' | '4/3' | '3/4' | '9/16' | '2/3' | '3/2' | 'square' | 'video' | 'portrait' | 'landscape' | (string & {});
6
6
  export interface EditableBoxProps extends React.HTMLAttributes<HTMLElement> {
7
7
  as?: React.ElementType;
8
+ /** Independent container style target; does not mark child content as one text field. */
9
+ stylePath?: string;
8
10
  /**
9
11
  * Field path or item path in siteData.
10
12
  */
@@ -53,4 +55,4 @@ export interface EditableBoxProps extends React.HTMLAttributes<HTMLElement> {
53
55
  * EditableBox is a flexible, smart container ("div-like") component with built-in
54
56
  * width/height resizing, aspect-ratio controls, and auto-balancing for fivora cards.
55
57
  */
56
- export declare function EditableBox({ as: Component, id, 'data-preview-field-path': previewFieldPath, 'data-preview-item-path': previewItemPath, 'data-preview-static': previewStatic, width, minWidth, maxWidth, height, minHeight, maxHeight, aspectRatio, balance, grow, shrink, padding, margin, bg, color, radius, shadow, border, display, direction, wrap, align, justify, gap, overflow, style, children, className, ...props }: EditableBoxProps): React.JSX.Element;
58
+ export declare function EditableBox({ as: Component, stylePath, id, 'data-preview-field-path': previewFieldPath, 'data-preview-item-path': previewItemPath, 'data-preview-static': previewStatic, width, minWidth, maxWidth, height, minHeight, maxHeight, aspectRatio, balance, grow, shrink, padding, margin, bg, color, radius, shadow, border, display, direction, wrap, align, justify, gap, overflow, style, children, className, ...props }: EditableBoxProps): React.JSX.Element;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EditableBox = EditableBox;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const SiteDataProvider_1 = require("./SiteDataProvider");
6
+ const useComponentStyle_1 = require("./hooks/useComponentStyle");
6
7
  const RADIUS_MAP = {
7
8
  none: '0px',
8
9
  sm: '4px',
@@ -46,7 +47,7 @@ const ASPECT_MAP = {
46
47
  * EditableBox is a flexible, smart container ("div-like") component with built-in
47
48
  * width/height resizing, aspect-ratio controls, and auto-balancing for fivora cards.
48
49
  */
49
- function EditableBox({ as: Component = 'div', id, 'data-preview-field-path': previewFieldPath, 'data-preview-item-path': previewItemPath, 'data-preview-static': previewStatic, width, minWidth, maxWidth, height, minHeight, maxHeight, aspectRatio, balance, grow, shrink, padding, margin, bg, color, radius, shadow, border, display, direction, wrap, align, justify, gap, overflow, style, children, className = '', ...props }) {
50
+ function EditableBox({ as: Component = 'div', stylePath, id, 'data-preview-field-path': previewFieldPath, 'data-preview-item-path': previewItemPath, 'data-preview-static': previewStatic, width, minWidth, maxWidth, height, minHeight, maxHeight, aspectRatio, balance, grow, shrink, padding, margin, bg, color, radius, shadow, border, display, direction, wrap, align, justify, gap, overflow, style, children, className = '', ...props }) {
50
51
  const resolvedRadius = radius !== undefined ? (RADIUS_MAP[String(radius)] || String(radius)) : undefined;
51
52
  const resolvedShadow = shadow ? (SHADOW_MAP[shadow] || shadow) : undefined;
52
53
  const resolvedPadding = padding !== undefined ? (SPACING_MAP[String(padding)] || String(padding)) : undefined;
@@ -72,6 +73,24 @@ function EditableBox({ as: Component = 'div', id, 'data-preview-field-path': pre
72
73
  ? border
73
74
  : undefined;
74
75
  const path = previewFieldPath || id || previewItemPath;
76
+ const effectiveStylePath = stylePath ?? path;
77
+ const { cssVars } = (0, useComponentStyle_1.useComponentStyle)(effectiveStylePath, 'card');
78
+ const variables = cssVars;
79
+ const containerStyle = {};
80
+ // Only set persisted values: unedited CSS classes and existing props keep their defaults.
81
+ const bindings = {
82
+ backgroundColor: '--deneb-card-bg',
83
+ paddingTop: '--deneb-card-pt', paddingBottom: '--deneb-card-pb',
84
+ paddingLeft: '--deneb-card-pl', paddingRight: '--deneb-card-pr',
85
+ marginTop: '--deneb-card-margin-top', marginBottom: '--deneb-card-margin-bottom',
86
+ marginLeft: '--deneb-card-margin-left', marginRight: '--deneb-card-margin-right',
87
+ gap: '--deneb-card-gap',
88
+ };
89
+ for (const [property, variable] of Object.entries(bindings)) {
90
+ if (variables[variable] !== undefined) {
91
+ Object.assign(containerStyle, { [property]: `var(${variable})` });
92
+ }
93
+ }
75
94
  const dynamicStyle = (0, SiteDataProvider_1.useFieldStyle)(path);
76
95
  const dynamicMarginTop = dynamicStyle?.spacingTop !== undefined && dynamicStyle?.spacingTop !== '' ? `${dynamicStyle.spacingTop}px` : undefined;
77
96
  const dynamicMarginBottom = dynamicStyle?.spacingBottom !== undefined && dynamicStyle?.spacingBottom !== '' ? `${dynamicStyle.spacingBottom}px` : undefined;
@@ -108,6 +127,8 @@ function EditableBox({ as: Component = 'div', id, 'data-preview-field-path': pre
108
127
  ...(dynamicMarginLeft ? { marginLeft: dynamicMarginLeft } : {}),
109
128
  ...(dynamicMarginRight ? { marginRight: dynamicMarginRight } : {}),
110
129
  ...(dynamicStyle?.color ? { color: String(dynamicStyle.color) } : {}),
130
+ ...cssVars,
131
+ ...containerStyle,
111
132
  };
112
- return ((0, jsx_runtime_1.jsx)(Component, { "data-preview-field-path": previewFieldPath || id, "data-preview-item-path": previewItemPath, "data-preview-static": previewStatic, className: `editable-box ${className}`.trim(), style: boxStyle, ...props, children: children }));
133
+ return ((0, jsx_runtime_1.jsx)(Component, { "data-preview-field-path": previewFieldPath || id, "data-preview-item-path": previewItemPath, "data-preview-static": previewStatic, "data-preview-style-target": effectiveStylePath, "data-preview-style-type": effectiveStylePath ? 'card' : undefined, className: `editable-box ${className}`.trim(), style: boxStyle, ...props, children: children }));
113
134
  }
@@ -7,6 +7,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
7
7
  const react_1 = require("react");
8
8
  const SiteDataProvider_1 = require("./SiteDataProvider");
9
9
  const EditableText_1 = require("./EditableText");
10
+ const EditableIcon_1 = require("./EditableIcon");
10
11
  const DEFAULT_FOOTER_PAGES = [
11
12
  { id: 'home', label: 'Home', route: '/' },
12
13
  { id: 'about_us', label: 'About', route: '/about_us' },
@@ -226,14 +227,9 @@ function EditableFooter({ defaultLinks = DEFAULT_FOOTER_PAGES, links, basePath =
226
227
  textDecoration: 'none',
227
228
  transition: 'all 0.15s ease',
228
229
  }, className: "w-8 h-8 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-400 hover:text-white hover:border-slate-700 hover:bg-slate-800 transition-all", children: iconConfig.icon }, platformKey));
229
- }) })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h4", { "data-preview-static": "footer-explore-heading", style: {
230
- fontSize: '0.8125rem',
231
- fontWeight: 700,
232
- textTransform: 'uppercase',
233
- letterSpacing: '0.08em',
234
- color: '#f1f5f9',
230
+ }) })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "h4", fieldPath: "common.footerExploreHeading", defaultValue: common.footerExploreHeading ?? 'Explore', placeholder: "Add a navigation heading", size: "0.8125rem", weight: 700, transform: "uppercase", letterSpacing: "0.08em", color: "#f1f5f9", style: {
235
231
  marginBottom: '1.25rem',
236
- }, className: "text-xs font-bold uppercase tracking-wider text-slate-200 mb-5", children: "Explore" }), (0, jsx_runtime_1.jsx)("ul", { style: {
232
+ }, className: "text-xs font-bold uppercase tracking-wider text-slate-200 mb-5" }), (0, jsx_runtime_1.jsx)("ul", { style: {
237
233
  listStyle: 'none',
238
234
  padding: 0,
239
235
  margin: 0,
@@ -265,20 +261,15 @@ function EditableFooter({ defaultLinks = DEFAULT_FOOTER_PAGES, links, basePath =
265
261
  }
266
262
  // Unselected route marker fallback (visible for strict schema coverage without route link)
267
263
  return ((0, jsx_runtime_1.jsx)("li", { className: "unavailable", children: (0, jsx_runtime_1.jsx)("span", { "data-target-page": link.id, children: (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `common.navLabels.${link.id}`, children: label }) }) }, link.id));
268
- }) })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h4", { "data-preview-static": "footer-contact-heading", style: {
269
- fontSize: '0.8125rem',
270
- fontWeight: 700,
271
- textTransform: 'uppercase',
272
- letterSpacing: '0.08em',
273
- color: '#f1f5f9',
264
+ }) })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "h4", fieldPath: "common.footerContactHeading", defaultValue: common.footerContactHeading ?? 'Contact & Support', placeholder: "Add a contact heading", size: "0.8125rem", weight: 700, transform: "uppercase", letterSpacing: "0.08em", color: "#f1f5f9", style: {
274
265
  marginBottom: '1.25rem',
275
- }, className: "text-xs font-bold uppercase tracking-wider text-slate-200 mb-5", children: "Contact & Support" }), (0, jsx_runtime_1.jsxs)("div", { style: {
266
+ }, className: "text-xs font-bold uppercase tracking-wider text-slate-200 mb-5" }), (0, jsx_runtime_1.jsxs)("div", { style: {
276
267
  display: 'flex',
277
268
  flexDirection: 'column',
278
269
  gap: '0.875rem',
279
270
  fontSize: '0.875rem',
280
271
  color: '#94a3b8',
281
- }, className: "flex flex-col gap-3.5 text-sm text-slate-400", children: [emailContact && ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: [(0, jsx_runtime_1.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-slate-500", children: [(0, jsx_runtime_1.jsx)("rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" }), (0, jsx_runtime_1.jsx)("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" })] }), (0, jsx_runtime_1.jsx)("a", { href: `mailto:${emailContact}`, "data-preview-field-path": "contact.email", className: "hover:text-white transition-colors", style: { color: 'inherit', textDecoration: 'none' }, children: emailContact })] })), phone && ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: [(0, jsx_runtime_1.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-slate-500", children: (0, jsx_runtime_1.jsx)("path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" }) }), (0, jsx_runtime_1.jsx)("a", { href: `tel:${phone}`, "data-preview-field-path": "contact.phone", className: "hover:text-white transition-colors", style: { color: 'inherit', textDecoration: 'none' }, children: phone })] })), address && ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'flex-start', gap: '0.5rem' }, children: [(0, jsx_runtime_1.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-slate-500 mt-0.5", children: [(0, jsx_runtime_1.jsx)("path", { d: "M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" }), (0, jsx_runtime_1.jsx)("circle", { cx: "12", cy: "10", r: "3" })] }), (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": "contact.address", children: address })] })), hasContact && ((0, jsx_runtime_1.jsx)("div", { style: { marginTop: '0.5rem' }, children: (0, jsx_runtime_1.jsxs)("a", { href: whatsappHref, target: "_blank", rel: "noopener noreferrer", "data-preview-field-path": "contact.whatsapp", "aria-label": "Direct message on WhatsApp", style: {
272
+ }, className: "flex flex-col gap-3.5 text-sm text-slate-400", children: [emailContact && ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: [(0, jsx_runtime_1.jsx)(EditableIcon_1.EditableIcon, { name: "mail", fieldPath: "common.footerEmailIcon", size: 14, className: "text-slate-500" }), (0, jsx_runtime_1.jsx)("a", { href: `mailto:${emailContact}`, "data-preview-field-path": "contact.email", className: "hover:text-white transition-colors", style: { color: 'inherit', textDecoration: 'none' }, children: emailContact })] })), phone && ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: [(0, jsx_runtime_1.jsx)(EditableIcon_1.EditableIcon, { name: "phone", fieldPath: "common.footerPhoneIcon", size: 14, className: "text-slate-500" }), (0, jsx_runtime_1.jsx)("a", { href: `tel:${phone}`, "data-preview-field-path": "contact.phone", className: "hover:text-white transition-colors", style: { color: 'inherit', textDecoration: 'none' }, children: phone })] })), address && ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'flex-start', gap: '0.5rem' }, children: [(0, jsx_runtime_1.jsx)(EditableIcon_1.EditableIcon, { name: "map-pin", fieldPath: "common.footerLocationIcon", size: 14, className: "text-slate-500 mt-0.5" }), (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": "contact.address", children: address })] })), hasContact && ((0, jsx_runtime_1.jsx)("div", { style: { marginTop: '0.5rem' }, children: (0, jsx_runtime_1.jsxs)("a", { href: whatsappHref, target: "_blank", rel: "noopener noreferrer", "data-preview-field-path": "contact.whatsapp", "aria-label": "Direct message on WhatsApp", style: {
282
273
  display: 'inline-flex',
283
274
  alignItems: 'center',
284
275
  gap: '0.375rem',
@@ -298,12 +289,9 @@ function EditableFooter({ defaultLinks = DEFAULT_FOOTER_PAGES, links, basePath =
298
289
  letterSpacing: '0.08em',
299
290
  color: '#f1f5f9',
300
291
  marginBottom: '1rem',
301
- }, className: "text-xs font-bold uppercase tracking-wider text-slate-200 mb-4", children: footerHeading }), showNewsletter && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3", children: [(0, jsx_runtime_1.jsx)("p", { "data-preview-static": "newsletter-description", style: {
302
- fontSize: '0.8125rem',
303
- color: '#94a3b8',
304
- lineHeight: 1.5,
292
+ }, className: "text-xs font-bold uppercase tracking-wider text-slate-200 mb-4", children: footerHeading }), showNewsletter && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3", children: [(0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "p", fieldPath: "common.newsletterDescription", defaultValue: common.newsletterDescription ?? 'Join our exclusive community for releases, updates, and special perks.', placeholder: "Add a newsletter description", size: "0.8125rem", color: "#94a3b8", lineHeight: 1.5, style: {
305
293
  margin: 0,
306
- }, className: "text-xs text-slate-400 leading-relaxed", children: "Join our exclusive community for releases, updates, and special perks." }), subscribed ? ((0, jsx_runtime_1.jsx)("p", { "data-preview-static": "newsletter-success", style: {
294
+ }, className: "text-xs text-slate-400 leading-relaxed" }), subscribed ? ((0, jsx_runtime_1.jsx)("p", { "data-preview-static": "newsletter-success", style: {
307
295
  padding: '0.625rem 0.875rem',
308
296
  borderRadius: '8px',
309
297
  backgroundColor: 'rgba(34, 197, 94, 0.12)',
@@ -319,7 +307,7 @@ function EditableFooter({ defaultLinks = DEFAULT_FOOTER_PAGES, links, basePath =
319
307
  display: 'flex',
320
308
  alignItems: 'center',
321
309
  position: 'relative',
322
- }, children: (0, jsx_runtime_1.jsx)("input", { type: "email", required: true, "data-preview-static": "newsletter-input", placeholder: "Enter your email", value: email, onChange: (e) => setEmail(e.target.value), style: {
310
+ }, children: (0, jsx_runtime_1.jsx)("input", { type: "email", required: true, "data-preview-field-path": "common.newsletterEmailPlaceholder", placeholder: common.newsletterEmailPlaceholder ?? 'Enter your email', value: email, onChange: (e) => setEmail(e.target.value), style: {
323
311
  width: '100%',
324
312
  backgroundColor: '#0f172a', // slate-900
325
313
  border: '1px solid #1e293b', // slate-800
@@ -328,7 +316,7 @@ function EditableFooter({ defaultLinks = DEFAULT_FOOTER_PAGES, links, basePath =
328
316
  fontSize: '0.8125rem',
329
317
  color: '#ffffff',
330
318
  outline: 'none',
331
- }, className: "w-full bg-slate-900 border border-slate-800 rounded-lg px-3 py-2 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-all" }) }), (0, jsx_runtime_1.jsxs)("button", { type: "submit", "data-preview-static": "newsletter-btn", style: {
319
+ }, className: "w-full bg-slate-900 border border-slate-800 rounded-lg px-3 py-2 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-all" }) }), (0, jsx_runtime_1.jsxs)("button", { type: "submit", style: {
332
320
  backgroundColor: '#ffffff',
333
321
  color: '#0f172a',
334
322
  border: 'none',
@@ -338,7 +326,7 @@ function EditableFooter({ defaultLinks = DEFAULT_FOOTER_PAGES, links, basePath =
338
326
  fontWeight: 600,
339
327
  cursor: 'pointer',
340
328
  transition: 'background-color 0.15s ease',
341
- }, className: "bg-white text-slate-900 hover:bg-slate-200 rounded-lg px-3.5 py-2 text-xs font-semibold transition-colors flex items-center justify-center gap-1.5 shadow-sm", children: ["Subscribe", (0, jsx_runtime_1.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("path", { d: "M5 12h14M12 5l7 7-7 7" }) })] })] }))] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { style: {
329
+ }, className: "bg-white text-slate-900 hover:bg-slate-200 rounded-lg px-3.5 py-2 text-xs font-semibold transition-colors flex items-center justify-center gap-1.5 shadow-sm", children: [(0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "span", fieldPath: "common.newsletterSubscribeLabel", defaultValue: common.newsletterSubscribeLabel ?? 'Subscribe', placeholder: "Subscribe" }), (0, jsx_runtime_1.jsx)("svg", { "data-preview-static": "newsletter-submit-arrow", "aria-hidden": "true", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("path", { d: "M5 12h14M12 5l7 7-7 7" }) })] })] }))] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { style: {
342
330
  paddingTop: '2rem',
343
331
  borderTop: '1px solid rgba(255, 255, 255, 0.08)',
344
332
  display: 'flex',
@@ -233,17 +233,17 @@ const EditableIcon = ({ name = 'circle', fieldPath, size = 16, strokeWidth = 2,
233
233
  }
234
234
  // 3. Built-in SVG lookup
235
235
  const renderer = BUILTIN_ICONS[normalized] || BUILTIN_ICONS[fallback] || BUILTIN_ICONS.circle;
236
- return ((0, jsx_runtime_1.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", width: size, height: size, className: `deneb-editable-icon ${className}`, style: {
236
+ const svg = ((0, jsx_runtime_1.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", width: size, height: size, className: `deneb-editable-icon ${className}`, style: {
237
237
  display: 'inline-block',
238
238
  verticalAlign: 'middle',
239
239
  flexShrink: 0,
240
240
  ...style,
241
- }, "aria-hidden": !title, role: title ? 'img' : undefined, ...(fieldPath ? {
242
- 'data-preview-field-path': fieldPath,
243
- 'data-preview-control': 'icon-picker',
244
- 'data-preview-icon-type': 'lucide',
245
- 'data-preview-icon': activeName,
246
- } : {}), ...svgProps, children: [title && (0, jsx_runtime_1.jsx)("title", { children: title }), renderer({ strokeWidth })] }));
241
+ }, "aria-hidden": !title, role: title ? 'img' : undefined, ...svgProps, children: [title && (0, jsx_runtime_1.jsx)("title", { children: title }), renderer({ strokeWidth })] }));
242
+ if (!fieldPath)
243
+ return svg;
244
+ // Match the HTML selection target used by customIcon/iconMap above.
245
+ // SVG descendants bubble into this target even in HTMLElement-only bridges.
246
+ return ((0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": fieldPath, "data-preview-control": "icon-picker", "data-preview-icon-type": "lucide", "data-preview-icon": activeName, style: { display: 'inline-flex', alignItems: 'center', flexShrink: 0, lineHeight: 1 }, children: svg }));
247
247
  };
248
248
  exports.EditableIcon = EditableIcon;
249
249
  // Aliases for developer convenience
@@ -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.125",
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.125"
53
+ "@deneb-ui/core": "^2.0.127"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "react": "^18.0.0 || ^19.0.0",