@deneb-ui/ui 2.0.32 → 2.0.34

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.
@@ -151,7 +151,7 @@ function EditableText({ id, 'data-preview-field-path': previewFieldPath, default
151
151
  ...styleVars,
152
152
  ...style,
153
153
  };
154
- return ((0, jsx_runtime_1.jsx)(Component, { "data-preview-field-path": path, "data-preview-style-target": path, "data-preview-style-type": "text", "data-preview-placeholder": isPlaceholderActive ? 'true' : undefined, className: `deneb-text ${className}`.trim(), style: typographyStyle, ...props, children: displayContent }));
154
+ return ((0, jsx_runtime_1.jsx)(Component, { "data-preview-field-path": path, "data-preview-style-target": path, "data-preview-style-type": "text", "data-preview-placeholder": isPlaceholderActive ? 'true' : undefined, className: `deneb-text ${className}`.trim(), style: typographyStyle, suppressHydrationWarning: true, ...props, children: displayContent }));
155
155
  }
156
156
  function EditableHeading({ level = 2, as, ...props }) {
157
157
  const variant = `h${level}`;
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ export type PreviewFieldProps<T extends React.ElementType = 'span'> = {
3
+ as?: T;
4
+ path: string;
5
+ children?: React.ReactNode;
6
+ } & Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'children'>;
7
+ /**
8
+ * Leaf node for Fivora visual editing. Uses suppressHydrationWarning because the
9
+ * preview focus bridge may annotate the DOM before React hydrates.
10
+ */
11
+ export declare function PreviewField<T extends React.ElementType = 'span'>({ as, path, children, ...rest }: PreviewFieldProps<T>): React.JSX.Element;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.PreviewField = PreviewField;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ /**
7
+ * Leaf node for Fivora visual editing. Uses suppressHydrationWarning because the
8
+ * preview focus bridge may annotate the DOM before React hydrates.
9
+ */
10
+ function PreviewField({ as, path, children, ...rest }) {
11
+ const Component = (as || 'span');
12
+ return ((0, jsx_runtime_1.jsx)(Component, { "data-preview-field-path": path, suppressHydrationWarning: true, ...rest, children: children }));
13
+ }
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ export interface CollageImageItem {
3
+ id?: string;
4
+ image?: string | null;
5
+ caption?: string | null;
6
+ }
7
+ export interface HeritageCollageProps {
8
+ collageHeading?: React.ReactNode;
9
+ collageDescription?: React.ReactNode;
10
+ historyHeading?: React.ReactNode;
11
+ history?: React.ReactNode;
12
+ images?: CollageImageItem[];
13
+ collageHeadingPath?: string;
14
+ collageDescriptionPath?: string;
15
+ historyHeadingPath?: string;
16
+ historyPath?: string;
17
+ listPath?: string;
18
+ className?: string;
19
+ }
20
+ /**
21
+ * Heritage collage + history block for About pages.
22
+ * List images use standard Fivora list/item/field path attributes.
23
+ */
24
+ export declare function HeritageCollage({ collageHeading, collageDescription, historyHeading, history, images, collageHeadingPath, collageDescriptionPath, historyHeadingPath, historyPath, listPath, className, }: HeritageCollageProps): React.JSX.Element | null;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.HeritageCollage = HeritageCollage;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const urls_1 = require("../utils/urls");
7
+ const COLLAGE_LAYOUT = [
8
+ 'row-span-2 min-h-[220px]',
9
+ 'min-h-[140px]',
10
+ 'min-h-[140px]',
11
+ 'col-span-2 min-h-[180px]',
12
+ ];
13
+ /**
14
+ * Heritage collage + history block for About pages.
15
+ * List images use standard Fivora list/item/field path attributes.
16
+ */
17
+ function HeritageCollage({ collageHeading, collageDescription, historyHeading, history, images = [], collageHeadingPath, collageDescriptionPath, historyHeadingPath, historyPath, listPath = 'about.collageImages', className = '', }) {
18
+ const hasContent = images.length > 0 ||
19
+ (typeof collageDescription === 'string' ? collageDescription.trim() : collageDescription) ||
20
+ (typeof history === 'string' ? history.trim() : history);
21
+ if (!hasContent)
22
+ return null;
23
+ return ((0, jsx_runtime_1.jsx)("section", { className: `deneb-heritage-collage ${className}`.trim(), "data-design-section": "heritage-collage", style: { marginTop: '3rem', paddingTop: '2.5rem', borderTop: '1px solid var(--color-border, #e2e8f0)' }, children: (0, jsx_runtime_1.jsxs)("div", { style: {
24
+ display: 'grid',
25
+ gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
26
+ gap: '2rem',
27
+ alignItems: 'start',
28
+ }, children: [images.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { "data-preview-list-path": listPath, style: {
29
+ display: 'grid',
30
+ gridTemplateColumns: 'repeat(2, 1fr)',
31
+ gap: '0.75rem',
32
+ gridAutoRows: 'minmax(120px, auto)',
33
+ }, children: images.map((item, idx) => ((0, jsx_runtime_1.jsxs)("figure", { "data-preview-item-path": `${listPath}[${idx}]`, className: COLLAGE_LAYOUT[idx] ?? COLLAGE_LAYOUT[1], style: {
34
+ position: 'relative',
35
+ overflow: 'hidden',
36
+ borderRadius: '1rem',
37
+ border: '1px solid var(--color-border, #e2e8f0)',
38
+ margin: 0,
39
+ minHeight: '140px',
40
+ background: 'var(--color-secondary, #f1f5f9)',
41
+ }, children: [(0, jsx_runtime_1.jsx)("img", { src: (0, urls_1.withBasePath)(item.image || '/placeholder.svg'), alt: "", "data-preview-field-path": `${listPath}[${idx}].image`, style: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' } }), item.caption ? ((0, jsx_runtime_1.jsx)("figcaption", { style: {
42
+ position: 'absolute',
43
+ insetInline: 0,
44
+ bottom: 0,
45
+ padding: '0.75rem 1rem',
46
+ background: 'linear-gradient(transparent, rgba(15,23,42,0.85))',
47
+ color: '#fff',
48
+ fontSize: '0.8125rem',
49
+ fontWeight: 600,
50
+ }, children: (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${listPath}[${idx}].caption`, children: item.caption }) })) : null] }, item.id || `collage-${idx}`))) })) : null, (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', flexDirection: 'column', gap: '1rem' }, children: [collageHeading ? ((0, jsx_runtime_1.jsx)("h2", { ...(collageHeadingPath ? { 'data-preview-field-path': collageHeadingPath } : {}), style: { fontSize: '1.5rem', fontWeight: 700, margin: 0 }, children: collageHeading })) : null, collageDescription ? ((0, jsx_runtime_1.jsx)("p", { ...(collageDescriptionPath ? { 'data-preview-field-path': collageDescriptionPath } : {}), style: { margin: 0, lineHeight: 1.6, color: 'var(--color-muted, #64748b)' }, children: collageDescription })) : null, historyHeading ? ((0, jsx_runtime_1.jsx)("h3", { ...(historyHeadingPath ? { 'data-preview-field-path': historyHeadingPath } : {}), style: { fontSize: '1.125rem', fontWeight: 700, margin: '0.5rem 0 0' }, children: historyHeading })) : null, history ? ((0, jsx_runtime_1.jsx)("p", { ...(historyPath ? { 'data-preview-field-path': historyPath } : {}), style: { margin: 0, lineHeight: 1.7 }, children: history })) : null] })] }) }));
51
+ }
@@ -1 +1,2 @@
1
1
  export * from './BusinessHours';
2
+ export * from './HeritageCollage';
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./BusinessHours"), exports);
18
+ __exportStar(require("./HeritageCollage"), exports);
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ export interface ListActionCtaProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
3
+ /** List path without index, e.g. `seasonal.preOrderCta`. */
4
+ listPath: string;
5
+ itemIndex?: number;
6
+ /** When true, stamps list/item paths and editable label + hidden URL (use on first card only). */
7
+ editable?: boolean;
8
+ buttonLabel: string;
9
+ buttonUrl: string;
10
+ urlMode?: 'auto' | 'external';
11
+ /** Optional icons or extra content rendered before the label span. */
12
+ leading?: React.ReactNode;
13
+ trailing?: React.ReactNode;
14
+ }
15
+ /**
16
+ * Shared list CTA for grids (Pre-Order, Add to Tray, etc.).
17
+ * Edit via the first card's button; other cards mirror the same label/URL statically.
18
+ */
19
+ export declare function ListActionCta({ listPath, itemIndex, editable, buttonLabel, buttonUrl, urlMode, leading, trailing, className, ...rest }: ListActionCtaProps): React.JSX.Element;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ListActionCta = ListActionCta;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const urls_1 = require("../utils/urls");
7
+ /**
8
+ * Shared list CTA for grids (Pre-Order, Add to Tray, etc.).
9
+ * Edit via the first card's button; other cards mirror the same label/URL statically.
10
+ */
11
+ function ListActionCta({ listPath, itemIndex = 0, editable = true, buttonLabel, buttonUrl, urlMode = 'auto', leading, trailing, className, ...rest }) {
12
+ const href = (0, urls_1.resolveActionUrl)(buttonUrl, urlMode);
13
+ const labelPath = `${listPath}[${itemIndex}].buttonLabel`;
14
+ const urlPath = `${listPath}[${itemIndex}].buttonUrl`;
15
+ const opensNewTab = Boolean(href && /^https?:\/\//i.test(href));
16
+ const link = ((0, jsx_runtime_1.jsxs)("a", { href: href || '#', target: opensNewTab ? '_blank' : undefined, rel: opensNewTab ? 'noopener noreferrer' : undefined, className: className, "data-preview-static": editable ? undefined : 'list-action-cta', ...rest, children: [leading, editable ? ((0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": labelPath, children: buttonLabel })) : ((0, jsx_runtime_1.jsx)("span", { "data-preview-static": "list-action-cta-label", children: buttonLabel })), trailing] }));
17
+ if (!editable)
18
+ return link;
19
+ return ((0, jsx_runtime_1.jsx)("div", { "data-preview-list-path": listPath, children: (0, jsx_runtime_1.jsxs)("div", { "data-preview-item-path": `${listPath}[${itemIndex}]`, children: [link, (0, jsx_runtime_1.jsx)("span", { hidden: true, "aria-hidden": "true", "data-preview-field-path": urlPath, children: buttonUrl })] }) }));
20
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ export interface SplitFieldActionProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
3
+ /** Visible button / link label (inspector reads textContent). */
4
+ label: React.ReactNode;
5
+ /** Raw URL stored in site data (hidden from the visible label). */
6
+ urlValue: string;
7
+ labelFieldPath: string;
8
+ urlFieldPath: string;
9
+ /** When `auto`, bare phone numbers become WhatsApp links. */
10
+ urlMode?: 'auto' | 'external';
11
+ icon?: React.ReactNode;
12
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
13
+ size?: 'sm' | 'md' | 'lg';
14
+ }
15
+ /**
16
+ * Fivora-safe action link: editable label on the anchor, URL on a hidden sibling span.
17
+ * Never put the URL on the same node as the label — the inspector overwrites paired fields.
18
+ */
19
+ export declare function SplitFieldAction({ label, urlValue, labelFieldPath, urlFieldPath, urlMode, href, icon, variant, size, className, style, children, ...rest }: SplitFieldActionProps): React.JSX.Element | null;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.SplitFieldAction = SplitFieldAction;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const urls_1 = require("../utils/urls");
7
+ /**
8
+ * Fivora-safe action link: editable label on the anchor, URL on a hidden sibling span.
9
+ * Never put the URL on the same node as the label — the inspector overwrites paired fields.
10
+ */
11
+ function SplitFieldAction({ label, urlValue, labelFieldPath, urlFieldPath, urlMode = 'auto', href, icon, variant = 'primary', size = 'md', className = '', style, children, ...rest }) {
12
+ const resolvedHref = href || (0, urls_1.resolveActionUrl)(urlValue, urlMode);
13
+ if (!resolvedHref && !label && !children)
14
+ return null;
15
+ const baseStyles = {
16
+ display: 'inline-flex',
17
+ alignItems: 'center',
18
+ justifyContent: 'center',
19
+ gap: '0.5rem',
20
+ fontWeight: 500,
21
+ textDecoration: 'none',
22
+ transition: 'all 0.2s ease',
23
+ cursor: 'pointer',
24
+ borderRadius: '0.5rem',
25
+ ...style,
26
+ };
27
+ const sizeStyles = {
28
+ sm: { fontSize: '0.875rem', padding: '0.375rem 0.75rem' },
29
+ md: { fontSize: '0.95rem', padding: '0.5rem 1rem' },
30
+ lg: { fontSize: '1.05rem', padding: '0.75rem 1.5rem' },
31
+ };
32
+ const variantStyles = {
33
+ primary: {
34
+ backgroundColor: 'var(--color-primary, #0f172a)',
35
+ color: '#ffffff',
36
+ },
37
+ secondary: {
38
+ backgroundColor: 'var(--color-secondary, #f1f5f9)',
39
+ color: 'var(--color-text, #0f172a)',
40
+ },
41
+ outline: {
42
+ backgroundColor: 'transparent',
43
+ border: '1px solid var(--color-border, #e2e8f0)',
44
+ color: 'var(--color-text, #0f172a)',
45
+ },
46
+ ghost: {
47
+ backgroundColor: 'transparent',
48
+ color: 'var(--color-text, #0f172a)',
49
+ },
50
+ link: {
51
+ backgroundColor: 'transparent',
52
+ color: 'var(--color-primary, #2563eb)',
53
+ padding: 0,
54
+ textDecoration: 'underline',
55
+ borderRadius: 0,
56
+ },
57
+ };
58
+ const mergedStyles = {
59
+ ...baseStyles,
60
+ ...(variant !== 'link' ? sizeStyles[size] : {}),
61
+ ...variantStyles[variant],
62
+ };
63
+ const finalHref = resolvedHref || '#';
64
+ const opensNewTab = finalHref !== '#' && /^https?:\/\//i.test(finalHref);
65
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "deneb-split-field-action", style: { display: 'contents' }, children: [(0, jsx_runtime_1.jsxs)("a", { href: finalHref, "data-preview-static": "split-field-action", target: opensNewTab ? '_blank' : undefined, rel: opensNewTab ? 'noopener noreferrer' : undefined, className: `deneb-split-field-action-link ${className}`.trim(), style: mergedStyles, ...rest, children: [icon, children, (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": labelFieldPath, children: label })] }), (0, jsx_runtime_1.jsx)("span", { hidden: true, "aria-hidden": "true", "data-preview-field-path": urlFieldPath, children: urlValue })] }));
66
+ }
@@ -1 +1,3 @@
1
1
  export * from './DenebAction';
2
+ export * from './SplitFieldAction';
3
+ export * from './ListActionCta';
@@ -15,3 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./DenebAction"), exports);
18
+ __exportStar(require("./SplitFieldAction"), exports);
19
+ __exportStar(require("./ListActionCta"), exports);
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * DENEB UI — The Visual-First React Framework for High-Converting Storefronts
3
3
  * Created by Chamika Gayashan & Induranga Kawishwara
4
4
  */
5
+ export * from './PreviewField';
5
6
  export * from './EditableText';
6
7
  export * from './EditableImage';
7
8
  export * from './EditableList';
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.Hero = exports.Footer = exports.Header = exports.Navbar = exports.ContactForm = exports.FAQ = exports.Accordion = exports.TestimonialCard = exports.PricingCard = exports.ServiceCard = exports.CustomerReviews = exports.ProductDetail = exports.ProductGrid = exports.ProductCard = exports.List = exports.Box = exports.Section = exports.Grid = exports.Image = exports.Quote = exports.Badge = exports.Paragraph = exports.Heading = exports.Text = exports.Dialog = exports.Card = exports.Button = exports.collectStyleTargetsFromHtml = exports.validateStyleTree = exports.styleToCssVariables = exports.patchStyleByPath = exports.patchElementStyle = exports.STYLE_TYPE_ATTRIBUTE = exports.STYLE_TARGET_ATTRIBUTE = exports.DENEB_STYLE_PATCH_MESSAGE = exports.STYLE_PATCH_MESSAGE = exports.formatResponsiveFontSize = exports.resolveInstallableFont = exports.normalizeFontId = exports.lookupFontDefinition = exports.listFontsByCategory = exports.collectFontIdsFromSiteData = exports.buildGoogleFontsStylesheetUrl = exports.DENEB_GOOGLE_FONT_COUNT = exports.DENEB_FONT_REGISTRY = exports.useDenebFonts = exports.DENEB_FONTS_LINK_ID = exports.FontLoader = exports.DenebComponentStyles = exports.ResponsiveBaseStyles = void 0;
22
22
  exports.DENEB_AUTHOR = exports.DENEB_FRAMEWORK_VERSION = exports.DENEB_FRAMEWORK_NAME = exports.FilterSidebar = exports.CartDrawer = exports.CategoryPills = exports.AnnouncementBar = exports.HeroSplit = void 0;
23
+ __exportStar(require("./PreviewField"), exports);
23
24
  __exportStar(require("./EditableText"), exports);
24
25
  __exportStar(require("./EditableImage"), exports);
25
26
  __exportStar(require("./EditableList"), exports);
@@ -1,13 +1,18 @@
1
1
  import React from 'react';
2
2
  export interface LocationCardProps {
3
+ name?: string | null;
3
4
  title?: string;
4
5
  titleFieldPath?: string;
5
6
  address?: string | null;
6
7
  city?: string | null;
8
+ state?: string | null;
7
9
  country?: string | null;
8
10
  postalCode?: string | null;
9
11
  mapUrl?: string | null;
12
+ addressUrl?: string | null;
13
+ url?: string | null;
10
14
  fieldPath?: string;
15
+ nameFieldPath?: string;
11
16
  addressFieldPath?: string;
12
17
  mapUrlFieldPath?: string;
13
18
  directionsLabel?: string;
@@ -17,4 +22,4 @@ export interface LocationCardProps {
17
22
  /**
18
23
  * Premium storefront LocationCard displaying address, pin icon, and Google Maps directions button.
19
24
  */
20
- export declare function LocationCard({ title, titleFieldPath, address, city, country, postalCode, mapUrl, fieldPath, addressFieldPath, mapUrlFieldPath, directionsLabel, className, style, }: LocationCardProps): React.JSX.Element | null;
25
+ export declare function LocationCard({ name, title, titleFieldPath, address, city, state, country, postalCode, mapUrl, addressUrl, url, fieldPath, nameFieldPath, addressFieldPath, mapUrlFieldPath, directionsLabel, className, style, }: LocationCardProps): React.JSX.Element | null;
@@ -9,8 +9,11 @@ const EditableText_1 = require("../EditableText");
9
9
  /**
10
10
  * Premium storefront LocationCard displaying address, pin icon, and Google Maps directions button.
11
11
  */
12
- function LocationCard({ title = 'Our Location', titleFieldPath, address, city, country, postalCode, mapUrl, fieldPath, addressFieldPath = 'common.business.location.address', mapUrlFieldPath = 'common.business.location.mapUrl', directionsLabel = 'Get Directions →', className = '', style, }) {
13
- if (!address && !city && !country && !mapUrl) {
12
+ function LocationCard({ name, title = 'Our Location', titleFieldPath, address, city, state, country, postalCode, mapUrl, addressUrl, url, fieldPath, nameFieldPath, addressFieldPath = 'common.business.location.address', mapUrlFieldPath = 'common.business.location.addressUrl', directionsLabel = 'Get Directions →', className = '', style, }) {
13
+ const explicitUrl = (addressUrl || mapUrl || url || '').trim();
14
+ const cardTitle = name || title || 'Our Location';
15
+ const resolvedTitleFieldPath = nameFieldPath || titleFieldPath || (fieldPath ? `${fieldPath}.name` : 'contact.locationTitle');
16
+ if (!address && !city && !country && !explicitUrl && !name) {
14
17
  return null;
15
18
  }
16
19
  const cardStyles = {
@@ -37,5 +40,5 @@ function LocationCard({ title = 'Our Location', titleFieldPath, address, city, c
37
40
  alignItems: 'center',
38
41
  justifyContent: 'center',
39
42
  };
40
- return ((0, jsx_runtime_1.jsxs)("div", { className: `deneb-location-card ${className}`.trim(), style: cardStyles, children: [(0, jsx_runtime_1.jsx)("div", { style: iconCircleStyles, children: (0, jsx_runtime_1.jsxs)("svg", { "data-preview-static": "location-card-icon", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)("path", { d: "M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" }), (0, jsx_runtime_1.jsx)("circle", { cx: "12", cy: "10", r: "3" })] }) }), (0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "h3", id: titleFieldPath || (fieldPath ? `${fieldPath}.title` : 'contact.locationTitle'), defaultValue: title, style: { margin: 0, fontSize: '1.25rem', fontWeight: 600, color: 'var(--color-text, #0f172a)' } }), (0, jsx_runtime_1.jsx)(Address_1.Address, { address: address, city: city, country: country, postalCode: postalCode, fieldPath: fieldPath, addressFieldPath: addressFieldPath }), (0, jsx_runtime_1.jsx)("div", { style: { marginTop: '0.5rem', width: '100%' }, children: (0, jsx_runtime_1.jsx)(MapLink_1.MapLink, { mapUrl: mapUrl, address: address, city: city, country: country, label: directionsLabel, fieldPath: mapUrlFieldPath, variant: "primary", size: "md", style: { width: '100%' } }) })] }));
43
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `deneb-location-card ${className}`.trim(), style: cardStyles, children: [(0, jsx_runtime_1.jsx)("div", { style: iconCircleStyles, children: (0, jsx_runtime_1.jsxs)("svg", { "data-preview-static": "location-card-icon", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)("path", { d: "M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" }), (0, jsx_runtime_1.jsx)("circle", { cx: "12", cy: "10", r: "3" })] }) }), (0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "h3", id: titleFieldPath || (fieldPath ? `${fieldPath}.title` : 'contact.locationTitle'), defaultValue: cardTitle, style: { margin: 0, fontSize: '1.25rem', fontWeight: 600, color: 'var(--color-text, #0f172a)' } }), (0, jsx_runtime_1.jsx)(Address_1.Address, { address: address, city: city, country: country, postalCode: postalCode, fieldPath: fieldPath, addressFieldPath: addressFieldPath }), (0, jsx_runtime_1.jsx)("div", { style: { marginTop: '0.5rem', width: '100%' }, children: (0, jsx_runtime_1.jsx)(MapLink_1.MapLink, { name: name, mapUrl: explicitUrl, addressUrl: explicitUrl, url: explicitUrl, address: address, city: city, state: state, country: country, postalCode: postalCode, label: directionsLabel, urlFieldPath: mapUrlFieldPath, variant: "primary", size: "md", style: { width: '100%' } }) })] }));
41
44
  }
@@ -8,7 +8,7 @@ const MapLink_1 = require("./MapLink");
8
8
  /**
9
9
  * Combines physical address with an interactive "Get Directions" action.
10
10
  */
11
- function LocationLink({ address, city, country, mapUrl, fieldPath, addressFieldPath = 'common.business.location.address', mapUrlFieldPath = 'common.business.location.mapUrl', directionsLabel = 'Get Directions', showAddress = true, className = '', style, }) {
11
+ function LocationLink({ address, city, country, mapUrl, fieldPath, addressFieldPath = 'common.business.location.address', mapUrlFieldPath = 'common.business.location.addressUrl', directionsLabel = 'Get Directions', showAddress = true, className = '', style, }) {
12
12
  if (!address && !city && !country && !mapUrl) {
13
13
  return null;
14
14
  }
@@ -1,13 +1,26 @@
1
1
  import React from 'react';
2
2
  export interface MapLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
3
+ name?: string | null;
3
4
  mapUrl?: string | null;
5
+ addressUrl?: string | null;
6
+ url?: string | null;
4
7
  address?: string | null;
5
8
  city?: string | null;
9
+ state?: string | null;
6
10
  country?: string | null;
11
+ postalCode?: string | null;
7
12
  label?: React.ReactNode;
13
+ /** Visible label field path, e.g. `contact.directionsLabel`. */
14
+ labelFieldPath?: string;
15
+ /** Hidden URL field path, e.g. `contact.directionsUrl`. */
16
+ urlFieldPath?: string;
17
+ /** @deprecated Use `urlFieldPath` instead. */
8
18
  fieldPath?: string;
9
19
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
10
20
  size?: 'sm' | 'md' | 'lg';
11
21
  icon?: React.ReactNode;
12
22
  }
13
- export declare function MapLink({ mapUrl, address, city, country, label, fieldPath, variant, size, icon, className, style, ...rest }: MapLinkProps): React.JSX.Element | null;
23
+ /**
24
+ * Directions CTA with Fivora-safe split fields: label on the button, URL on a hidden sibling.
25
+ */
26
+ export declare function MapLink({ name, mapUrl, addressUrl, url, address, city, state, country, postalCode, label, labelFieldPath, urlFieldPath, fieldPath, variant, size, icon, className, style, ...rest }: MapLinkProps): React.JSX.Element | null;
@@ -4,11 +4,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.MapLink = MapLink;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const urls_1 = require("../utils/urls");
7
- function MapLink({ mapUrl, address, city, country, label = 'Get Directions', fieldPath = 'common.business.location.mapUrl', variant = 'outline', size = 'md', icon, className = '', style, ...rest }) {
8
- const resolvedHref = (0, urls_1.createMapUrl)({ mapUrl, address, city, country });
9
- const hasFieldPath = Boolean(fieldPath && fieldPath.trim());
10
- const finalHref = resolvedHref || (hasFieldPath ? '#' : '');
11
- if (!finalHref)
7
+ /**
8
+ * Directions CTA with Fivora-safe split fields: label on the button, URL on a hidden sibling.
9
+ */
10
+ function MapLink({ name, mapUrl, addressUrl, url, address, city, state, country, postalCode, label = 'Get Directions', labelFieldPath, urlFieldPath, fieldPath, variant = 'outline', size = 'md', icon, className = '', style, ...rest }) {
11
+ const resolvedUrlFieldPath = urlFieldPath || fieldPath || 'contact.directionsUrl';
12
+ const resolvedLabelFieldPath = labelFieldPath || resolvedUrlFieldPath.replace(/Url$/, 'Label');
13
+ const explicitUrl = (addressUrl || mapUrl || url || '').trim();
14
+ const resolvedHref = (0, urls_1.createMapUrl)({ name, mapUrl: explicitUrl, addressUrl: explicitUrl, url: explicitUrl, address, city, state, country, postalCode });
15
+ const urlText = explicitUrl || resolvedHref;
16
+ const finalHref = resolvedHref || '#';
17
+ if (!finalHref && !label)
12
18
  return null;
13
19
  const defaultIcon = ((0, jsx_runtime_1.jsxs)("svg", { "data-preview-static": "map-link-icon", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)("path", { d: "M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" }), (0, jsx_runtime_1.jsx)("circle", { cx: "12", cy: "10", r: "3" })] }));
14
20
  const baseStyles = {
@@ -59,5 +65,6 @@ function MapLink({ mapUrl, address, city, country, label = 'Get Directions', fie
59
65
  ...(variant !== 'link' ? sizeStyles[size] : {}),
60
66
  ...variantStyles[variant],
61
67
  };
62
- return ((0, jsx_runtime_1.jsxs)("a", { href: finalHref, "data-preview-field-path": fieldPath, target: finalHref !== '#' ? '_blank' : undefined, rel: finalHref !== '#' ? 'noopener noreferrer' : undefined, className: `deneb-map-link ${className}`.trim(), style: mergedStyles, ...rest, children: [icon !== undefined ? icon : defaultIcon, label] }));
68
+ const opensNewTab = finalHref !== '#';
69
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "deneb-map-link-group", style: { display: 'contents' }, children: [(0, jsx_runtime_1.jsxs)("a", { href: finalHref, "data-preview-static": "map-link", target: opensNewTab ? '_blank' : undefined, rel: opensNewTab ? 'noopener noreferrer' : undefined, className: `deneb-map-link ${className}`.trim(), style: mergedStyles, ...rest, children: [icon !== undefined ? icon : defaultIcon, (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": resolvedLabelFieldPath, children: label })] }), (0, jsx_runtime_1.jsx)("span", { hidden: true, "aria-hidden": "true", "data-preview-field-path": resolvedUrlFieldPath, children: urlText })] }));
63
70
  }
@@ -3,10 +3,27 @@
3
3
  * Created by Chamika Gayashan & Induranga Kawishwara
4
4
  */
5
5
  export interface MapLocationInput {
6
+ /** Location or branch name, e.g. "Main Roastery", "Downtown Cafe". */
7
+ name?: string | null;
8
+ /** Alternative title property. */
9
+ title?: string | null;
10
+ /** Explicit Google Maps or destination URL. */
6
11
  mapUrl?: string | null;
12
+ /** Fivora-paired sibling of `address` (`addressUrl` shares the `address` inspector stem). */
13
+ addressUrl?: string | null;
14
+ /** Generic external or map URL. */
15
+ url?: string | null;
16
+ /** Link URL alias. */
17
+ linkUrl?: string | null;
18
+ /** Street address. */
7
19
  address?: string | null;
8
20
  city?: string | null;
21
+ state?: string | null;
9
22
  country?: string | null;
23
+ postalCode?: string | null;
24
+ zipCode?: string | null;
25
+ /** Permissive index signature so custom or template-specific keys never trigger TS2561. */
26
+ [key: string]: any;
10
27
  }
11
28
  /**
12
29
  * Creates a valid WhatsApp click-to-chat URL.
@@ -26,6 +43,11 @@ export declare function createEmailUrl(email?: string | null, subject?: string |
26
43
  * Prefers an explicit mapUrl, otherwise constructs a query using address, city, and country.
27
44
  */
28
45
  export declare function createMapUrl(location?: MapLocationInput | string | null): string;
46
+ /**
47
+ * Resolves a merchant-entered action URL for CTAs (WhatsApp, tel, mailto, or external http(s)).
48
+ * Phone-like values without a scheme become WhatsApp links when `mode` is `auto`.
49
+ */
50
+ export declare function resolveActionUrl(raw?: string | null, mode?: 'auto' | 'external'): string;
29
51
  /**
30
52
  * Validates if a string is a safe, valid external link (prevents script injection).
31
53
  */
@@ -37,4 +59,7 @@ export declare function withBasePath(value?: string | null): string;
37
59
  /**
38
60
  * Resolves standard page route from pageKey.
39
61
  */
40
- export declare function pageRoute(pageKey: string): string;
62
+ export declare function resolvePageRoute(pageKey: string, pages?: Array<{
63
+ id: string;
64
+ route: string;
65
+ }>): string;
@@ -8,9 +8,10 @@ exports.createWhatsAppUrl = createWhatsAppUrl;
8
8
  exports.createPhoneUrl = createPhoneUrl;
9
9
  exports.createEmailUrl = createEmailUrl;
10
10
  exports.createMapUrl = createMapUrl;
11
+ exports.resolveActionUrl = resolveActionUrl;
11
12
  exports.isSafeExternalLink = isSafeExternalLink;
12
13
  exports.withBasePath = withBasePath;
13
- exports.pageRoute = pageRoute;
14
+ exports.resolvePageRoute = resolvePageRoute;
14
15
  /**
15
16
  * Creates a valid WhatsApp click-to-chat URL.
16
17
  * Automatically cleans spaces, dashes, and special characters from the phone number.
@@ -65,17 +66,48 @@ function createMapUrl(location) {
65
66
  }
66
67
  return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(location)}`;
67
68
  }
68
- if (location.mapUrl && location.mapUrl.trim()) {
69
- return location.mapUrl.trim();
69
+ const explicitUrl = (location.addressUrl ||
70
+ location.mapUrl ||
71
+ location.url ||
72
+ location.linkUrl ||
73
+ '').trim();
74
+ if (explicitUrl) {
75
+ return explicitUrl;
70
76
  }
71
- const queryParts = [location.address, location.city, location.country]
72
- .filter((part) => Boolean(part && part.trim()))
73
- .map((part) => part.trim());
77
+ const queryParts = [
78
+ location.name || location.title,
79
+ location.address,
80
+ location.city,
81
+ location.state,
82
+ location.country,
83
+ location.postalCode || location.zipCode,
84
+ ]
85
+ .filter((part) => Boolean(part && String(part).trim()))
86
+ .map((part) => String(part).trim());
74
87
  if (queryParts.length > 0) {
75
88
  return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(queryParts.join(', '))}`;
76
89
  }
77
90
  return '';
78
91
  }
92
+ /**
93
+ * Resolves a merchant-entered action URL for CTAs (WhatsApp, tel, mailto, or external http(s)).
94
+ * Phone-like values without a scheme become WhatsApp links when `mode` is `auto`.
95
+ */
96
+ function resolveActionUrl(raw, mode = 'auto') {
97
+ const value = raw?.trim() ?? '';
98
+ if (!value)
99
+ return '';
100
+ if (mode === 'external') {
101
+ return isSafeExternalLink(value) ? value : '';
102
+ }
103
+ if (/^https?:\/\//i.test(value) || /^tel:/i.test(value) || /^mailto:/i.test(value)) {
104
+ return value;
105
+ }
106
+ if (/^\+?[\d\s().-]+$/.test(value)) {
107
+ return createWhatsAppUrl(value);
108
+ }
109
+ return isSafeExternalLink(value) ? value : '';
110
+ }
79
111
  /**
80
112
  * Validates if a string is a safe, valid external link (prevents script injection).
81
113
  */
@@ -105,6 +137,7 @@ function withBasePath(value) {
105
137
  /**
106
138
  * Resolves standard page route from pageKey.
107
139
  */
108
- function pageRoute(pageKey) {
109
- return pageKey === 'home' ? '/' : `/${pageKey}`;
140
+ function resolvePageRoute(pageKey, pages) {
141
+ const match = pages?.find((page) => page.id === pageKey);
142
+ return match?.route ?? `/${pageKey.replace(/_/g, '/')}`;
110
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/ui",
3
- "version": "2.0.32",
3
+ "version": "2.0.34",
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.32"
52
+ "@deneb-ui/core": "^2.0.34"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "react": "^18.0.0 || ^19.0.0",