@deneb-ui/ui 2.0.50 → 2.0.52

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.
@@ -1,6 +1,9 @@
1
1
  import React from 'react';
2
2
  export interface EditableContactFormProps extends React.FormHTMLAttributes<HTMLFormElement> {
3
3
  endpoint?: string;
4
+ whatsappUrl?: string;
5
+ whatsappUrlPath?: string;
6
+ submitMode?: 'whatsapp' | 'api' | 'both';
4
7
  formTitle?: string;
5
8
  formTitlePath?: string;
6
9
  submitButtonText?: string;
@@ -16,4 +19,4 @@ export interface EditableContactFormProps extends React.FormHTMLAttributes<HTMLF
16
19
  successMessage?: string;
17
20
  showPhone?: boolean;
18
21
  }
19
- export declare function EditableContactForm({ endpoint, formTitle, formTitlePath, submitButtonText, submitLabelPath, nameLabelPath, namePlaceholderPath, emailLabelPath, emailPlaceholderPath, phoneLabelPath, phonePlaceholderPath, messageLabelPath, messagePlaceholderPath, successMessage, showPhone, className, style, ...props }: EditableContactFormProps): React.JSX.Element;
22
+ export declare function EditableContactForm({ endpoint, whatsappUrl, whatsappUrlPath, submitMode, formTitle, formTitlePath, submitButtonText, submitLabelPath, nameLabelPath, namePlaceholderPath, emailLabelPath, emailPlaceholderPath, phoneLabelPath, phonePlaceholderPath, messageLabelPath, messagePlaceholderPath, successMessage, showPhone, className, style, ...props }: EditableContactFormProps): React.JSX.Element;
@@ -4,7 +4,8 @@ exports.EditableContactForm = EditableContactForm;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const SiteDataProvider_1 = require("./SiteDataProvider");
7
- function EditableContactForm({ endpoint = 'https://api.fivora.com/site-contact', formTitle, formTitlePath, submitButtonText = 'Send Message', submitLabelPath = 'contact.submitLabel', nameLabelPath = 'contact.nameLabel', namePlaceholderPath = 'contact.namePlaceholder', emailLabelPath = 'contact.emailLabel', emailPlaceholderPath = 'contact.emailPlaceholder', phoneLabelPath = 'contact.phoneLabel', phonePlaceholderPath = 'contact.phonePlaceholder', messageLabelPath = 'contact.messageLabel', messagePlaceholderPath = 'contact.messagePlaceholder', successMessage = 'Thank you! Your message has been sent successfully.', showPhone = false, className = '', style, ...props }) {
7
+ const useWhatsAppForm_1 = require("./hooks/useWhatsAppForm");
8
+ function EditableContactForm({ endpoint = 'https://api.fivora.com/site-contact', whatsappUrl, whatsappUrlPath = 'contact.formWhatsappUrl', submitMode = 'whatsapp', formTitle, formTitlePath, submitButtonText = 'Send Message', submitLabelPath = 'contact.submitLabel', nameLabelPath = 'contact.nameLabel', namePlaceholderPath = 'contact.namePlaceholder', emailLabelPath = 'contact.emailLabel', emailPlaceholderPath = 'contact.emailPlaceholder', phoneLabelPath = 'contact.phoneLabel', phonePlaceholderPath = 'contact.phonePlaceholder', messageLabelPath = 'contact.messageLabel', messagePlaceholderPath = 'contact.messagePlaceholder', successMessage = 'Thank you! Your message has been sent successfully.', showPhone = false, className = '', style, ...props }) {
8
9
  const siteData = (0, SiteDataProvider_1.useSiteData)();
9
10
  const [status, setStatus] = (0, react_1.useState)('idle');
10
11
  const [errorMessage, setErrorMessage] = (0, react_1.useState)('');
@@ -19,6 +20,18 @@ function EditableContactForm({ endpoint = 'https://api.fivora.com/site-contact',
19
20
  const resolvedPhonePlaceholder = String(contactContent?.phonePlaceholder || '+1 (555) 000-0000');
20
21
  const resolvedMessageLabel = String(contactContent?.messageLabel || 'Message');
21
22
  const resolvedMessagePlaceholder = String(contactContent?.messagePlaceholder || 'Tell us how we can help...');
23
+ const { submitViaWhatsApp, resolvedWhatsappUrl } = (0, useWhatsAppForm_1.useWhatsAppForm)({
24
+ whatsappUrl,
25
+ whatsappUrlPath,
26
+ formName: resolvedFormTitle || 'Contact Form',
27
+ onSuccess: () => {
28
+ setStatus('success');
29
+ },
30
+ onError: (err) => {
31
+ setStatus('error');
32
+ setErrorMessage(err.message || 'Something went wrong. Please try again.');
33
+ },
34
+ });
22
35
  const handleSubmit = async (e) => {
23
36
  e.preventDefault();
24
37
  setStatus('submitting');
@@ -32,22 +45,38 @@ function EditableContactForm({ endpoint = 'https://api.fivora.com/site-contact',
32
45
  phone: showPhone ? formData.get('phone') : undefined,
33
46
  message: formData.get('message'),
34
47
  };
35
- try {
36
- const res = await fetch(endpoint, {
48
+ if (submitMode === 'api') {
49
+ try {
50
+ const res = await fetch(endpoint, {
51
+ method: 'POST',
52
+ headers: { 'Content-Type': 'application/json' },
53
+ body: JSON.stringify(payload),
54
+ });
55
+ if (!res.ok) {
56
+ throw new Error(`Submission error: ${res.statusText || 'Unable to send message'}`);
57
+ }
58
+ setStatus('success');
59
+ form.reset();
60
+ }
61
+ catch (err) {
62
+ setStatus('error');
63
+ setErrorMessage(err.message || 'Something went wrong. Please try again.');
64
+ }
65
+ return;
66
+ }
67
+ // Default to WhatsApp dispatch (or both if requested)
68
+ if (submitMode === 'both') {
69
+ fetch(endpoint, {
37
70
  method: 'POST',
38
71
  headers: { 'Content-Type': 'application/json' },
39
72
  body: JSON.stringify(payload),
40
- });
41
- if (!res.ok) {
42
- throw new Error(`Submission error: ${res.statusText || 'Unable to send message'}`);
43
- }
73
+ }).catch(() => { });
74
+ }
75
+ const success = submitViaWhatsApp(form);
76
+ if (success) {
44
77
  setStatus('success');
45
78
  form.reset();
46
79
  }
47
- catch (err) {
48
- setStatus('error');
49
- setErrorMessage(err.message || 'Something went wrong. Please try again.');
50
- }
51
80
  };
52
81
  return ((0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, "data-fivora-contact-disabled": true, style: {
53
82
  display: 'flex',
@@ -132,7 +161,7 @@ function EditableContactForm({ endpoint = 'https://api.fivora.com/site-contact',
132
161
  opacity: status === 'submitting' ? 0.7 : 1,
133
162
  boxShadow: '0 4px 14px 0 rgba(37, 99, 235, 0.35)',
134
163
  transition: 'all 0.2s ease',
135
- }, children: (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": submitLabelPath, children: status === 'submitting' ? 'Sending Message...' : resolvedSubmitText }) }), status === 'success' && ((0, jsx_runtime_1.jsx)("div", { style: {
164
+ }, children: (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": submitLabelPath, children: status === 'submitting' ? 'Sending Message...' : resolvedSubmitText }) }), (0, jsx_runtime_1.jsx)("span", { hidden: true, "aria-hidden": "true", "data-preview-field-path": whatsappUrlPath, children: resolvedWhatsappUrl }), status === 'success' && ((0, jsx_runtime_1.jsx)("div", { style: {
136
165
  padding: '1rem',
137
166
  borderRadius: '10px',
138
167
  backgroundColor: 'rgba(34, 197, 94, 0.1)',
@@ -17,8 +17,7 @@ export interface ContactActionsProps {
17
17
  style?: React.CSSProperties;
18
18
  }
19
19
  /**
20
- * Smart container that automatically renders available contact channels.
21
- * When a merchant adds phone, WhatsApp, or email in site-data.json,
22
- * the corresponding action button appears automatically with zero template code changes.
20
+ * Always-mounted contact channels so Fivora empty-state validation keeps
21
+ * the field markers clickable when values are blank.
23
22
  */
24
- export declare function ContactActions({ phone, whatsapp, email, phoneFieldPath, whatsappFieldPath, emailFieldPath, labels, size, layout, className, style, }: ContactActionsProps): React.JSX.Element | null;
23
+ export declare function ContactActions({ phone, whatsapp, email, phoneFieldPath, whatsappFieldPath, emailFieldPath, labels, size, layout, className, style, }: ContactActionsProps): React.JSX.Element;
@@ -7,17 +7,10 @@ const WhatsAppButton_1 = require("./WhatsAppButton");
7
7
  const PhoneButton_1 = require("./PhoneButton");
8
8
  const EmailButton_1 = require("./EmailButton");
9
9
  /**
10
- * Smart container that automatically renders available contact channels.
11
- * When a merchant adds phone, WhatsApp, or email in site-data.json,
12
- * the corresponding action button appears automatically with zero template code changes.
10
+ * Always-mounted contact channels so Fivora empty-state validation keeps
11
+ * the field markers clickable when values are blank.
13
12
  */
14
13
  function ContactActions({ phone, whatsapp, email, phoneFieldPath = 'common.business.phone', whatsappFieldPath = 'common.business.whatsapp', emailFieldPath = 'common.business.email', labels = {}, size = 'md', layout = 'row', className = '', style, }) {
15
- const hasPhone = Boolean(phone && phone.trim());
16
- const hasWhatsApp = Boolean(whatsapp && whatsapp.trim());
17
- const hasEmail = Boolean(email && email.trim());
18
- if (!hasPhone && !hasWhatsApp && !hasEmail) {
19
- return null;
20
- }
21
14
  const layoutStyles = {
22
15
  display: 'flex',
23
16
  flexDirection: layout === 'column' ? 'column' : 'row',
@@ -26,5 +19,5 @@ function ContactActions({ phone, whatsapp, email, phoneFieldPath = 'common.busin
26
19
  gap: '0.75rem',
27
20
  ...style,
28
21
  };
29
- return ((0, jsx_runtime_1.jsxs)("div", { className: `deneb-contact-actions deneb-layout-${layout} ${className}`.trim(), style: layoutStyles, children: [hasPhone && ((0, jsx_runtime_1.jsx)(PhoneButton_1.PhoneButton, { value: phone, fieldPath: phoneFieldPath, label: labels.phone || 'Call Us', size: size })), hasWhatsApp && ((0, jsx_runtime_1.jsx)(WhatsAppButton_1.WhatsAppButton, { value: whatsapp, fieldPath: whatsappFieldPath, label: labels.whatsapp || 'Chat on WhatsApp', size: size })), hasEmail && ((0, jsx_runtime_1.jsx)(EmailButton_1.EmailButton, { value: email, fieldPath: emailFieldPath, label: labels.email || 'Email Us', size: size }))] }));
22
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `deneb-contact-actions deneb-layout-${layout} ${className}`.trim(), style: layoutStyles, children: [(0, jsx_runtime_1.jsx)(PhoneButton_1.PhoneButton, { value: phone ?? '', fieldPath: phoneFieldPath, label: labels.phone || 'Call Us', size: size }), (0, jsx_runtime_1.jsx)(WhatsAppButton_1.WhatsAppButton, { value: whatsapp ?? '', fieldPath: whatsappFieldPath, label: labels.whatsapp || 'Chat on WhatsApp', size: size }), (0, jsx_runtime_1.jsx)(EmailButton_1.EmailButton, { value: email ?? '', fieldPath: emailFieldPath, label: labels.email || 'Email Us', size: size })] }));
30
23
  }
@@ -5,7 +5,7 @@ exports.PhoneButton = PhoneButton;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const ContactButton_1 = require("./ContactButton");
7
7
  function PhoneButton({ phoneNumber, value, label = 'Call Us', fieldPath = 'common.business.phone', variant = 'primary', ...rest }) {
8
- const number = phoneNumber || value;
8
+ const number = phoneNumber ?? value;
9
9
  if (!number && !fieldPath)
10
10
  return null;
11
11
  return ((0, jsx_runtime_1.jsx)(ContactButton_1.ContactButton, { type: "phone", value: number, label: label, fieldPath: fieldPath, variant: variant, ...rest }));
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ export interface UseWhatsAppFormOptions {
3
+ /** The WhatsApp URL or phone number (e.g. "https://wa.me/1234567890", "+1234567890") */
4
+ whatsappUrl?: string | null;
5
+ /** Field path to query in siteData (e.g. "contact.formWhatsappUrl") */
6
+ whatsappUrlPath?: string;
7
+ /** Form name for message header (e.g. "Contact Inquiry", "Book Repair") */
8
+ formName?: string;
9
+ /** Business name for message header */
10
+ businessName?: string;
11
+ /** Custom message formatter callback */
12
+ formatMessage?: (data: Record<string, any>) => string;
13
+ /** Callback fired after opening WhatsApp */
14
+ onSuccess?: (details: {
15
+ data: Record<string, any>;
16
+ url: string;
17
+ }) => void;
18
+ /** Callback fired if submission encounters an error */
19
+ onError?: (error: Error) => void;
20
+ }
21
+ export interface UseWhatsAppFormReturn {
22
+ /** Submit directly from an HTML form element or submit FormEvent */
23
+ submitViaWhatsApp: (eOrForm: React.FormEvent<HTMLFormElement> | HTMLFormElement, extraData?: Record<string, any>) => boolean;
24
+ /** Submit explicit key-value data */
25
+ submitDataViaWhatsApp: (data: Record<string, any>) => boolean;
26
+ /** Construct full WhatsApp URL without triggering window navigation */
27
+ buildWhatsAppUrl: (data: Record<string, any>) => string;
28
+ /** Resolved target phone or URL */
29
+ resolvedWhatsappUrl: string;
30
+ /** The most recently generated WhatsApp link */
31
+ whatsAppLink: string | null;
32
+ /** Submission state */
33
+ isSubmitting: boolean;
34
+ }
35
+ /**
36
+ * Extracts digits from a raw phone number, full wa.me link, or WhatsApp API link.
37
+ */
38
+ export declare function resolveWhatsAppNumber(target?: string | null): string;
39
+ /**
40
+ * Extracts form field labels and values from an HTMLFormElement.
41
+ */
42
+ export declare function extractFormData(form: HTMLFormElement): Record<string, any>;
43
+ export declare function defaultFormatMessage(data: Record<string, any>, options?: {
44
+ formName?: string;
45
+ businessName?: string;
46
+ }): string;
47
+ /**
48
+ * Universal Form-to-WhatsApp Hook.
49
+ * Supports HTMLFormElement submission, controlled component state data,
50
+ * automatic siteData URL lookup, and popup-blocker safe redirection.
51
+ */
52
+ export declare function useWhatsAppForm(options?: UseWhatsAppFormOptions): UseWhatsAppFormReturn;
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.resolveWhatsAppNumber = resolveWhatsAppNumber;
5
+ exports.extractFormData = extractFormData;
6
+ exports.defaultFormatMessage = defaultFormatMessage;
7
+ exports.useWhatsAppForm = useWhatsAppForm;
8
+ const react_1 = require("react");
9
+ const SiteDataProvider_1 = require("../SiteDataProvider");
10
+ /**
11
+ * Extracts digits from a raw phone number, full wa.me link, or WhatsApp API link.
12
+ */
13
+ function resolveWhatsAppNumber(target) {
14
+ if (!target)
15
+ return '';
16
+ const trimmed = String(target).trim();
17
+ const waMatch = trimmed.match(/wa\.me\/([0-9+]+)/i);
18
+ if (waMatch && waMatch[1]) {
19
+ return waMatch[1].replace(/[^0-9]/g, '');
20
+ }
21
+ const queryMatch = trimmed.match(/[?&]phone=([0-9+]+)/i);
22
+ if (queryMatch && queryMatch[1]) {
23
+ return queryMatch[1].replace(/[^0-9]/g, '');
24
+ }
25
+ return trimmed.replace(/[^0-9]/g, '');
26
+ }
27
+ function getValueByPath(obj, path) {
28
+ if (!obj || !path)
29
+ return undefined;
30
+ const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
31
+ let curr = obj;
32
+ for (const part of parts) {
33
+ if (curr == null)
34
+ return undefined;
35
+ curr = curr[part];
36
+ }
37
+ return curr;
38
+ }
39
+ function humanizeFieldName(name) {
40
+ return name
41
+ .replace(/([A-Z])/g, ' $1')
42
+ .replace(/[-_]+/g, ' ')
43
+ .trim()
44
+ .replace(/\b\w/g, (c) => c.toUpperCase());
45
+ }
46
+ /**
47
+ * Extracts form field labels and values from an HTMLFormElement.
48
+ */
49
+ function extractFormData(form) {
50
+ const result = {};
51
+ const elements = Array.from(form.elements);
52
+ for (const el of elements) {
53
+ if (!(el instanceof HTMLInputElement ||
54
+ el instanceof HTMLTextAreaElement ||
55
+ el instanceof HTMLSelectElement)) {
56
+ continue;
57
+ }
58
+ if (!el.name || el.disabled || el.type === 'submit' || el.type === 'reset' || el.type === 'button') {
59
+ continue;
60
+ }
61
+ if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked) {
62
+ continue;
63
+ }
64
+ let fieldLabel = '';
65
+ if (el.id) {
66
+ const labelEl = form.querySelector(`label[for="${el.id}"]`);
67
+ if (labelEl && labelEl.textContent) {
68
+ fieldLabel = labelEl.textContent.trim();
69
+ }
70
+ }
71
+ if (!fieldLabel) {
72
+ const parentLabel = el.closest('label');
73
+ if (parentLabel) {
74
+ const clone = parentLabel.cloneNode(true);
75
+ clone.querySelectorAll('input, select, textarea').forEach((n) => n.remove());
76
+ fieldLabel = clone.textContent?.trim() || '';
77
+ }
78
+ }
79
+ if (!fieldLabel && el.getAttribute('aria-label')) {
80
+ fieldLabel = el.getAttribute('aria-label') || '';
81
+ }
82
+ if (!fieldLabel && 'placeholder' in el && el.placeholder) {
83
+ fieldLabel = el.placeholder.trim();
84
+ }
85
+ if (!fieldLabel) {
86
+ fieldLabel = humanizeFieldName(el.name);
87
+ }
88
+ result[fieldLabel] = el.value;
89
+ }
90
+ return result;
91
+ }
92
+ function defaultFormatMessage(data, options = {}) {
93
+ const lines = [];
94
+ if (options.formName && options.businessName) {
95
+ lines.push(`*${options.formName} - ${options.businessName}*`);
96
+ }
97
+ else if (options.formName) {
98
+ lines.push(`*${options.formName}*`);
99
+ }
100
+ else if (options.businessName) {
101
+ lines.push(`*New Message for ${options.businessName}*`);
102
+ }
103
+ else {
104
+ lines.push(`*New Form Submission*`);
105
+ }
106
+ lines.push('────────────────────────');
107
+ for (const [key, val] of Object.entries(data)) {
108
+ if (val === undefined || val === null || val === '')
109
+ continue;
110
+ if (Array.isArray(val)) {
111
+ lines.push(`*${key}*:`);
112
+ val.forEach((item, idx) => {
113
+ if (typeof item === 'object' && item !== null) {
114
+ lines.push(` ${idx + 1}. ${JSON.stringify(item)}`);
115
+ }
116
+ else {
117
+ lines.push(` • ${item}`);
118
+ }
119
+ });
120
+ }
121
+ else if (typeof val === 'object' && val !== null) {
122
+ lines.push(`*${key}*: ${JSON.stringify(val)}`);
123
+ }
124
+ else {
125
+ lines.push(`*${key}*: ${val}`);
126
+ }
127
+ }
128
+ lines.push('────────────────────────');
129
+ lines.push('_Sent via website form_');
130
+ return lines.join('\n');
131
+ }
132
+ /**
133
+ * Universal Form-to-WhatsApp Hook.
134
+ * Supports HTMLFormElement submission, controlled component state data,
135
+ * automatic siteData URL lookup, and popup-blocker safe redirection.
136
+ */
137
+ function useWhatsAppForm(options = {}) {
138
+ const siteData = (0, SiteDataProvider_1.useSiteData)();
139
+ const [isSubmitting, setIsSubmitting] = (0, react_1.useState)(false);
140
+ const [whatsAppLink, setWhatsAppLink] = (0, react_1.useState)(null);
141
+ // 1. Resolve WhatsApp URL from options, siteData path, or fallback siteData fields
142
+ const content = siteData?.content;
143
+ const business = siteData?.business;
144
+ let resolvedUrl = options.whatsappUrl;
145
+ if (!resolvedUrl && options.whatsappUrlPath) {
146
+ resolvedUrl =
147
+ getValueByPath(content, options.whatsappUrlPath) ||
148
+ getValueByPath(siteData, options.whatsappUrlPath);
149
+ }
150
+ if (!resolvedUrl) {
151
+ resolvedUrl =
152
+ content?.contact?.formWhatsappUrl ||
153
+ content?.contact?.whatsappUrl ||
154
+ content?.home?.contact?.formWhatsappUrl ||
155
+ business?.whatsapp ||
156
+ siteData?.contact?.whatsapp ||
157
+ 'https://wa.me/1234567890';
158
+ }
159
+ const resolvedBusinessName = options.businessName ||
160
+ business?.name ||
161
+ content?.common?.business?.name ||
162
+ siteData?.project?.name ||
163
+ '';
164
+ const buildUrl = (0, react_1.useCallback)((data) => {
165
+ const number = resolveWhatsAppNumber(resolvedUrl);
166
+ const message = options.formatMessage
167
+ ? options.formatMessage(data)
168
+ : defaultFormatMessage(data, {
169
+ formName: options.formName,
170
+ businessName: resolvedBusinessName,
171
+ });
172
+ if (number) {
173
+ return `https://wa.me/${number}?text=${encodeURIComponent(message)}`;
174
+ }
175
+ if (resolvedUrl && /^https?:\/\//i.test(resolvedUrl)) {
176
+ const separator = resolvedUrl.includes('?') ? '&' : '?';
177
+ return `${resolvedUrl}${separator}text=${encodeURIComponent(message)}`;
178
+ }
179
+ return `https://wa.me/?text=${encodeURIComponent(message)}`;
180
+ }, [resolvedUrl, options.formatMessage, options.formName, resolvedBusinessName]);
181
+ const dispatchUrl = (0, react_1.useCallback)((url, data) => {
182
+ setIsSubmitting(true);
183
+ setWhatsAppLink(url);
184
+ try {
185
+ if (typeof window !== 'undefined') {
186
+ const opened = window.open(url, '_blank', 'noopener,noreferrer');
187
+ if (!opened || opened.closed || typeof opened.closed === 'undefined') {
188
+ window.location.href = url;
189
+ }
190
+ }
191
+ options.onSuccess?.({ data, url });
192
+ return true;
193
+ }
194
+ catch (err) {
195
+ options.onError?.(err);
196
+ return false;
197
+ }
198
+ finally {
199
+ setIsSubmitting(false);
200
+ }
201
+ }, [options]);
202
+ const submitDataViaWhatsApp = (0, react_1.useCallback)((data) => {
203
+ const url = buildUrl(data);
204
+ return dispatchUrl(url, data);
205
+ }, [buildUrl, dispatchUrl]);
206
+ const submitViaWhatsApp = (0, react_1.useCallback)((eOrForm, extraData) => {
207
+ let form = null;
208
+ if ('preventDefault' in eOrForm && typeof eOrForm.preventDefault === 'function') {
209
+ eOrForm.preventDefault();
210
+ form = (eOrForm.currentTarget || eOrForm.target);
211
+ }
212
+ else if (eOrForm instanceof HTMLElement) {
213
+ form = eOrForm;
214
+ }
215
+ const extracted = form ? extractFormData(form) : {};
216
+ const merged = extraData ? { ...extracted, ...extraData } : extracted;
217
+ const url = buildUrl(merged);
218
+ return dispatchUrl(url, merged);
219
+ }, [buildUrl, dispatchUrl]);
220
+ return {
221
+ submitViaWhatsApp,
222
+ submitDataViaWhatsApp,
223
+ buildWhatsAppUrl: buildUrl,
224
+ resolvedWhatsappUrl: String(resolvedUrl),
225
+ whatsAppLink,
226
+ isSubmitting,
227
+ };
228
+ }
package/dist/index.d.ts CHANGED
@@ -42,6 +42,7 @@ export { DenebComponentStyles } from './DenebComponentStyles';
42
42
  export { FontLoader, DENEB_FONTS_LINK_ID } from './fonts/FontLoader';
43
43
  export { useDenebFonts } from './fonts/useDenebFonts';
44
44
  export * from './hooks/useComponentStyle';
45
+ export * from './hooks/useWhatsAppForm';
45
46
  export { DENEB_FONT_REGISTRY, DENEB_GOOGLE_FONT_COUNT, buildGoogleFontsStylesheetUrl, collectFontIdsFromSiteData, listFontsByCategory, lookupFontDefinition, normalizeFontId, resolveInstallableFont, formatResponsiveFontSize, } from '@deneb-ui/core';
46
47
  export { STYLE_PATCH_MESSAGE, DENEB_STYLE_PATCH_MESSAGE, STYLE_TARGET_ATTRIBUTE, STYLE_TYPE_ATTRIBUTE, patchElementStyle, patchStyleByPath, styleToCssVariables, validateStyleTree, collectStyleTargetsFromHtml, } from '@deneb-ui/core';
47
48
  export * from './utils';
package/dist/index.js CHANGED
@@ -65,6 +65,7 @@ Object.defineProperty(exports, "DENEB_FONTS_LINK_ID", { enumerable: true, get: f
65
65
  var useDenebFonts_1 = require("./fonts/useDenebFonts");
66
66
  Object.defineProperty(exports, "useDenebFonts", { enumerable: true, get: function () { return useDenebFonts_1.useDenebFonts; } });
67
67
  __exportStar(require("./hooks/useComponentStyle"), exports);
68
+ __exportStar(require("./hooks/useWhatsAppForm"), exports);
68
69
  var core_1 = require("@deneb-ui/core");
69
70
  Object.defineProperty(exports, "DENEB_FONT_REGISTRY", { enumerable: true, get: function () { return core_1.DENEB_FONT_REGISTRY; } });
70
71
  Object.defineProperty(exports, "DENEB_GOOGLE_FONT_COUNT", { enumerable: true, get: function () { return core_1.DENEB_GOOGLE_FONT_COUNT; } });
@@ -8,4 +8,4 @@ export interface SocialButtonProps extends React.AnchorHTMLAttributes<HTMLAnchor
8
8
  variant?: 'icon' | 'pill' | 'button';
9
9
  size?: 'sm' | 'md' | 'lg';
10
10
  }
11
- export declare function SocialButton({ platform, url, href, label, fieldPath, variant, size, className, style, ...rest }: SocialButtonProps): React.JSX.Element | null;
11
+ export declare function SocialButton({ platform, url, href, label, fieldPath, variant, size, className, style, ...rest }: SocialButtonProps): React.JSX.Element;
@@ -61,11 +61,7 @@ const PLATFORM_CONFIG = {
61
61
  };
62
62
  function SocialButton({ platform, url, href, label, fieldPath, variant = 'icon', size = 'md', className = '', style, ...rest }) {
63
63
  const derivedFieldPath = fieldPath || `common.business.social.${platform}`;
64
- const hasFieldPath = Boolean(fieldPath);
65
- const targetUrl = url || href || (hasFieldPath ? '#' : '');
66
- // Don't render broken button if no valid URL and no fieldPath
67
- if (!targetUrl)
68
- return null;
64
+ const targetUrl = url ?? href ?? '#';
69
65
  const config = PLATFORM_CONFIG[platform] || PLATFORM_CONFIG.x;
70
66
  const iconSize = size === 'sm' ? 16 : size === 'lg' ? 22 : 18;
71
67
  const baseStyles = {
@@ -8,7 +8,6 @@ export interface SocialLinksProps {
8
8
  style?: React.CSSProperties;
9
9
  }
10
10
  /**
11
- * Smart container rendering active social buttons.
12
- * Automatically filters out any unconfigured or empty platforms.
11
+ * Always-mounted social buttons so empty-state preview keeps field markers.
13
12
  */
14
- export declare function SocialLinks({ social, fieldPathPrefix, variant, size, className, style, }: SocialLinksProps): React.JSX.Element | null;
13
+ export declare function SocialLinks({ social, fieldPathPrefix, variant, size, className, style, }: SocialLinksProps): React.JSX.Element;
@@ -16,17 +16,11 @@ const SUPPORTED_PLATFORMS = [
16
16
  'github',
17
17
  ];
18
18
  /**
19
- * Smart container rendering active social buttons.
20
- * Automatically filters out any unconfigured or empty platforms.
19
+ * Always-mounted social buttons so empty-state preview keeps field markers.
21
20
  */
22
21
  function SocialLinks({ social = {}, fieldPathPrefix = 'common.business.social', variant = 'icon', size = 'md', className = '', style, }) {
23
- // Find platforms with valid URLs
24
- const activePlatforms = Object.entries(social).filter(([platform, url]) => {
25
- return Boolean(url && typeof url === 'string' && url.trim().length > 0);
26
- });
27
- if (activePlatforms.length === 0) {
28
- return null;
29
- }
22
+ const platforms = SUPPORTED_PLATFORMS.filter((platform) => Object.prototype.hasOwnProperty.call(social, platform) || Object.keys(social).length === 0);
23
+ const rendered = platforms.length ? platforms : SUPPORTED_PLATFORMS;
30
24
  const containerStyles = {
31
25
  display: 'inline-flex',
32
26
  alignItems: 'center',
@@ -34,9 +28,5 @@ function SocialLinks({ social = {}, fieldPathPrefix = 'common.business.social',
34
28
  gap: '0.625rem',
35
29
  ...style,
36
30
  };
37
- return ((0, jsx_runtime_1.jsx)("div", { className: `deneb-social-links ${className}`.trim(), style: containerStyles, children: activePlatforms.map(([key, url]) => {
38
- const lowerKey = key.toLowerCase();
39
- const platform = SUPPORTED_PLATFORMS.includes(lowerKey) ? lowerKey : 'x';
40
- return ((0, jsx_runtime_1.jsx)(SocialButton_1.SocialButton, { platform: platform, url: url, fieldPath: `${fieldPathPrefix}.${key}`, variant: variant, size: size }, key));
41
- }) }));
31
+ return ((0, jsx_runtime_1.jsx)("div", { className: `deneb-social-links ${className}`.trim(), style: containerStyles, children: rendered.map((platform) => ((0, jsx_runtime_1.jsx)(SocialButton_1.SocialButton, { platform: platform, url: social[platform] ?? '', fieldPath: `${fieldPathPrefix}.${platform}`, variant: variant, size: size }, platform))) }));
42
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/ui",
3
- "version": "2.0.50",
3
+ "version": "2.0.52",
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.50"
52
+ "@deneb-ui/core": "^2.0.52"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "react": "^18.0.0 || ^19.0.0",