@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 +66 -1
- package/dist/EditableBox.d.ts +3 -1
- package/dist/EditableBox.js +23 -2
- package/dist/EditableFooter.js +11 -23
- package/dist/EditableIcon.js +7 -7
- package/dist/EditableProductCard.d.ts +1 -0
- package/dist/EditableProductCard.js +2 -8
- package/dist/EditableProductDetail.d.ts +1 -0
- package/dist/EditableProductDetail.js +6 -8
- package/dist/EditableServiceCard.d.ts +3 -0
- package/dist/EditableServiceCard.js +3 -1
- package/dist/EditableUniversalCard.d.ts +5 -3
- package/dist/PlatformProductDetail.js +3 -8
- package/dist/ProductQuickView.d.ts +2 -0
- package/dist/ProductQuickView.js +3 -2
- package/dist/SiteDataProvider.d.ts +1 -0
- package/dist/SiteDataProvider.js +87 -3
- package/dist/utils/catalogMedia.d.ts +18 -0
- package/dist/utils/catalogMedia.js +43 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +2 -2
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)**.
|
package/dist/EditableBox.d.ts
CHANGED
|
@@ -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;
|
package/dist/EditableBox.js
CHANGED
|
@@ -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
|
}
|
package/dist/EditableFooter.js
CHANGED
|
@@ -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",
|
|
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"
|
|
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",
|
|
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"
|
|
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.
|
|
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",
|
|
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"
|
|
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-
|
|
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",
|
|
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',
|
package/dist/EditableIcon.js
CHANGED
|
@@ -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
|
-
|
|
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, ...(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
@@ -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
|
|
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)
|
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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,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
|
|
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
|
|
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
|
-
* - 🍽️
|
|
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
|
-
* - 💼
|
|
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 =
|
|
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
|
: {}),
|
package/dist/ProductQuickView.js
CHANGED
|
@@ -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(
|
|
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 =
|
|
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>;
|
package/dist/SiteDataProvider.js
CHANGED
|
@@ -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 ??
|
|
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
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/ui",
|
|
3
|
-
"version": "2.0.
|
|
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.
|
|
53
|
+
"@deneb-ui/core": "^2.0.127"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"react": "^18.0.0 || ^19.0.0",
|