@deneb-ui/ui 2.0.38 → 2.0.40
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/dist/EditableProductCard.d.ts +20 -0
- package/dist/EditableProductCard.js +5 -0
- package/dist/EditableProductDetail.d.ts +17 -0
- package/dist/EditableProductDetail.js +13 -4
- package/dist/SiteDataProvider.d.ts +65 -4
- package/dist/SiteDataProvider.js +152 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/productOptions.d.ts +74 -0
- package/dist/utils/productOptions.js +174 -0
- package/package.json +2 -2
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { MeasurementUnit } from './utils/productOptions';
|
|
2
3
|
export interface ProductItem {
|
|
3
4
|
id?: string | number;
|
|
4
5
|
name?: string;
|
|
5
6
|
title?: string;
|
|
7
|
+
brand?: string;
|
|
6
8
|
price?: string | number;
|
|
7
9
|
originalPrice?: string | number;
|
|
8
10
|
description?: string;
|
|
9
11
|
category?: string;
|
|
10
12
|
imageUrl?: string;
|
|
11
13
|
image?: string;
|
|
14
|
+
images?: string[];
|
|
12
15
|
badge?: string;
|
|
13
16
|
rating?: number | string;
|
|
17
|
+
reviewsCount?: number | string;
|
|
18
|
+
isNew?: boolean;
|
|
19
|
+
isBestSeller?: boolean;
|
|
20
|
+
unit?: MeasurementUnit;
|
|
21
|
+
measurement?: string;
|
|
22
|
+
optionsLabel?: string;
|
|
23
|
+
options?: (string | number)[] | string;
|
|
24
|
+
optionsText?: string;
|
|
25
|
+
sizes?: (string | number)[] | string;
|
|
26
|
+
sizesText?: string;
|
|
27
|
+
sizesLabel?: string;
|
|
28
|
+
colors?: Array<string | {
|
|
29
|
+
name: string;
|
|
30
|
+
hex?: string;
|
|
31
|
+
}> | string;
|
|
32
|
+
colorsText?: string;
|
|
33
|
+
colorsLabel?: string;
|
|
14
34
|
[key: string]: unknown;
|
|
15
35
|
}
|
|
16
36
|
export type ProductCardVariant = 'modern-glass' | 'classic' | 'minimal' | 'horizontal';
|
|
@@ -102,6 +102,11 @@ function EditableProductCard({ itemPath, product, cardVariant = 'modern-glass',
|
|
|
102
102
|
fontSize: '0.875rem',
|
|
103
103
|
textDecoration: 'line-through',
|
|
104
104
|
color: 'var(--muted-text, #94a3b8)',
|
|
105
|
+
} })) : null, product?.measurement ? ((0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "span", id: `${itemPath}.measurement`, defaultValue: `/ ${String(product.measurement)}`, style: {
|
|
106
|
+
fontSize: '0.8rem',
|
|
107
|
+
fontWeight: 500,
|
|
108
|
+
color: 'var(--muted-text, #64748b)',
|
|
109
|
+
marginLeft: '0.25rem',
|
|
105
110
|
} })) : null] })), actionSlot ? ((0, jsx_runtime_1.jsx)("div", { children: actionSlot })) : ((0, jsx_runtime_1.jsx)("button", { type: "button", style: {
|
|
106
111
|
backgroundColor: 'var(--brand-color, #2563eb)',
|
|
107
112
|
color: '#ffffff',
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { MeasurementUnit } from './utils/productOptions';
|
|
2
3
|
export interface ProductDetailItem {
|
|
3
4
|
id?: string;
|
|
4
5
|
name?: string;
|
|
5
6
|
title?: string;
|
|
7
|
+
brand?: string;
|
|
8
|
+
category?: string;
|
|
6
9
|
price?: string | number;
|
|
7
10
|
originalPrice?: string | number;
|
|
8
11
|
description?: string;
|
|
@@ -23,6 +26,20 @@ export interface ProductDetailItem {
|
|
|
23
26
|
relatedTitle?: string;
|
|
24
27
|
whatsappNumber?: string;
|
|
25
28
|
whatsappMessage?: string;
|
|
29
|
+
unit?: MeasurementUnit;
|
|
30
|
+
measurement?: string;
|
|
31
|
+
optionsLabel?: string;
|
|
32
|
+
options?: (string | number)[];
|
|
33
|
+
optionsText?: string;
|
|
34
|
+
sizes?: (string | number)[] | string;
|
|
35
|
+
sizesText?: string;
|
|
36
|
+
sizesLabel?: string;
|
|
37
|
+
colors?: Array<string | {
|
|
38
|
+
name: string;
|
|
39
|
+
hex?: string;
|
|
40
|
+
}> | string;
|
|
41
|
+
colorsText?: string;
|
|
42
|
+
colorsLabel?: string;
|
|
26
43
|
[key: string]: unknown;
|
|
27
44
|
}
|
|
28
45
|
export interface EditableProductDetailProps extends React.HTMLAttributes<HTMLElement> {
|
|
@@ -4,6 +4,7 @@ exports.EditableProductDetail = EditableProductDetail;
|
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const react_1 = require("react");
|
|
6
6
|
const utils_1 = require("./utils");
|
|
7
|
+
const productOptions_1 = require("./utils/productOptions");
|
|
7
8
|
const urls_1 = require("./utils/urls");
|
|
8
9
|
/**
|
|
9
10
|
* EditableProductDetail is an elite, fully calibrated Single Product View component
|
|
@@ -26,8 +27,15 @@ function EditableProductDetail({ sectionPath = 'product', product, sizes = ['40'
|
|
|
26
27
|
const images = (product.gallery && product.gallery.length > 0)
|
|
27
28
|
? product.gallery
|
|
28
29
|
: [product.featuredImage || product.imageUrl || '/products/vanta-aero-x.jpg'];
|
|
30
|
+
const resolved = (0, productOptions_1.resolveProductOptions)(product);
|
|
31
|
+
const effectiveOptions = product.options || product.optionsText
|
|
32
|
+
? resolved.options
|
|
33
|
+
: (product.sizes || product.sizesText)
|
|
34
|
+
? resolved.options
|
|
35
|
+
: sizes;
|
|
36
|
+
const effectiveOptionsLabel = resolved.optionsLabel || 'Available Sizes';
|
|
29
37
|
const [activeImage, setActiveImage] = (0, react_1.useState)(images[0] || '');
|
|
30
|
-
const [selectedSize, setSelectedSize] = (0, react_1.useState)(
|
|
38
|
+
const [selectedSize, setSelectedSize] = (0, react_1.useState)(effectiveOptions[0] || '');
|
|
31
39
|
const [selectedColor, setSelectedColor] = (0, react_1.useState)(colors[0]?.name || '');
|
|
32
40
|
const [activeTab, setActiveTab] = (0, react_1.useState)('specs');
|
|
33
41
|
const name = String(product.name || product.title || 'Product Title');
|
|
@@ -39,14 +47,15 @@ function EditableProductDetail({ sectionPath = 'product', product, sizes = ['40'
|
|
|
39
47
|
const shippingTitle = String(product.shippingTitle || 'Shipping & Returns');
|
|
40
48
|
const shippingSummary = String(product.shippingSummary || 'Free Islandwide Delivery within 2-3 business days. Cash on delivery available.');
|
|
41
49
|
const shippingReturns = String(product.shippingReturns || '14-day hassle-free exchanges for unworn items in original packaging.');
|
|
50
|
+
const orderSnippet = resolved.formatOrderSnippet(selectedSize, selectedColor);
|
|
42
51
|
const resolvedWhatsappUrl = whatsappUrl || (product.whatsappNumber
|
|
43
|
-
? (0, urls_1.createWhatsAppUrl)(product.whatsappNumber, product.whatsappMessage || `Hi, I would like to order ${name}
|
|
52
|
+
? (0, urls_1.createWhatsAppUrl)(product.whatsappNumber, product.whatsappMessage || `Hi, I would like to order ${name}${orderSnippet ? ` ${orderSnippet}` : ''} - ${price}`)
|
|
44
53
|
: '');
|
|
45
54
|
return ((0, jsx_runtime_1.jsx)("section", { "data-preview-page-key": sectionPath, className: `editable-product-detail max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 ${className}`.trim(), style: style, ...props, children: (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-start", children: [(0, jsx_runtime_1.jsxs)("div", { className: "lg:col-span-7 flex flex-col-reverse md:flex-row gap-4", children: [images.length > 1 && ((0, jsx_runtime_1.jsx)("div", { className: "deneb-product-detail-gallery flex md:flex-col gap-3 overflow-x-auto md:overflow-y-auto max-h-[580px] pb-2 md:pb-0 scrollbar-none", children: images.map((img, idx) => ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveImage(img), className: `w-20 h-20 sm:w-24 sm:h-24 rounded-2xl overflow-hidden border-2 transition-all duration-200 flex-shrink-0 bg-slate-900/40 p-2 ${activeImage === img
|
|
46
55
|
? 'border-brand-primary scale-98 shadow-md'
|
|
47
56
|
: 'border-slate-800/80 opacity-60 hover:opacity-100 hover:border-slate-700'}`, children: (0, jsx_runtime_1.jsx)("img", { src: (0, utils_1.withBasePath)(img), alt: "", className: "w-full h-full object-contain" }) }, idx))) })), (0, jsx_runtime_1.jsxs)("div", { className: "relative flex-1 aspect-square rounded-3xl bg-gradient-to-b from-slate-900/50 to-slate-950/80 border border-slate-800/80 p-8 flex items-center justify-center overflow-hidden group", children: [badge && ((0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${sectionPath}.badge`, className: "absolute top-6 left-6 z-10 px-3.5 py-1 text-xs font-black tracking-wider uppercase rounded-full bg-lime-400 text-slate-950 shadow-lg", children: badge })), (0, jsx_runtime_1.jsx)("img", { "data-preview-field-path": `${sectionPath}.featuredImage`, src: (0, utils_1.withBasePath)(activeImage), alt: name, className: "w-full h-full object-contain transition-transform duration-500 group-hover:scale-105" })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "lg:col-span-5 flex flex-col justify-between", children: (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { "data-preview-field-path": `${sectionPath}.name`, className: "text-3xl sm:text-4xl font-extrabold tracking-tight text-white leading-tight", children: name }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-4 flex items-baseline gap-3", children: [(0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${sectionPath}.price`, className: "text-3xl font-black text-lime-400 tracking-tight", children: price }), product.originalPrice ? ((0, jsx_runtime_1.jsx)("span", { className: "text-lg font-medium text-slate-500 line-through", children: String(product.originalPrice) })) : null] }), description && ((0, jsx_runtime_1.jsx)("p", { "data-preview-field-path": `${sectionPath}.description`, className: "mt-6 text-base text-slate-300 leading-relaxed", children: description })), colors.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-8", children: [(0, jsx_runtime_1.jsxs)("span", { className: "block text-xs font-bold uppercase tracking-wider text-slate-400 mb-3", children: ["Color \u2014 ", (0, jsx_runtime_1.jsx)("span", { className: "text-white", children: selectedColor })] }), (0, jsx_runtime_1.jsx)("div", { className: "flex gap-3", children: colors.map((c) => ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setSelectedColor(c.name), className: `w-9 h-9 rounded-full border-2 transition-all p-0.5 ${selectedColor === c.name
|
|
48
57
|
? 'border-lime-400 ring-2 ring-lime-400/30 scale-105'
|
|
49
|
-
: 'border-slate-700 hover:border-slate-500'}`, style: { backgroundColor: c.hex }, title: c.name, "aria-label": c.name }, c.name))) })] })),
|
|
58
|
+
: 'border-slate-700 hover:border-slate-500'}`, style: { backgroundColor: c.hex }, title: c.name, "aria-label": c.name }, c.name))) })] })), effectiveOptions.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-8", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-3", children: [(0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${sectionPath}.${product.optionsLabel ? 'optionsLabel' : 'sizesLabel'}`, className: "block text-xs font-bold uppercase tracking-wider text-slate-400", children: effectiveOptionsLabel }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-semibold text-slate-300", children: resolved.formatSelectedDisplay(selectedSize) })] }), (0, jsx_runtime_1.jsx)("div", { className: "deneb-product-detail-sizes grid grid-cols-3 sm:grid-cols-6 md:grid-cols-7 gap-2", children: effectiveOptions.map((s) => ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setSelectedSize(s), className: `py-2.5 px-2 rounded-xl font-bold text-sm transition-all duration-150 ${selectedSize === s
|
|
50
59
|
? 'bg-white text-slate-950 shadow-md font-extrabold scale-102'
|
|
51
|
-
: 'bg-slate-900/60 text-slate-300 border border-slate-800 hover:bg-slate-800 hover:text-white'}`, children: s }, s))) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "mt-8 flex flex-col gap-3", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => onAddToSelection?.(product, selectedSize, selectedColor), className: "w-full py-4 px-6 rounded-2xl font-black text-sm uppercase tracking-wider bg-lime-400 text-slate-950 hover:bg-lime-300 active:scale-98 transition-all duration-150 shadow-lg shadow-lime-400/20 flex items-center justify-center gap-2", children: (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${sectionPath}.addToSelectionLabel`, children: addToSelectionLabel }) }), resolvedWhatsappUrl && ((0, jsx_runtime_1.jsxs)("a", { href: resolvedWhatsappUrl, target: "_blank", rel: "noopener noreferrer", className: "w-full py-3.5 px-6 rounded-2xl font-bold text-sm bg-emerald-600/20 text-emerald-400 hover:bg-emerald-600/30 border border-emerald-500/30 transition-all duration-150 flex items-center justify-center gap-2", children: [(0, jsx_runtime_1.jsx)("svg", { className: "w-5 h-5 fill-current", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { d: "M17.472 14.382c-.301-.15-1.78-.879-2.056-.98-.276-.1-.477-.15-.678.15-.201.3-.78 0.98-.956 1.18-.175.2-.351.226-.652.075-.301-.15-1.272-.469-2.423-1.496-.896-.799-1.501-1.787-1.677-2.088-.175-.301-.019-.464.132-.614.136-.135.301-.351.452-.527.15-.176.201-.301.301-.502.1-.2.05-.376-.025-.527-.075-.15-.678-1.635-.93-2.242-.244-.592-.493-.512-.678-.521-.175-.009-.376-.009-.577-.009-.201 0-.527.075-.803.376-.276.301-1.054 1.03-1.054 2.512 0 1.482 1.079 2.912 1.23 3.113.15.201 2.124 3.243 5.145 4.548.718.311 1.279.497 1.716.636.722.23 1.378.197 1.898.12.579-.087 1.78-.728 2.03-1.431.251-.703.251-1.305.176-1.431-.075-.126-.276-.201-.577-.351zM12.004 2C6.48 2 2 6.48 2 12c0 1.82.49 3.52 1.34 4.99L2 22l5.14-1.35c1.42.78 3.04 1.22 4.86 1.22 5.52 0 10-4.48 10-10S17.524 2 12.004 2z" }) }), (0, jsx_runtime_1.jsx)("span", { children: "Order Directly via WhatsApp" })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-10 border-t border-slate-800/80 pt-6", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex gap-6 border-b border-slate-800 pb-3 mb-4", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab('specs'), className: `text-sm font-bold tracking-wide transition-colors ${activeTab === 'specs' ? 'text-lime-400' : 'text-slate-400 hover:text-slate-200'}`, children: (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${sectionPath}.specsTitle`, children: specsTitle }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab('shipping'), className: `text-sm font-bold tracking-wide transition-colors ${activeTab === 'shipping' ? 'text-lime-400' : 'text-slate-400 hover:text-slate-200'}`, children: (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${sectionPath}.shippingTitle`, children: shippingTitle }) })] }), activeTab === 'specs' ? ((0, jsx_runtime_1.jsx)("div", { className: "space-y-3", children: product.specs && product.specs.length > 0 ? (product.specs.map((spec, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-xs py-1 border-b border-slate-900", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-slate-400 font-medium", children: spec.label }), (0, jsx_runtime_1.jsx)("span", { className: "text-slate-200 font-semibold", children: spec.value })] }, i)))) : ((0, jsx_runtime_1.jsxs)("div", { className: "text-xs text-slate-400 space-y-2", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between py-1 border-b border-slate-800/50", children: [(0, jsx_runtime_1.jsx)("span", { children: "Upper Material" }), (0, jsx_runtime_1.jsx)("span", { className: "text-slate-200 font-semibold", children: "Engineered breathable mesh" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between py-1 border-b border-slate-800/50", children: [(0, jsx_runtime_1.jsx)("span", { children: "Midsole Technology" }), (0, jsx_runtime_1.jsx)("span", { className: "text-slate-200 font-semibold", children: "Dual-density responsive foam" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between py-1 border-b border-slate-800/50", children: [(0, jsx_runtime_1.jsx)("span", { children: "Outsole Grip" }), (0, jsx_runtime_1.jsx)("span", { className: "text-slate-200 font-semibold", children: "High-abrasion tactical rubber" })] })] })) })) : ((0, jsx_runtime_1.jsxs)("div", { className: "text-xs text-slate-300 space-y-3 leading-relaxed", children: [(0, jsx_runtime_1.jsx)("p", { "data-preview-field-path": `${sectionPath}.shippingSummary`, children: shippingSummary }), (0, jsx_runtime_1.jsx)("p", { "data-preview-field-path": `${sectionPath}.shippingReturns`, className: "text-slate-400", children: shippingReturns })] }))] })] }) })] }) }));
|
|
60
|
+
: 'bg-slate-900/60 text-slate-300 border border-slate-800 hover:bg-slate-800 hover:text-white'}`, children: resolved.formatOption(s) }, s))) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "mt-8 flex flex-col gap-3", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => onAddToSelection?.(product, selectedSize, selectedColor), className: "w-full py-4 px-6 rounded-2xl font-black text-sm uppercase tracking-wider bg-lime-400 text-slate-950 hover:bg-lime-300 active:scale-98 transition-all duration-150 shadow-lg shadow-lime-400/20 flex items-center justify-center gap-2", children: (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${sectionPath}.addToSelectionLabel`, children: addToSelectionLabel }) }), resolvedWhatsappUrl && ((0, jsx_runtime_1.jsxs)("a", { href: resolvedWhatsappUrl, target: "_blank", rel: "noopener noreferrer", className: "w-full py-3.5 px-6 rounded-2xl font-bold text-sm bg-emerald-600/20 text-emerald-400 hover:bg-emerald-600/30 border border-emerald-500/30 transition-all duration-150 flex items-center justify-center gap-2", children: [(0, jsx_runtime_1.jsx)("svg", { className: "w-5 h-5 fill-current", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { d: "M17.472 14.382c-.301-.15-1.78-.879-2.056-.98-.276-.1-.477-.15-.678.15-.201.3-.78 0.98-.956 1.18-.175.2-.351.226-.652.075-.301-.15-1.272-.469-2.423-1.496-.896-.799-1.501-1.787-1.677-2.088-.175-.301-.019-.464.132-.614.136-.135.301-.351.452-.527.15-.176.201-.301.301-.502.1-.2.05-.376-.025-.527-.075-.15-.678-1.635-.93-2.242-.244-.592-.493-.512-.678-.521-.175-.009-.376-.009-.577-.009-.201 0-.527.075-.803.376-.276.301-1.054 1.03-1.054 2.512 0 1.482 1.079 2.912 1.23 3.113.15.201 2.124 3.243 5.145 4.548.718.311 1.279.497 1.716.636.722.23 1.378.197 1.898.12.579-.087 1.78-.728 2.03-1.431.251-.703.251-1.305.176-1.431-.075-.126-.276-.201-.577-.351zM12.004 2C6.48 2 2 6.48 2 12c0 1.82.49 3.52 1.34 4.99L2 22l5.14-1.35c1.42.78 3.04 1.22 4.86 1.22 5.52 0 10-4.48 10-10S17.524 2 12.004 2z" }) }), (0, jsx_runtime_1.jsx)("span", { children: "Order Directly via WhatsApp" })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-10 border-t border-slate-800/80 pt-6", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex gap-6 border-b border-slate-800 pb-3 mb-4", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab('specs'), className: `text-sm font-bold tracking-wide transition-colors ${activeTab === 'specs' ? 'text-lime-400' : 'text-slate-400 hover:text-slate-200'}`, children: (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${sectionPath}.specsTitle`, children: specsTitle }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab('shipping'), className: `text-sm font-bold tracking-wide transition-colors ${activeTab === 'shipping' ? 'text-lime-400' : 'text-slate-400 hover:text-slate-200'}`, children: (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${sectionPath}.shippingTitle`, children: shippingTitle }) })] }), activeTab === 'specs' ? ((0, jsx_runtime_1.jsx)("div", { className: "space-y-3", children: product.specs && product.specs.length > 0 ? (product.specs.map((spec, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-xs py-1 border-b border-slate-900", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-slate-400 font-medium", children: spec.label }), (0, jsx_runtime_1.jsx)("span", { className: "text-slate-200 font-semibold", children: spec.value })] }, i)))) : ((0, jsx_runtime_1.jsxs)("div", { className: "text-xs text-slate-400 space-y-2", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between py-1 border-b border-slate-800/50", children: [(0, jsx_runtime_1.jsx)("span", { children: "Upper Material" }), (0, jsx_runtime_1.jsx)("span", { className: "text-slate-200 font-semibold", children: "Engineered breathable mesh" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between py-1 border-b border-slate-800/50", children: [(0, jsx_runtime_1.jsx)("span", { children: "Midsole Technology" }), (0, jsx_runtime_1.jsx)("span", { className: "text-slate-200 font-semibold", children: "Dual-density responsive foam" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between py-1 border-b border-slate-800/50", children: [(0, jsx_runtime_1.jsx)("span", { children: "Outsole Grip" }), (0, jsx_runtime_1.jsx)("span", { className: "text-slate-200 font-semibold", children: "High-abrasion tactical rubber" })] })] })) })) : ((0, jsx_runtime_1.jsxs)("div", { className: "text-xs text-slate-300 space-y-3 leading-relaxed", children: [(0, jsx_runtime_1.jsx)("p", { "data-preview-field-path": `${sectionPath}.shippingSummary`, children: shippingSummary }), (0, jsx_runtime_1.jsx)("p", { "data-preview-field-path": `${sectionPath}.shippingReturns`, className: "text-slate-400", children: shippingReturns })] }))] })] }) })] }) }));
|
|
52
61
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React, { type ReactNode } from 'react';
|
|
2
|
+
import type { ProductItem } from './EditableProductCard';
|
|
3
|
+
import type { ServiceItem } from './EditableServiceCard';
|
|
2
4
|
export declare const DENEB_PREVIEW_DATA_MESSAGE = "DENEB_PREVIEW_SITE_DATA";
|
|
3
5
|
export declare const PREVIEW_DATA_MESSAGE = "FIVORA_PREVIEW_SITE_DATA";
|
|
4
6
|
export declare const LEGACY_PREVIEW_DATA_MESSAGE: string;
|
|
@@ -11,10 +13,34 @@ export declare const LEGACY_PREVIEW_FOCUS_MESSAGE: string;
|
|
|
11
13
|
export declare const PREVIEW_FIELD_ATTRIBUTE = "data-preview-field-path";
|
|
12
14
|
export { STYLE_PATCH_MESSAGE, DENEB_STYLE_PATCH_MESSAGE } from '@deneb-ui/core';
|
|
13
15
|
export type GenericRecord = Record<string, any>;
|
|
16
|
+
export interface SiteDataApiConfig {
|
|
17
|
+
baseUrl?: string | null;
|
|
18
|
+
catalogUrl?: string | null;
|
|
19
|
+
contactUrl?: string | null;
|
|
20
|
+
analyticsUrl?: string | null;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface SiteDataProject {
|
|
24
|
+
id?: string | null;
|
|
25
|
+
slug?: string | null;
|
|
26
|
+
title?: string | null;
|
|
27
|
+
status?: string | null;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
export interface SiteInstanceData {
|
|
31
|
+
id?: string | null;
|
|
32
|
+
slug?: string | null;
|
|
33
|
+
domain?: string | null;
|
|
34
|
+
subdomain?: string | null;
|
|
35
|
+
customDomain?: string | null;
|
|
36
|
+
liveUrl?: string | null;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
14
39
|
export type SiteData = {
|
|
15
|
-
project?:
|
|
16
|
-
|
|
17
|
-
|
|
40
|
+
project?: SiteDataProject | null;
|
|
41
|
+
siteInstance?: SiteInstanceData | null;
|
|
42
|
+
api?: SiteDataApiConfig | null;
|
|
43
|
+
shop?: GenericRecord | null;
|
|
18
44
|
merchant?: GenericRecord | null;
|
|
19
45
|
template?: {
|
|
20
46
|
structure?: {
|
|
@@ -26,6 +52,8 @@ export type SiteData = {
|
|
|
26
52
|
requiredPages?: string[] | null;
|
|
27
53
|
} | null;
|
|
28
54
|
content?: GenericRecord | null;
|
|
55
|
+
media?: Record<string, string[]> | null;
|
|
56
|
+
seo?: GenericRecord | null;
|
|
29
57
|
styles?: GenericRecord | null;
|
|
30
58
|
[key: string]: unknown;
|
|
31
59
|
};
|
|
@@ -44,8 +72,17 @@ export interface SiteDataProviderProps<T = SiteData> {
|
|
|
44
72
|
children: ReactNode;
|
|
45
73
|
initialSiteData?: T;
|
|
46
74
|
fallbackSiteData?: T;
|
|
75
|
+
/**
|
|
76
|
+
* Optional custom live catalog endpoint.
|
|
77
|
+
* If omitted, defaults to `/api/site-catalog/${slug}/live-data`.
|
|
78
|
+
*/
|
|
79
|
+
liveCatalogEndpoint?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Optional site slug for live rehydration. If omitted, resolved from initialSiteData.
|
|
82
|
+
*/
|
|
83
|
+
siteSlug?: string;
|
|
47
84
|
}
|
|
48
|
-
export declare function SiteDataProvider<T extends SiteData = SiteData>({ children, initialSiteData, fallbackSiteData, }: SiteDataProviderProps<T>): React.JSX.Element;
|
|
85
|
+
export declare function SiteDataProvider<T extends SiteData = SiteData>({ children, initialSiteData, fallbackSiteData, liveCatalogEndpoint, siteSlug, }: SiteDataProviderProps<T>): React.JSX.Element;
|
|
49
86
|
export declare function useSiteData<T = SiteData>(): T;
|
|
50
87
|
export declare function contentObject(value: unknown): GenericRecord;
|
|
51
88
|
export declare function contentText(value: unknown): string;
|
|
@@ -62,3 +99,27 @@ export declare const DenebDataProvider: typeof SiteDataProvider;
|
|
|
62
99
|
export declare const useDenebData: typeof useSiteData;
|
|
63
100
|
export declare const DenebDataContext: React.Context<SiteData>;
|
|
64
101
|
export type DenebData = SiteData;
|
|
102
|
+
/**
|
|
103
|
+
* Hook to retrieve products cleanly from SiteData, supporting both
|
|
104
|
+
* top-level content.products and nested content.home.products.
|
|
105
|
+
*/
|
|
106
|
+
export declare function useProducts(fallback?: ProductItem[]): ProductItem[];
|
|
107
|
+
/**
|
|
108
|
+
* Hook to retrieve services cleanly from SiteData, supporting both
|
|
109
|
+
* top-level content.services and nested content.home.services.
|
|
110
|
+
*/
|
|
111
|
+
export declare function useServices(fallback?: ServiceItem[]): ServiceItem[];
|
|
112
|
+
/**
|
|
113
|
+
* Hook to access official Fivora backend API endpoints (catalogUrl, contactUrl, analyticsUrl).
|
|
114
|
+
*/
|
|
115
|
+
export declare function useSiteApi(): SiteDataApiConfig | null;
|
|
116
|
+
/**
|
|
117
|
+
* Hook to access full catalog metadata and live status.
|
|
118
|
+
*/
|
|
119
|
+
export declare function useSiteCatalog(): {
|
|
120
|
+
products: ProductItem[];
|
|
121
|
+
services: ServiceItem[];
|
|
122
|
+
project: SiteDataProject | null;
|
|
123
|
+
siteInstance: SiteInstanceData | null;
|
|
124
|
+
api: SiteDataApiConfig | null;
|
|
125
|
+
};
|
package/dist/SiteDataProvider.js
CHANGED
|
@@ -16,6 +16,10 @@ exports.contentNumber = contentNumber;
|
|
|
16
16
|
exports.parseFieldPath = parseFieldPath;
|
|
17
17
|
exports.getFieldStyle = getFieldStyle;
|
|
18
18
|
exports.useFieldStyle = useFieldStyle;
|
|
19
|
+
exports.useProducts = useProducts;
|
|
20
|
+
exports.useServices = useServices;
|
|
21
|
+
exports.useSiteApi = useSiteApi;
|
|
22
|
+
exports.useSiteCatalog = useSiteCatalog;
|
|
19
23
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
20
24
|
const react_1 = require("react");
|
|
21
25
|
const core_1 = require("@deneb-ui/core");
|
|
@@ -167,13 +171,92 @@ function readCachedSiteData() {
|
|
|
167
171
|
return null;
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
|
-
function SiteDataProvider({ children, initialSiteData, fallbackSiteData, }) {
|
|
174
|
+
function SiteDataProvider({ children, initialSiteData, fallbackSiteData, liveCatalogEndpoint, siteSlug, }) {
|
|
171
175
|
// SSR HTML and the first client paint must match. Reading sessionStorage here
|
|
172
176
|
// causes React hydration error #418 when a previous preview left merchant
|
|
173
177
|
// content in the cache. Cache is applied after mount in useEffect instead.
|
|
174
178
|
const [siteData, setSiteData] = (0, react_1.useState)(() => {
|
|
175
179
|
return (initialSiteData ?? fallbackSiteData ?? {});
|
|
176
180
|
});
|
|
181
|
+
// Real-time catalog & theme hydration for standalone live sites
|
|
182
|
+
(0, react_1.useEffect)(() => {
|
|
183
|
+
if (typeof window === "undefined" || window.parent !== window)
|
|
184
|
+
return;
|
|
185
|
+
const candidateSlug = siteSlug ||
|
|
186
|
+
initialSiteData?.siteInstance?.slug ||
|
|
187
|
+
initialSiteData?.project?.slug ||
|
|
188
|
+
initialSiteData?.project?.id;
|
|
189
|
+
const endpoint = liveCatalogEndpoint ||
|
|
190
|
+
initialSiteData?.api?.catalogUrl ||
|
|
191
|
+
(candidateSlug && initialSiteData?.api?.baseUrl
|
|
192
|
+
? `${initialSiteData.api.baseUrl.replace(/\/+$/, '')}/site-catalog/${candidateSlug}/live-data`
|
|
193
|
+
: candidateSlug
|
|
194
|
+
? `/site-catalog/${candidateSlug}/live-data`
|
|
195
|
+
: null);
|
|
196
|
+
if (!endpoint)
|
|
197
|
+
return;
|
|
198
|
+
const controller = new AbortController();
|
|
199
|
+
fetch(endpoint, {
|
|
200
|
+
signal: controller.signal,
|
|
201
|
+
headers: { Accept: "application/json" },
|
|
202
|
+
})
|
|
203
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
204
|
+
.then((live) => {
|
|
205
|
+
if (!live || !isRecord(live))
|
|
206
|
+
return;
|
|
207
|
+
setSiteData((current) => {
|
|
208
|
+
const currentContent = isRecord(current.content) ? current.content : {};
|
|
209
|
+
const currentHome = isRecord(currentContent.home) ? currentContent.home : null;
|
|
210
|
+
const nextProducts = Array.isArray(live.products)
|
|
211
|
+
? live.products
|
|
212
|
+
: currentContent.products;
|
|
213
|
+
const nextServices = Array.isArray(live.services)
|
|
214
|
+
? live.services
|
|
215
|
+
: currentContent.services;
|
|
216
|
+
return {
|
|
217
|
+
...current,
|
|
218
|
+
content: {
|
|
219
|
+
...currentContent,
|
|
220
|
+
...(nextProducts !== undefined ? { products: nextProducts } : {}),
|
|
221
|
+
...(nextServices !== undefined ? { services: nextServices } : {}),
|
|
222
|
+
...(currentHome
|
|
223
|
+
? {
|
|
224
|
+
home: {
|
|
225
|
+
...currentHome,
|
|
226
|
+
...(nextProducts !== undefined && 'products' in currentHome
|
|
227
|
+
? { products: nextProducts }
|
|
228
|
+
: {}),
|
|
229
|
+
...(nextServices !== undefined && 'services' in currentHome
|
|
230
|
+
? { services: nextServices }
|
|
231
|
+
: {}),
|
|
232
|
+
...(nextProducts !== undefined && 'featuredProducts' in currentHome
|
|
233
|
+
? { featuredProducts: nextProducts }
|
|
234
|
+
: {}),
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
: {}),
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
// Dynamic theme variable injection for instant color updates
|
|
242
|
+
if (isRecord(live.theme)) {
|
|
243
|
+
const rootStyle = document.documentElement.style;
|
|
244
|
+
if (typeof live.theme.primaryColor === "string") {
|
|
245
|
+
rootStyle.setProperty("--brand-primary", live.theme.primaryColor);
|
|
246
|
+
}
|
|
247
|
+
if (typeof live.theme.secondaryColor === "string") {
|
|
248
|
+
rootStyle.setProperty("--brand-secondary", live.theme.secondaryColor);
|
|
249
|
+
}
|
|
250
|
+
if (typeof live.theme.accentColor === "string") {
|
|
251
|
+
rootStyle.setProperty("--brand-accent", live.theme.accentColor);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
.catch(() => {
|
|
256
|
+
// Gracefully keep pre-rendered static fallback if live API is unreachable
|
|
257
|
+
});
|
|
258
|
+
return () => controller.abort();
|
|
259
|
+
}, [liveCatalogEndpoint, siteSlug, initialSiteData]);
|
|
177
260
|
(0, react_1.useEffect)(() => {
|
|
178
261
|
let parentOrigin = resolveParentOrigin();
|
|
179
262
|
const applyIncomingSiteData = (incoming) => {
|
|
@@ -342,3 +425,71 @@ function useFieldStyle(path) {
|
|
|
342
425
|
exports.DenebDataProvider = SiteDataProvider;
|
|
343
426
|
exports.useDenebData = useSiteData;
|
|
344
427
|
exports.DenebDataContext = exports.SiteDataContext;
|
|
428
|
+
/**
|
|
429
|
+
* Hook to retrieve products cleanly from SiteData, supporting both
|
|
430
|
+
* top-level content.products and nested content.home.products.
|
|
431
|
+
*/
|
|
432
|
+
function useProducts(fallback = []) {
|
|
433
|
+
const siteData = useSiteData();
|
|
434
|
+
const content = isRecord(siteData?.content) ? siteData.content : null;
|
|
435
|
+
if (!content)
|
|
436
|
+
return fallback;
|
|
437
|
+
if (Array.isArray(content.products) && content.products.length > 0) {
|
|
438
|
+
return content.products;
|
|
439
|
+
}
|
|
440
|
+
const home = isRecord(content.home) ? content.home : null;
|
|
441
|
+
if (home) {
|
|
442
|
+
if (Array.isArray(home.products) && home.products.length > 0) {
|
|
443
|
+
return home.products;
|
|
444
|
+
}
|
|
445
|
+
if (Array.isArray(home.featuredProducts) && home.featuredProducts.length > 0) {
|
|
446
|
+
return home.featuredProducts;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return fallback;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Hook to retrieve services cleanly from SiteData, supporting both
|
|
453
|
+
* top-level content.services and nested content.home.services.
|
|
454
|
+
*/
|
|
455
|
+
function useServices(fallback = []) {
|
|
456
|
+
const siteData = useSiteData();
|
|
457
|
+
const content = isRecord(siteData?.content) ? siteData.content : null;
|
|
458
|
+
if (!content)
|
|
459
|
+
return fallback;
|
|
460
|
+
if (Array.isArray(content.services) && content.services.length > 0) {
|
|
461
|
+
return content.services;
|
|
462
|
+
}
|
|
463
|
+
const home = isRecord(content.home) ? content.home : null;
|
|
464
|
+
if (home) {
|
|
465
|
+
if (Array.isArray(home.services) && home.services.length > 0) {
|
|
466
|
+
return home.services;
|
|
467
|
+
}
|
|
468
|
+
if (Array.isArray(home.featuredServices) && home.featuredServices.length > 0) {
|
|
469
|
+
return home.featuredServices;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return fallback;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Hook to access official Fivora backend API endpoints (catalogUrl, contactUrl, analyticsUrl).
|
|
476
|
+
*/
|
|
477
|
+
function useSiteApi() {
|
|
478
|
+
const siteData = useSiteData();
|
|
479
|
+
return siteData?.api ?? null;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Hook to access full catalog metadata and live status.
|
|
483
|
+
*/
|
|
484
|
+
function useSiteCatalog() {
|
|
485
|
+
const products = useProducts();
|
|
486
|
+
const services = useServices();
|
|
487
|
+
const siteData = useSiteData();
|
|
488
|
+
return {
|
|
489
|
+
products,
|
|
490
|
+
services,
|
|
491
|
+
project: siteData?.project ?? null,
|
|
492
|
+
siteInstance: siteData?.siteInstance ?? null,
|
|
493
|
+
api: siteData?.api ?? null,
|
|
494
|
+
};
|
|
495
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./urls"), exports);
|
|
18
18
|
__exportStar(require("./responsive"), exports);
|
|
19
|
+
__exportStar(require("./productOptions"), exports);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type MeasurementUnit = 'size' | 'g' | 'kg' | 'mg' | 'ml' | 'l' | 'pcs' | 'pack' | 'oz' | 'lb' | (string & {});
|
|
2
|
+
export interface ProductOptionInput {
|
|
3
|
+
id?: string | number;
|
|
4
|
+
name?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
unit?: MeasurementUnit;
|
|
7
|
+
measurement?: string;
|
|
8
|
+
optionsLabel?: string;
|
|
9
|
+
options?: (string | number)[];
|
|
10
|
+
optionsText?: string;
|
|
11
|
+
sizes?: (string | number)[] | string;
|
|
12
|
+
sizesText?: string;
|
|
13
|
+
sizesLabel?: string;
|
|
14
|
+
colors?: Array<string | {
|
|
15
|
+
name: string;
|
|
16
|
+
hex?: string;
|
|
17
|
+
}> | string;
|
|
18
|
+
colorsText?: string;
|
|
19
|
+
colorsLabel?: string;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface ResolvedProductOptions {
|
|
23
|
+
/**
|
|
24
|
+
* The detected or specified unit, e.g. "g", "ml", "size", "pcs".
|
|
25
|
+
*/
|
|
26
|
+
unit: string;
|
|
27
|
+
/**
|
|
28
|
+
* The user-facing label for the options selector.
|
|
29
|
+
* e.g. "Net Weight", "Volume", "Available Sizes", "Pack Size".
|
|
30
|
+
*/
|
|
31
|
+
optionsLabel: string;
|
|
32
|
+
/**
|
|
33
|
+
* Normalized list of available option values (e.g. ["100g", "250g", "500g"] or ["40", "41", "42"]).
|
|
34
|
+
*/
|
|
35
|
+
options: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Default selected option (first option, or empty string).
|
|
38
|
+
*/
|
|
39
|
+
defaultOption: string;
|
|
40
|
+
/**
|
|
41
|
+
* Whether this option represents a physical measurement (weight, volume, count).
|
|
42
|
+
*/
|
|
43
|
+
isMeasurement: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Format option for button or badge rendering (e.g. "250g", "50ml", "42").
|
|
46
|
+
*/
|
|
47
|
+
formatOption: (option: string | number) => string;
|
|
48
|
+
/**
|
|
49
|
+
* Format option for headline or summary display (e.g. "250g", "50ml", "Size 42").
|
|
50
|
+
*/
|
|
51
|
+
formatSelectedDisplay: (option: string | number) => string;
|
|
52
|
+
/**
|
|
53
|
+
* Format an order snippet for WhatsApp or checkout (e.g. "(250g)" or "(Ivory, Size 42)").
|
|
54
|
+
*/
|
|
55
|
+
formatOrderSnippet: (selectedOption?: string | number, selectedColor?: string) => string;
|
|
56
|
+
/**
|
|
57
|
+
* Available color names.
|
|
58
|
+
*/
|
|
59
|
+
colors: string[];
|
|
60
|
+
/**
|
|
61
|
+
* Color selector label (e.g. "Available Colors").
|
|
62
|
+
*/
|
|
63
|
+
colorsLabel: string;
|
|
64
|
+
/**
|
|
65
|
+
* Default selected color.
|
|
66
|
+
*/
|
|
67
|
+
defaultColor: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Universal product option & measurement resolver.
|
|
71
|
+
* Handles footwear/apparel sizes, food/grocery weights (g, kg, mg),
|
|
72
|
+
* liquids/cosmetics volumes (ml, l), and counts (pcs, pack) seamlessly.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveProductOptions(product?: ProductOptionInput | Record<string, unknown> | null): ResolvedProductOptions;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveProductOptions = resolveProductOptions;
|
|
4
|
+
function parseList(value) {
|
|
5
|
+
if (typeof value === 'string') {
|
|
6
|
+
return value
|
|
7
|
+
.split(',')
|
|
8
|
+
.map((s) => s.trim())
|
|
9
|
+
.filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return value
|
|
13
|
+
.map((item) => {
|
|
14
|
+
if (item == null)
|
|
15
|
+
return '';
|
|
16
|
+
if (typeof item === 'object' && 'name' in item && typeof item.name === 'string') {
|
|
17
|
+
return item.name.trim();
|
|
18
|
+
}
|
|
19
|
+
return String(item).trim();
|
|
20
|
+
})
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Universal product option & measurement resolver.
|
|
27
|
+
* Handles footwear/apparel sizes, food/grocery weights (g, kg, mg),
|
|
28
|
+
* liquids/cosmetics volumes (ml, l), and counts (pcs, pack) seamlessly.
|
|
29
|
+
*/
|
|
30
|
+
function resolveProductOptions(product) {
|
|
31
|
+
const p = (product || {});
|
|
32
|
+
// 1. Resolve Colors
|
|
33
|
+
let colors = parseList(p.colorsText);
|
|
34
|
+
if (!colors.length)
|
|
35
|
+
colors = parseList(p.colors);
|
|
36
|
+
const colorsLabel = (typeof p.colorsLabel === 'string' && p.colorsLabel.trim()) || 'Available Colors';
|
|
37
|
+
const defaultColor = colors[0] || 'Standard';
|
|
38
|
+
// 2. Resolve Options / Sizes
|
|
39
|
+
let options = parseList(p.optionsText);
|
|
40
|
+
if (!options.length)
|
|
41
|
+
options = parseList(p.options);
|
|
42
|
+
if (!options.length)
|
|
43
|
+
options = parseList(p.sizesText);
|
|
44
|
+
if (!options.length)
|
|
45
|
+
options = parseList(p.sizes);
|
|
46
|
+
if (!options.length && typeof p.measurement === 'string' && p.measurement.trim()) {
|
|
47
|
+
options = [p.measurement.trim()];
|
|
48
|
+
}
|
|
49
|
+
// 3. Detect Unit
|
|
50
|
+
let unit = (typeof p.unit === 'string' ? p.unit.trim().toLowerCase() : '');
|
|
51
|
+
if (!unit) {
|
|
52
|
+
// Auto-detect unit from options, measurement, or title
|
|
53
|
+
const probe = [
|
|
54
|
+
...options,
|
|
55
|
+
typeof p.measurement === 'string' ? p.measurement : '',
|
|
56
|
+
typeof p.title === 'string' ? p.title : '',
|
|
57
|
+
typeof p.name === 'string' ? p.name : '',
|
|
58
|
+
].join(' ');
|
|
59
|
+
if (/\b(\d+)\s*(mg)\b/i.test(probe)) {
|
|
60
|
+
unit = 'mg';
|
|
61
|
+
}
|
|
62
|
+
else if (/\b(\d+)\s*(kg)\b/i.test(probe)) {
|
|
63
|
+
unit = 'kg';
|
|
64
|
+
}
|
|
65
|
+
else if (/\b(\d+)\s*(g|grams?)\b/i.test(probe)) {
|
|
66
|
+
unit = 'g';
|
|
67
|
+
}
|
|
68
|
+
else if (/\b(\d+)\s*(ml)\b/i.test(probe)) {
|
|
69
|
+
unit = 'ml';
|
|
70
|
+
}
|
|
71
|
+
else if (/\b(\d+)\s*(l|litres?|liters?)\b/i.test(probe)) {
|
|
72
|
+
unit = 'l';
|
|
73
|
+
}
|
|
74
|
+
else if (/\b(\d+)\s*(pcs|pieces?|pack|packs?|boxes?)\b/i.test(probe)) {
|
|
75
|
+
unit = 'pcs';
|
|
76
|
+
}
|
|
77
|
+
else if (/\b(\d+)\s*(oz|fl\s*oz)\b/i.test(probe)) {
|
|
78
|
+
unit = 'oz';
|
|
79
|
+
}
|
|
80
|
+
else if (/\b(\d+)\s*(lb|lbs)\b/i.test(probe)) {
|
|
81
|
+
unit = 'lb';
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
unit = 'size';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const isMeasurement = unit !== 'size';
|
|
88
|
+
// 4. Resolve Label
|
|
89
|
+
let optionsLabel = (typeof p.optionsLabel === 'string' && p.optionsLabel.trim()) || '';
|
|
90
|
+
if (!optionsLabel && typeof p.sizesLabel === 'string' && p.sizesLabel.trim()) {
|
|
91
|
+
optionsLabel = p.sizesLabel.trim();
|
|
92
|
+
}
|
|
93
|
+
if (!optionsLabel) {
|
|
94
|
+
switch (unit) {
|
|
95
|
+
case 'g':
|
|
96
|
+
case 'kg':
|
|
97
|
+
case 'mg':
|
|
98
|
+
case 'oz':
|
|
99
|
+
case 'lb':
|
|
100
|
+
optionsLabel = 'Net Weight';
|
|
101
|
+
break;
|
|
102
|
+
case 'ml':
|
|
103
|
+
case 'l':
|
|
104
|
+
optionsLabel = 'Volume';
|
|
105
|
+
break;
|
|
106
|
+
case 'pcs':
|
|
107
|
+
case 'pack':
|
|
108
|
+
optionsLabel = 'Pack Size';
|
|
109
|
+
break;
|
|
110
|
+
case 'size':
|
|
111
|
+
optionsLabel = 'Available Sizes';
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
optionsLabel = 'Available Options';
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const defaultOption = options[0] || '';
|
|
119
|
+
// 5. Formatter helpers
|
|
120
|
+
const formatOption = (option) => {
|
|
121
|
+
const raw = String(option ?? '').trim();
|
|
122
|
+
if (!raw)
|
|
123
|
+
return '';
|
|
124
|
+
if (unit === 'size')
|
|
125
|
+
return raw;
|
|
126
|
+
// Check if raw already ends with unit
|
|
127
|
+
const regex = new RegExp(`\\b(${unit})$`, 'i');
|
|
128
|
+
if (regex.test(raw))
|
|
129
|
+
return raw;
|
|
130
|
+
// If pure number and measurement unit
|
|
131
|
+
if (/^\d+(\.\d+)?$/.test(raw)) {
|
|
132
|
+
return `${raw}${unit}`;
|
|
133
|
+
}
|
|
134
|
+
return raw;
|
|
135
|
+
};
|
|
136
|
+
const formatSelectedDisplay = (option) => {
|
|
137
|
+
const formatted = formatOption(option);
|
|
138
|
+
if (!formatted)
|
|
139
|
+
return '';
|
|
140
|
+
if (unit === 'size') {
|
|
141
|
+
if (/^size\s+/i.test(formatted))
|
|
142
|
+
return formatted;
|
|
143
|
+
return `Size ${formatted}`;
|
|
144
|
+
}
|
|
145
|
+
return formatted;
|
|
146
|
+
};
|
|
147
|
+
const formatOrderSnippet = (selectedOption, selectedColor) => {
|
|
148
|
+
const cleanColor = selectedColor && selectedColor !== 'Standard' ? String(selectedColor).trim() : '';
|
|
149
|
+
const cleanOption = selectedOption ? formatSelectedDisplay(selectedOption) : '';
|
|
150
|
+
if (cleanColor && cleanOption) {
|
|
151
|
+
return `(${cleanColor}, ${cleanOption})`;
|
|
152
|
+
}
|
|
153
|
+
if (cleanOption) {
|
|
154
|
+
return `(${cleanOption})`;
|
|
155
|
+
}
|
|
156
|
+
if (cleanColor) {
|
|
157
|
+
return `(${cleanColor})`;
|
|
158
|
+
}
|
|
159
|
+
return '';
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
unit,
|
|
163
|
+
optionsLabel,
|
|
164
|
+
options,
|
|
165
|
+
defaultOption,
|
|
166
|
+
isMeasurement,
|
|
167
|
+
formatOption,
|
|
168
|
+
formatSelectedDisplay,
|
|
169
|
+
formatOrderSnippet,
|
|
170
|
+
colors,
|
|
171
|
+
colorsLabel,
|
|
172
|
+
defaultColor,
|
|
173
|
+
};
|
|
174
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/ui",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.40",
|
|
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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
],
|
|
50
50
|
"license": "MIT",
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@deneb-ui/core": "^2.0.
|
|
52
|
+
"@deneb-ui/core": "^2.0.40"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"react": "^18.0.0 || ^19.0.0",
|