@forms.expert/sdk 0.2.10 → 0.2.12

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 +1 @@
1
- {"version":3,"sources":["../../core/index.ts","../../core/types.ts","../../core/api-client.ts","../../core/forms-sdk.ts"],"sourcesContent":["// Core exports\nexport * from './types';\nexport * from './api-client';\nexport * from './forms-sdk';\n","/**\n * Form field types\n */\nexport type BasicFieldType =\n | 'text' | 'email' | 'number' | 'textarea' | 'select'\n | 'checkbox' | 'file' | 'date' | 'hidden';\n\nexport type InteractiveFieldType =\n | 'radio' | 'multiselect' | 'rating' | 'scale' | 'toggle'\n | 'ranking' | 'imageChoice' | 'phone' | 'url' | 'password'\n | 'richText' | 'slider' | 'currency' | 'time' | 'datetime'\n | 'dateRange' | 'address' | 'name' | 'dropdown' | 'colorPicker'\n | 'location' | 'opinionScale' | 'consent';\n\nexport type LayoutFieldType = 'heading' | 'divider' | 'paragraph';\n\nexport type FormFieldType = BasicFieldType | InteractiveFieldType | LayoutFieldType;\n\nexport interface FormFieldOption {\n label: string;\n value: string;\n imageUrl?: string;\n}\n\n/**\n * Form field definition\n */\nexport interface FormField {\n name: string;\n type: FormFieldType;\n label?: string;\n placeholder?: string;\n required?: boolean;\n options?: string[] | FormFieldOption[];\n defaultValue?: unknown;\n maxFileSize?: number;\n allowedMimeTypes?: string[];\n multiple?: boolean;\n min?: number;\n max?: number;\n step?: number;\n ratingMax?: number;\n lowLabel?: string;\n highLabel?: string;\n defaultCountryCode?: string;\n currencyCode?: string;\n currencySymbol?: string;\n addressFields?: ('street' | 'street2' | 'city' | 'state' | 'zip' | 'country')[];\n nameFields?: ('prefix' | 'first' | 'middle' | 'last' | 'suffix')[];\n content?: string;\n paragraphFontSize?: number;\n consentText?: string;\n consentUrl?: string;\n maxLength?: number;\n stepId?: string;\n visibleWhen?: {\n field: string;\n operator: 'eq' | 'neq' | 'contains' | 'gt' | 'lt';\n value: unknown;\n };\n}\n\n/**\n * Secondary button configuration\n */\nexport interface SecondaryButton {\n enabled: boolean;\n text: string;\n href: string;\n openInNewTab?: boolean;\n position?: 'left' | 'right' | 'below';\n align?: 'left' | 'center' | 'right';\n marginTop?: number;\n marginBottom?: number;\n style?: 'filled' | 'outlined' | 'minimal' | 'link';\n color?: string;\n textColor?: string;\n fontSize?: number;\n}\n\n/**\n * Form styling configuration\n */\nexport interface FormStyling {\n theme: 'light' | 'dark' | 'system';\n primaryColor: string;\n backgroundColor: string;\n textColor: string;\n errorColor?: string;\n successColor?: string;\n borderRadius: 'none' | 'sm' | 'md' | 'lg';\n fontSize: 'sm' | 'md' | 'lg';\n buttonStyle: 'filled' | 'outline';\n labelPosition: 'top' | 'left' | 'floating';\n customCss?: string;\n // Extended styling (matching hosted form builder)\n fontFamily?: string;\n formWidth?: 'narrow' | 'medium' | 'wide' | 'full';\n fieldLayout?: 'stacked' | 'inline';\n fieldBorderStyle?: 'full' | 'bottom';\n buttonColor?: string;\n buttonText?: string;\n buttonRadius?: 'none' | 'small' | 'medium' | 'large' | 'full';\n buttonAlign?: 'left' | 'center' | 'right';\n buttonSize?: 'small' | 'medium' | 'large';\n buttonPaddingX?: number;\n buttonPaddingY?: number;\n buttonGradient?: string;\n buttonFullWidth?: boolean;\n buttonFontSize?: number;\n fieldSpacing?: 'compact' | 'normal' | 'relaxed' | 'spacious';\n formPadding?: 'none' | 'compact' | 'normal' | 'relaxed' | 'spacious';\n labelSpacing?: 'compact' | 'normal' | 'relaxed';\n placeholderFontSize?: 'small' | 'medium' | 'large';\n headingSize?: 'small' | 'medium' | 'large' | 'extra-large';\n paragraphSize?: 'small' | 'medium' | 'large';\n formNameFontSize?: number;\n formNameFontWeight?: 'normal' | 'medium' | 'semibold' | 'bold';\n hideRequiredAsterisk?: boolean;\n logoUrl?: string;\n logoPosition?: 'top-left' | 'top-center' | 'top-right';\n coverImageUrl?: string;\n backgroundImageUrl?: string;\n backgroundOverlay?: number;\n transparentBackground?: boolean;\n secondaryButton?: SecondaryButton;\n}\n\n/**\n * Form schema\n */\nexport interface FormSchema {\n fields: FormField[];\n styling?: FormStyling;\n}\n\n/**\n * Form captcha settings\n */\nexport interface CaptchaSettings {\n enabled: boolean;\n provider?: 'turnstile' | 'recaptcha' | 'hcaptcha';\n siteKey?: string;\n}\n\n/**\n * Form branding configuration\n */\nexport interface FormBranding {\n enabled: boolean;\n text?: string;\n url?: string;\n}\n\n/**\n * Form status response from API\n */\nexport interface FormStatusResponse {\n active: boolean;\n formId?: string;\n name?: string;\n description?: string | null;\n mode?: 'free' | 'schema';\n type?: 'hosted' | 'embed' | 'both';\n layout?: 'single' | 'multi-step';\n schema?: FormSchema;\n steps?: Array<{ title: string; description?: string; fields: string[] }>;\n /** Top-level styling (full hosted styling config) */\n styling?: FormStyling;\n embedConfig?: {\n width?: string;\n height?: string;\n maxHeight?: string;\n minHeight?: string;\n autoResize?: boolean;\n transparentBackground?: boolean;\n [key: string]: unknown;\n };\n error?: string;\n /** Captcha settings (top-level) */\n captcha?: CaptchaSettings;\n settings?: {\n /** @deprecated Use top-level captcha instead */\n captcha?: CaptchaSettings;\n honeypot: boolean;\n allowAttachments: boolean;\n maxAttachments?: number;\n maxAttachmentSize?: number;\n successMessage?: string;\n redirectUrl?: string;\n showFormName?: boolean;\n };\n /** Access control configuration */\n accessControl?: {\n mode: string;\n passwordProtected: boolean;\n };\n /** Branding configuration */\n branding?: FormBranding;\n /** Hosted config (page settings, success page, etc.) */\n hostedConfig?: {\n pageTitle?: string;\n successMessage?: string;\n successPageType?: 'message' | 'redirect' | 'custom';\n redirectUrl?: string;\n customSuccessHtml?: string;\n [key: string]: unknown;\n };\n closesAt?: string | null;\n resourceId?: string;\n /** Available published translation language codes */\n availableLanguages?: string[];\n /** Current language applied to this response (null if base language) */\n currentLanguage?: string | null;\n /** Whether to show language switch UI */\n showLanguageSwitch?: boolean;\n}\n\n/**\n * Validation error\n */\nexport interface ValidationError {\n field: string;\n message: string;\n}\n\n/**\n * Validation response from API\n */\nexport interface ValidationResponse {\n valid: boolean;\n errors: ValidationError[];\n}\n\n/**\n * Submission response from API\n */\nexport interface SubmissionResponse {\n success: boolean;\n submissionId: string;\n message: string;\n}\n\n/**\n * Submit options\n */\nexport interface SubmitOptions {\n pageUrl?: string;\n captchaToken?: string;\n /** Callback for upload progress */\n onProgress?: (progress: UploadProgress) => void;\n}\n\n/**\n * Upload progress information\n */\nexport interface UploadProgress {\n loaded: number;\n total: number;\n percentage: number;\n}\n\n/**\n * File validation error\n */\nexport interface FileValidationError {\n field: string;\n file: string;\n error: 'size' | 'type' | 'count';\n message: string;\n}\n\n/**\n * SDK configuration\n */\nexport interface FormsSDKConfig {\n apiKey: string;\n resourceId: string;\n baseUrl?: string;\n}\n\n/**\n * Form handler options\n */\nexport interface FormHandlerOptions {\n /** Track form views for analytics (completion rate) */\n trackViews?: boolean;\n onSubmitStart?: () => void;\n onSubmitSuccess?: (response: SubmissionResponse) => void;\n onSubmitError?: (error: FormsError) => void;\n onValidationError?: (errors: ValidationError[]) => void;\n}\n\n/**\n * Forms SDK error\n */\nexport class FormsError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode: number,\n public retryAfter?: number\n ) {\n super(message);\n this.name = 'FormsError';\n }\n}\n\n/**\n * Validation error class\n */\nexport class FormValidationError extends Error {\n constructor(public errors: ValidationError[]) {\n super('Validation failed');\n this.name = 'FormValidationError';\n }\n}\n","import {\n FormsSDKConfig,\n FormStatusResponse,\n ValidationResponse,\n SubmissionResponse,\n SubmitOptions,\n FormsError,\n UploadProgress,\n} from './types';\n\n/**\n * API client for forms backend\n */\nexport class FormsApiClient {\n private baseUrl: string;\n private apiKey: string;\n private resourceId: string;\n\n constructor(config: FormsSDKConfig) {\n this.apiKey = config.apiKey;\n this.resourceId = config.resourceId;\n this.baseUrl = (config.baseUrl || 'https://api.forms.expert/api/v1').replace(/\\/$/, '');\n }\n\n /**\n * Build URL with token query parameter\n */\n private buildUrl(path: string): string {\n const separator = path.includes('?') ? '&' : '?';\n return `${this.baseUrl}${path}${separator}token=${encodeURIComponent(this.apiKey)}`;\n }\n\n /**\n * Make an API request\n */\n private async request<T>(\n method: string,\n path: string,\n body?: object\n ): Promise<T> {\n const url = this.buildUrl(path);\n\n const response = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n throw new FormsError(\n data.message || 'Request failed',\n data.code || 'UNKNOWN_ERROR',\n response.status,\n data.retryAfter\n );\n }\n\n return data;\n }\n\n /**\n * Check if form is active and get configuration\n */\n async isActive(slug: string, lang?: string): Promise<FormStatusResponse> {\n const langParam = lang ? `?lang=${encodeURIComponent(lang)}` : '';\n return this.request('GET', `/f/${this.resourceId}/${slug}/is-active${langParam}`);\n }\n\n /**\n * Validate form data without submitting\n */\n async validate(\n slug: string,\n data: Record<string, unknown>\n ): Promise<ValidationResponse> {\n return this.request('POST', `/f/${this.resourceId}/${slug}/validate`, {\n data,\n });\n }\n\n /**\n * Submit form data (supports files)\n */\n async submit(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n const url = this.buildUrl(`/f/${this.resourceId}/${slug}`);\n \n // Check if data contains files\n const hasFiles = Object.values(data).some(\n (v) => v instanceof File || (v instanceof FileList && v.length > 0)\n );\n\n if (hasFiles || options?.onProgress) {\n // Use FormData and XMLHttpRequest for file uploads with progress\n return this.submitWithFormData(url, data, options);\n }\n\n // Use regular JSON request for non-file submissions\n return this.request('POST', `/f/${this.resourceId}/${slug}`, {\n data,\n pageUrl: options?.pageUrl || (typeof window !== 'undefined' ? window.location.href : undefined),\n captchaToken: options?.captchaToken,\n });\n }\n\n /**\n * Submit with FormData (for file uploads with progress tracking)\n */\n private submitWithFormData(\n url: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n return new Promise((resolve, reject) => {\n const formData = new FormData();\n\n // Add data fields\n for (const [key, value] of Object.entries(data)) {\n if (value instanceof File) {\n formData.append(key, value);\n } else if (value instanceof FileList) {\n Array.from(value).forEach((file) => formData.append(key, file));\n } else if (value !== undefined && value !== null) {\n formData.append(`data[${key}]`, String(value));\n }\n }\n\n // Add metadata\n const pageUrl = options?.pageUrl || (typeof window !== 'undefined' ? window.location.href : '');\n if (pageUrl) {\n formData.append('pageUrl', pageUrl);\n }\n if (options?.captchaToken) {\n formData.append('captchaToken', options.captchaToken);\n }\n\n const xhr = new XMLHttpRequest();\n\n // Progress tracking\n if (options?.onProgress) {\n xhr.upload.addEventListener('progress', (event) => {\n if (event.lengthComputable) {\n options.onProgress!({\n loaded: event.loaded,\n total: event.total,\n percentage: Math.round((event.loaded / event.total) * 100),\n });\n }\n });\n }\n\n xhr.addEventListener('load', () => {\n try {\n const response = JSON.parse(xhr.responseText);\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve(response);\n } else {\n reject(new FormsError(\n response.message || 'Submission failed',\n response.code || 'UNKNOWN_ERROR',\n xhr.status,\n response.retryAfter\n ));\n }\n } catch {\n reject(new FormsError('Invalid response', 'PARSE_ERROR', xhr.status));\n }\n });\n\n xhr.addEventListener('error', () => {\n reject(new FormsError('Network error', 'NETWORK_ERROR', 0));\n });\n\n xhr.addEventListener('abort', () => {\n reject(new FormsError('Request aborted', 'ABORTED', 0));\n });\n\n xhr.open('POST', url);\n xhr.send(formData);\n });\n }\n\n /**\n * Track a form view (for analytics completion rate)\n */\n async trackView(slug: string): Promise<void> {\n const url = this.buildUrl(`/f/${this.resourceId}/${slug}/view`);\n await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }).catch(() => {});\n }\n\n /**\n * Get resource ID\n */\n getResourceId(): string {\n return this.resourceId;\n }\n\n /**\n * Get base URL\n */\n getBaseUrl(): string {\n return this.baseUrl;\n }\n}\n","import { FormsApiClient } from './api-client';\nimport {\n FormsSDKConfig,\n FormStatusResponse,\n ValidationResponse,\n SubmissionResponse,\n SubmitOptions,\n FormHandlerOptions,\n FormsError,\n FormValidationError,\n} from './types';\n\n/**\n * Form handler for a specific form\n */\nexport class FormHandler {\n private apiClient: FormsApiClient;\n private slug: string;\n private config: FormStatusResponse | null = null;\n private options: FormHandlerOptions;\n\n constructor(\n apiClient: FormsApiClient,\n slug: string,\n options: FormHandlerOptions = {}\n ) {\n this.apiClient = apiClient;\n this.slug = slug;\n this.options = options;\n }\n\n /**\n * Initialize form handler and fetch configuration\n */\n async initialize(lang?: string): Promise<FormStatusResponse> {\n this.config = await this.apiClient.isActive(this.slug, lang);\n if (this.options.trackViews) {\n this.apiClient.trackView(this.slug);\n }\n return this.config;\n }\n\n /**\n * Get cached form configuration\n */\n getConfig(): FormStatusResponse | null {\n return this.config;\n }\n\n /**\n * Check if form is active\n */\n isActive(): boolean {\n return this.config?.active ?? false;\n }\n\n /**\n * Check if captcha is required\n */\n requiresCaptcha(): boolean {\n return this.config?.settings?.captcha?.enabled ?? false;\n }\n\n /**\n * Get captcha provider\n */\n getCaptchaProvider(): 'turnstile' | 'recaptcha' | 'hcaptcha' | undefined {\n return this.config?.settings?.captcha?.provider;\n }\n\n /**\n * Get form schema\n */\n getSchema() {\n return this.config?.schema;\n }\n\n /**\n * Validate form data\n */\n async validate(data: Record<string, unknown>): Promise<ValidationResponse> {\n return this.apiClient.validate(this.slug, data);\n }\n\n /**\n * Submit form data\n */\n async submit(\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n this.options.onSubmitStart?.();\n\n try {\n // Validate first if in schema mode\n if (this.config?.mode === 'schema') {\n const validation = await this.validate(data);\n if (!validation.valid) {\n this.options.onValidationError?.(validation.errors);\n throw new FormValidationError(validation.errors);\n }\n }\n\n const response = await this.apiClient.submit(this.slug, data, options);\n this.options.onSubmitSuccess?.(response);\n return response;\n } catch (error) {\n if (error instanceof FormsError) {\n this.options.onSubmitError?.(error);\n }\n throw error;\n }\n }\n\n /**\n * Get success message from config\n */\n getSuccessMessage(): string {\n return this.config?.settings?.successMessage || 'Form submitted successfully!';\n }\n\n /**\n * Get redirect URL from config\n */\n getRedirectUrl(): string | undefined {\n return this.config?.settings?.redirectUrl;\n }\n}\n\n/**\n * Main Forms SDK class\n */\nexport class FormsSDK {\n private apiClient: FormsApiClient;\n\n constructor(config: FormsSDKConfig) {\n this.apiClient = new FormsApiClient(config);\n }\n\n /**\n * Check if form is active and get configuration\n */\n async isActive(slug: string, lang?: string): Promise<FormStatusResponse> {\n return this.apiClient.isActive(slug, lang);\n }\n\n /**\n * Validate form data without submitting\n */\n async validate(\n slug: string,\n data: Record<string, unknown>\n ): Promise<ValidationResponse> {\n return this.apiClient.validate(slug, data);\n }\n\n /**\n * Submit form data\n */\n async submit(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n return this.apiClient.submit(slug, data, options);\n }\n\n /**\n * Create a form handler for a specific form\n */\n form(slug: string, options?: FormHandlerOptions): FormHandler {\n return new FormHandler(this.apiClient, slug, options);\n }\n\n /**\n * Track a form view (for analytics completion rate)\n */\n async trackView(slug: string): Promise<void> {\n return this.apiClient.trackView(slug);\n }\n\n /**\n * Submit with retry logic for rate limits\n */\n async submitWithRetry(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions & { maxRetries?: number }\n ): Promise<SubmissionResponse> {\n const maxRetries = options?.maxRetries ?? 3;\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n return await this.submit(slug, data, options);\n } catch (error) {\n lastError = error as Error;\n\n if (error instanceof FormsError) {\n // Don't retry validation or auth errors\n if (\n [\n 'VALIDATION_ERROR',\n 'CAPTCHA_REQUIRED',\n 'ORIGIN_NOT_ALLOWED',\n ].includes(error.code)\n ) {\n throw error;\n }\n\n // Retry rate limits with backoff\n if (error.code.includes('RATE_LIMIT')) {\n const retryAfter = error.retryAfter || Math.pow(2, attempt) * 1000;\n await new Promise((resolve) => setTimeout(resolve, retryAfter));\n continue;\n }\n }\n\n // Exponential backoff for network errors\n await new Promise((resolve) =>\n setTimeout(resolve, Math.pow(2, attempt) * 1000)\n );\n }\n }\n\n throw lastError!;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../core/index.ts","../../core/types.ts","../../core/api-client.ts","../../core/forms-sdk.ts"],"sourcesContent":["// Core exports\nexport * from './types';\nexport * from './api-client';\nexport * from './forms-sdk';\n","/**\n * Form field types\n */\nexport type BasicFieldType =\n | 'text' | 'email' | 'number' | 'textarea' | 'select'\n | 'checkbox' | 'file' | 'date' | 'hidden';\n\nexport type InteractiveFieldType =\n | 'radio' | 'multiselect' | 'rating' | 'scale' | 'toggle'\n | 'ranking' | 'imageChoice' | 'phone' | 'url' | 'password'\n | 'richText' | 'slider' | 'currency' | 'time' | 'datetime'\n | 'dateRange' | 'address' | 'name' | 'dropdown' | 'colorPicker'\n | 'location' | 'opinionScale' | 'consent';\n\nexport type LayoutFieldType = 'heading' | 'divider' | 'paragraph';\n\nexport type FormFieldType = BasicFieldType | InteractiveFieldType | LayoutFieldType;\n\nexport interface FormFieldOption {\n label: string;\n value: string;\n imageUrl?: string;\n}\n\n/**\n * Form field definition\n */\nexport interface FormField {\n name: string;\n type: FormFieldType;\n label?: string;\n placeholder?: string;\n required?: boolean;\n options?: string[] | FormFieldOption[];\n defaultValue?: unknown;\n maxFileSize?: number;\n allowedMimeTypes?: string[];\n multiple?: boolean;\n min?: number;\n max?: number;\n step?: number;\n ratingMax?: number;\n lowLabel?: string;\n highLabel?: string;\n defaultCountryCode?: string;\n currencyCode?: string;\n currencySymbol?: string;\n addressFields?: ('street' | 'street2' | 'city' | 'state' | 'zip' | 'country')[];\n nameFields?: ('prefix' | 'first' | 'middle' | 'last' | 'suffix')[];\n content?: string;\n paragraphFontSize?: number;\n consentText?: string;\n consentUrl?: string;\n maxLength?: number;\n stepId?: string;\n visibleWhen?: {\n field: string;\n operator: 'eq' | 'neq' | 'contains' | 'gt' | 'lt';\n value: unknown;\n };\n}\n\n/**\n * Secondary button configuration\n */\nexport interface SecondaryButton {\n enabled: boolean;\n text: string;\n href: string;\n openInNewTab?: boolean;\n position?: 'left' | 'right' | 'below';\n align?: 'left' | 'center' | 'right';\n marginTop?: number;\n marginBottom?: number;\n style?: 'filled' | 'outlined' | 'minimal' | 'link';\n color?: string;\n textColor?: string;\n fontSize?: number;\n}\n\n/**\n * Form styling configuration\n */\nexport interface FormStyling {\n theme: 'light' | 'dark' | 'system';\n primaryColor: string;\n backgroundColor: string;\n textColor: string;\n errorColor?: string;\n successColor?: string;\n borderRadius: 'none' | 'sm' | 'md' | 'lg';\n fontSize: 'sm' | 'md' | 'lg';\n buttonStyle: 'filled' | 'outline';\n labelPosition: 'top' | 'left' | 'floating';\n labelClassName?: string;\n fieldClassName?: string;\n buttonClassName?: string;\n customCss?: string;\n // Extended styling (matching hosted form builder)\n fontFamily?: string;\n formWidth?: 'narrow' | 'medium' | 'wide' | 'full';\n fieldLayout?: 'stacked' | 'inline';\n fieldBorderStyle?: 'full' | 'bottom';\n buttonColor?: string;\n buttonText?: string;\n buttonRadius?: 'none' | 'small' | 'medium' | 'large' | 'full';\n buttonAlign?: 'left' | 'center' | 'right';\n buttonSize?: 'small' | 'medium' | 'large';\n buttonPaddingX?: number;\n buttonPaddingY?: number;\n buttonGradient?: string;\n buttonFullWidth?: boolean;\n buttonFontSize?: number;\n fieldSpacing?: 'compact' | 'normal' | 'relaxed' | 'spacious';\n formPadding?: 'none' | 'compact' | 'normal' | 'relaxed' | 'spacious';\n labelSpacing?: 'compact' | 'normal' | 'relaxed';\n placeholderFontSize?: 'small' | 'medium' | 'large';\n headingSize?: 'small' | 'medium' | 'large' | 'extra-large';\n paragraphSize?: 'small' | 'medium' | 'large';\n formNameFontSize?: number;\n formNameFontWeight?: 'normal' | 'medium' | 'semibold' | 'bold';\n hideRequiredAsterisk?: boolean;\n logoUrl?: string;\n logoPosition?: 'top-left' | 'top-center' | 'top-right';\n coverImageUrl?: string;\n backgroundImageUrl?: string;\n backgroundOverlay?: number;\n transparentBackground?: boolean;\n secondaryButton?: SecondaryButton;\n}\n\n/**\n * Form schema\n */\nexport interface FormSchema {\n fields: FormField[];\n styling?: FormStyling;\n}\n\n/**\n * Form captcha settings\n */\nexport interface CaptchaSettings {\n enabled: boolean;\n provider?: 'turnstile' | 'recaptcha' | 'hcaptcha';\n siteKey?: string;\n}\n\n/**\n * Form branding configuration\n */\nexport interface FormBranding {\n enabled: boolean;\n text?: string;\n url?: string;\n}\n\n/**\n * Form status response from API\n */\nexport interface FormStatusResponse {\n active: boolean;\n formId?: string;\n name?: string;\n description?: string | null;\n mode?: 'free' | 'schema';\n type?: 'hosted' | 'embed' | 'both';\n layout?: 'single' | 'multi-step';\n schema?: FormSchema;\n steps?: Array<{ title: string; description?: string; fields: string[] }>;\n /** Top-level styling (full hosted styling config) */\n styling?: FormStyling;\n embedConfig?: {\n width?: string;\n height?: string;\n maxHeight?: string;\n minHeight?: string;\n autoResize?: boolean;\n transparentBackground?: boolean;\n [key: string]: unknown;\n };\n error?: string;\n /** Captcha settings (top-level) */\n captcha?: CaptchaSettings;\n settings?: {\n /** @deprecated Use top-level captcha instead */\n captcha?: CaptchaSettings;\n honeypot: boolean;\n allowAttachments: boolean;\n maxAttachments?: number;\n maxAttachmentSize?: number;\n successMessage?: string;\n redirectUrl?: string;\n showFormName?: boolean;\n };\n /** Access control configuration */\n accessControl?: {\n mode: string;\n passwordProtected: boolean;\n };\n /** Branding configuration */\n branding?: FormBranding;\n /** Hosted config (page settings, success page, etc.) */\n hostedConfig?: {\n pageTitle?: string;\n successMessage?: string;\n successPageType?: 'message' | 'redirect' | 'custom';\n redirectUrl?: string;\n customSuccessHtml?: string;\n [key: string]: unknown;\n };\n closesAt?: string | null;\n resourceId?: string;\n /** Available published translation language codes */\n availableLanguages?: string[];\n /** Current language applied to this response (null if base language) */\n currentLanguage?: string | null;\n /** Whether to show language switch UI */\n showLanguageSwitch?: boolean;\n}\n\n/**\n * Validation error\n */\nexport interface ValidationError {\n field: string;\n message: string;\n}\n\n/**\n * Validation response from API\n */\nexport interface ValidationResponse {\n valid: boolean;\n errors: ValidationError[];\n}\n\n/**\n * Submission response from API\n */\nexport interface SubmissionResponse {\n success: boolean;\n submissionId: string;\n message: string;\n}\n\n/**\n * Submit options\n */\nexport interface SubmitOptions {\n pageUrl?: string;\n captchaToken?: string;\n /** Callback for upload progress */\n onProgress?: (progress: UploadProgress) => void;\n}\n\n/**\n * Upload progress information\n */\nexport interface UploadProgress {\n loaded: number;\n total: number;\n percentage: number;\n}\n\n/**\n * File validation error\n */\nexport interface FileValidationError {\n field: string;\n file: string;\n error: 'size' | 'type' | 'count';\n message: string;\n}\n\n/**\n * SDK configuration\n */\nexport interface FormsSDKConfig {\n apiKey: string;\n resourceId: string;\n baseUrl?: string;\n}\n\n/**\n * Form handler options\n */\nexport interface FormHandlerOptions {\n /** Track form views for analytics (completion rate) */\n trackViews?: boolean;\n onSubmitStart?: () => void;\n onSubmitSuccess?: (response: SubmissionResponse) => void;\n onSubmitError?: (error: FormsError) => void;\n onValidationError?: (errors: ValidationError[]) => void;\n}\n\n/**\n * Forms SDK error\n */\nexport class FormsError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode: number,\n public retryAfter?: number\n ) {\n super(message);\n this.name = 'FormsError';\n }\n}\n\n/**\n * Validation error class\n */\nexport class FormValidationError extends Error {\n constructor(public errors: ValidationError[]) {\n super('Validation failed');\n this.name = 'FormValidationError';\n }\n}\n","import {\n FormsSDKConfig,\n FormStatusResponse,\n ValidationResponse,\n SubmissionResponse,\n SubmitOptions,\n FormsError,\n UploadProgress,\n} from './types';\n\n/**\n * API client for forms backend\n */\nexport class FormsApiClient {\n private baseUrl: string;\n private apiKey: string;\n private resourceId: string;\n\n constructor(config: FormsSDKConfig) {\n this.apiKey = config.apiKey;\n this.resourceId = config.resourceId;\n this.baseUrl = (config.baseUrl || 'https://api.forms.expert/api/v1').replace(/\\/$/, '');\n }\n\n /**\n * Build URL with token query parameter\n */\n private buildUrl(path: string): string {\n const separator = path.includes('?') ? '&' : '?';\n return `${this.baseUrl}${path}${separator}token=${encodeURIComponent(this.apiKey)}`;\n }\n\n /**\n * Make an API request\n */\n private async request<T>(\n method: string,\n path: string,\n body?: object\n ): Promise<T> {\n const url = this.buildUrl(path);\n\n const response = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n throw new FormsError(\n data.message || 'Request failed',\n data.code || 'UNKNOWN_ERROR',\n response.status,\n data.retryAfter\n );\n }\n\n return data;\n }\n\n /**\n * Check if form is active and get configuration\n */\n async isActive(slug: string, lang?: string): Promise<FormStatusResponse> {\n const langParam = lang ? `?lang=${encodeURIComponent(lang)}` : '';\n return this.request('GET', `/f/${this.resourceId}/${slug}/is-active${langParam}`);\n }\n\n /**\n * Validate form data without submitting\n */\n async validate(\n slug: string,\n data: Record<string, unknown>\n ): Promise<ValidationResponse> {\n return this.request('POST', `/f/${this.resourceId}/${slug}/validate`, {\n data,\n });\n }\n\n /**\n * Submit form data (supports files)\n */\n async submit(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n const url = this.buildUrl(`/f/${this.resourceId}/${slug}`);\n \n // Check if data contains files\n const hasFiles = Object.values(data).some(\n (v) => v instanceof File || (v instanceof FileList && v.length > 0)\n );\n\n if (hasFiles || options?.onProgress) {\n // Use FormData and XMLHttpRequest for file uploads with progress\n return this.submitWithFormData(url, data, options);\n }\n\n // Use regular JSON request for non-file submissions\n return this.request('POST', `/f/${this.resourceId}/${slug}`, {\n data,\n pageUrl: options?.pageUrl || (typeof window !== 'undefined' ? window.location.href : undefined),\n captchaToken: options?.captchaToken,\n });\n }\n\n /**\n * Submit with FormData (for file uploads with progress tracking)\n */\n private submitWithFormData(\n url: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n return new Promise((resolve, reject) => {\n const formData = new FormData();\n\n // Add data fields\n for (const [key, value] of Object.entries(data)) {\n if (value instanceof File) {\n formData.append(key, value);\n } else if (value instanceof FileList) {\n Array.from(value).forEach((file) => formData.append(key, file));\n } else if (value !== undefined && value !== null) {\n formData.append(`data[${key}]`, String(value));\n }\n }\n\n // Add metadata\n const pageUrl = options?.pageUrl || (typeof window !== 'undefined' ? window.location.href : '');\n if (pageUrl) {\n formData.append('pageUrl', pageUrl);\n }\n if (options?.captchaToken) {\n formData.append('captchaToken', options.captchaToken);\n }\n\n const xhr = new XMLHttpRequest();\n\n // Progress tracking\n if (options?.onProgress) {\n xhr.upload.addEventListener('progress', (event) => {\n if (event.lengthComputable) {\n options.onProgress!({\n loaded: event.loaded,\n total: event.total,\n percentage: Math.round((event.loaded / event.total) * 100),\n });\n }\n });\n }\n\n xhr.addEventListener('load', () => {\n try {\n const response = JSON.parse(xhr.responseText);\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve(response);\n } else {\n reject(new FormsError(\n response.message || 'Submission failed',\n response.code || 'UNKNOWN_ERROR',\n xhr.status,\n response.retryAfter\n ));\n }\n } catch {\n reject(new FormsError('Invalid response', 'PARSE_ERROR', xhr.status));\n }\n });\n\n xhr.addEventListener('error', () => {\n reject(new FormsError('Network error', 'NETWORK_ERROR', 0));\n });\n\n xhr.addEventListener('abort', () => {\n reject(new FormsError('Request aborted', 'ABORTED', 0));\n });\n\n xhr.open('POST', url);\n xhr.send(formData);\n });\n }\n\n /**\n * Track a form view (for analytics completion rate)\n */\n async trackView(slug: string): Promise<void> {\n const url = this.buildUrl(`/f/${this.resourceId}/${slug}/view`);\n await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }).catch(() => {});\n }\n\n /**\n * Get resource ID\n */\n getResourceId(): string {\n return this.resourceId;\n }\n\n /**\n * Get base URL\n */\n getBaseUrl(): string {\n return this.baseUrl;\n }\n}\n","import { FormsApiClient } from './api-client';\nimport {\n FormsSDKConfig,\n FormStatusResponse,\n ValidationResponse,\n SubmissionResponse,\n SubmitOptions,\n FormHandlerOptions,\n FormsError,\n FormValidationError,\n} from './types';\n\n/**\n * Form handler for a specific form\n */\nexport class FormHandler {\n private apiClient: FormsApiClient;\n private slug: string;\n private config: FormStatusResponse | null = null;\n private options: FormHandlerOptions;\n\n constructor(\n apiClient: FormsApiClient,\n slug: string,\n options: FormHandlerOptions = {}\n ) {\n this.apiClient = apiClient;\n this.slug = slug;\n this.options = options;\n }\n\n /**\n * Initialize form handler and fetch configuration\n */\n async initialize(lang?: string): Promise<FormStatusResponse> {\n this.config = await this.apiClient.isActive(this.slug, lang);\n if (this.options.trackViews) {\n this.apiClient.trackView(this.slug);\n }\n return this.config;\n }\n\n /**\n * Get cached form configuration\n */\n getConfig(): FormStatusResponse | null {\n return this.config;\n }\n\n /**\n * Check if form is active\n */\n isActive(): boolean {\n return this.config?.active ?? false;\n }\n\n /**\n * Check if captcha is required\n */\n requiresCaptcha(): boolean {\n return this.config?.settings?.captcha?.enabled ?? false;\n }\n\n /**\n * Get captcha provider\n */\n getCaptchaProvider(): 'turnstile' | 'recaptcha' | 'hcaptcha' | undefined {\n return this.config?.settings?.captcha?.provider;\n }\n\n /**\n * Get form schema\n */\n getSchema() {\n return this.config?.schema;\n }\n\n /**\n * Validate form data\n */\n async validate(data: Record<string, unknown>): Promise<ValidationResponse> {\n return this.apiClient.validate(this.slug, data);\n }\n\n /**\n * Submit form data\n */\n async submit(\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n this.options.onSubmitStart?.();\n\n try {\n // Validate first if in schema mode\n if (this.config?.mode === 'schema') {\n const validation = await this.validate(data);\n if (!validation.valid) {\n this.options.onValidationError?.(validation.errors);\n throw new FormValidationError(validation.errors);\n }\n }\n\n const response = await this.apiClient.submit(this.slug, data, options);\n this.options.onSubmitSuccess?.(response);\n return response;\n } catch (error) {\n if (error instanceof FormsError) {\n this.options.onSubmitError?.(error);\n }\n throw error;\n }\n }\n\n /**\n * Get success message from config\n */\n getSuccessMessage(): string {\n return this.config?.settings?.successMessage || 'Form submitted successfully!';\n }\n\n /**\n * Get redirect URL from config\n */\n getRedirectUrl(): string | undefined {\n return this.config?.settings?.redirectUrl;\n }\n}\n\n/**\n * Main Forms SDK class\n */\nexport class FormsSDK {\n private apiClient: FormsApiClient;\n\n constructor(config: FormsSDKConfig) {\n this.apiClient = new FormsApiClient(config);\n }\n\n /**\n * Check if form is active and get configuration\n */\n async isActive(slug: string, lang?: string): Promise<FormStatusResponse> {\n return this.apiClient.isActive(slug, lang);\n }\n\n /**\n * Validate form data without submitting\n */\n async validate(\n slug: string,\n data: Record<string, unknown>\n ): Promise<ValidationResponse> {\n return this.apiClient.validate(slug, data);\n }\n\n /**\n * Submit form data\n */\n async submit(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n return this.apiClient.submit(slug, data, options);\n }\n\n /**\n * Create a form handler for a specific form\n */\n form(slug: string, options?: FormHandlerOptions): FormHandler {\n return new FormHandler(this.apiClient, slug, options);\n }\n\n /**\n * Track a form view (for analytics completion rate)\n */\n async trackView(slug: string): Promise<void> {\n return this.apiClient.trackView(slug);\n }\n\n /**\n * Submit with retry logic for rate limits\n */\n async submitWithRetry(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions & { maxRetries?: number }\n ): Promise<SubmissionResponse> {\n const maxRetries = options?.maxRetries ?? 3;\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n return await this.submit(slug, data, options);\n } catch (error) {\n lastError = error as Error;\n\n if (error instanceof FormsError) {\n // Don't retry validation or auth errors\n if (\n [\n 'VALIDATION_ERROR',\n 'CAPTCHA_REQUIRED',\n 'ORIGIN_NOT_ALLOWED',\n ].includes(error.code)\n ) {\n throw error;\n }\n\n // Retry rate limits with backoff\n if (error.code.includes('RATE_LIMIT')) {\n const retryAfter = error.retryAfter || Math.pow(2, attempt) * 1000;\n await new Promise((resolve) => setTimeout(resolve, retryAfter));\n continue;\n }\n }\n\n // Exponential backoff for network errors\n await new Promise((resolve) =>\n setTimeout(resolve, Math.pow(2, attempt) * 1000)\n );\n }\n }\n\n throw lastError!;\n }\n}\n"],"mappings":"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","names":[]}
@@ -78,6 +78,9 @@ interface FormStyling {
78
78
  fontSize: 'sm' | 'md' | 'lg';
79
79
  buttonStyle: 'filled' | 'outline';
80
80
  labelPosition: 'top' | 'left' | 'floating';
81
+ labelClassName?: string;
82
+ fieldClassName?: string;
83
+ buttonClassName?: string;
81
84
  customCss?: string;
82
85
  fontFamily?: string;
83
86
  formWidth?: 'narrow' | 'medium' | 'wide' | 'full';
@@ -78,6 +78,9 @@ interface FormStyling {
78
78
  fontSize: 'sm' | 'md' | 'lg';
79
79
  buttonStyle: 'filled' | 'outline';
80
80
  labelPosition: 'top' | 'left' | 'floating';
81
+ labelClassName?: string;
82
+ fieldClassName?: string;
83
+ buttonClassName?: string;
81
84
  customCss?: string;
82
85
  fontFamily?: string;
83
86
  formWidth?: 'narrow' | 'medium' | 'wide' | 'full';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../core/types.ts","../../core/api-client.ts","../../core/forms-sdk.ts"],"sourcesContent":["/**\n * Form field types\n */\nexport type BasicFieldType =\n | 'text' | 'email' | 'number' | 'textarea' | 'select'\n | 'checkbox' | 'file' | 'date' | 'hidden';\n\nexport type InteractiveFieldType =\n | 'radio' | 'multiselect' | 'rating' | 'scale' | 'toggle'\n | 'ranking' | 'imageChoice' | 'phone' | 'url' | 'password'\n | 'richText' | 'slider' | 'currency' | 'time' | 'datetime'\n | 'dateRange' | 'address' | 'name' | 'dropdown' | 'colorPicker'\n | 'location' | 'opinionScale' | 'consent';\n\nexport type LayoutFieldType = 'heading' | 'divider' | 'paragraph';\n\nexport type FormFieldType = BasicFieldType | InteractiveFieldType | LayoutFieldType;\n\nexport interface FormFieldOption {\n label: string;\n value: string;\n imageUrl?: string;\n}\n\n/**\n * Form field definition\n */\nexport interface FormField {\n name: string;\n type: FormFieldType;\n label?: string;\n placeholder?: string;\n required?: boolean;\n options?: string[] | FormFieldOption[];\n defaultValue?: unknown;\n maxFileSize?: number;\n allowedMimeTypes?: string[];\n multiple?: boolean;\n min?: number;\n max?: number;\n step?: number;\n ratingMax?: number;\n lowLabel?: string;\n highLabel?: string;\n defaultCountryCode?: string;\n currencyCode?: string;\n currencySymbol?: string;\n addressFields?: ('street' | 'street2' | 'city' | 'state' | 'zip' | 'country')[];\n nameFields?: ('prefix' | 'first' | 'middle' | 'last' | 'suffix')[];\n content?: string;\n paragraphFontSize?: number;\n consentText?: string;\n consentUrl?: string;\n maxLength?: number;\n stepId?: string;\n visibleWhen?: {\n field: string;\n operator: 'eq' | 'neq' | 'contains' | 'gt' | 'lt';\n value: unknown;\n };\n}\n\n/**\n * Secondary button configuration\n */\nexport interface SecondaryButton {\n enabled: boolean;\n text: string;\n href: string;\n openInNewTab?: boolean;\n position?: 'left' | 'right' | 'below';\n align?: 'left' | 'center' | 'right';\n marginTop?: number;\n marginBottom?: number;\n style?: 'filled' | 'outlined' | 'minimal' | 'link';\n color?: string;\n textColor?: string;\n fontSize?: number;\n}\n\n/**\n * Form styling configuration\n */\nexport interface FormStyling {\n theme: 'light' | 'dark' | 'system';\n primaryColor: string;\n backgroundColor: string;\n textColor: string;\n errorColor?: string;\n successColor?: string;\n borderRadius: 'none' | 'sm' | 'md' | 'lg';\n fontSize: 'sm' | 'md' | 'lg';\n buttonStyle: 'filled' | 'outline';\n labelPosition: 'top' | 'left' | 'floating';\n customCss?: string;\n // Extended styling (matching hosted form builder)\n fontFamily?: string;\n formWidth?: 'narrow' | 'medium' | 'wide' | 'full';\n fieldLayout?: 'stacked' | 'inline';\n fieldBorderStyle?: 'full' | 'bottom';\n buttonColor?: string;\n buttonText?: string;\n buttonRadius?: 'none' | 'small' | 'medium' | 'large' | 'full';\n buttonAlign?: 'left' | 'center' | 'right';\n buttonSize?: 'small' | 'medium' | 'large';\n buttonPaddingX?: number;\n buttonPaddingY?: number;\n buttonGradient?: string;\n buttonFullWidth?: boolean;\n buttonFontSize?: number;\n fieldSpacing?: 'compact' | 'normal' | 'relaxed' | 'spacious';\n formPadding?: 'none' | 'compact' | 'normal' | 'relaxed' | 'spacious';\n labelSpacing?: 'compact' | 'normal' | 'relaxed';\n placeholderFontSize?: 'small' | 'medium' | 'large';\n headingSize?: 'small' | 'medium' | 'large' | 'extra-large';\n paragraphSize?: 'small' | 'medium' | 'large';\n formNameFontSize?: number;\n formNameFontWeight?: 'normal' | 'medium' | 'semibold' | 'bold';\n hideRequiredAsterisk?: boolean;\n logoUrl?: string;\n logoPosition?: 'top-left' | 'top-center' | 'top-right';\n coverImageUrl?: string;\n backgroundImageUrl?: string;\n backgroundOverlay?: number;\n transparentBackground?: boolean;\n secondaryButton?: SecondaryButton;\n}\n\n/**\n * Form schema\n */\nexport interface FormSchema {\n fields: FormField[];\n styling?: FormStyling;\n}\n\n/**\n * Form captcha settings\n */\nexport interface CaptchaSettings {\n enabled: boolean;\n provider?: 'turnstile' | 'recaptcha' | 'hcaptcha';\n siteKey?: string;\n}\n\n/**\n * Form branding configuration\n */\nexport interface FormBranding {\n enabled: boolean;\n text?: string;\n url?: string;\n}\n\n/**\n * Form status response from API\n */\nexport interface FormStatusResponse {\n active: boolean;\n formId?: string;\n name?: string;\n description?: string | null;\n mode?: 'free' | 'schema';\n type?: 'hosted' | 'embed' | 'both';\n layout?: 'single' | 'multi-step';\n schema?: FormSchema;\n steps?: Array<{ title: string; description?: string; fields: string[] }>;\n /** Top-level styling (full hosted styling config) */\n styling?: FormStyling;\n embedConfig?: {\n width?: string;\n height?: string;\n maxHeight?: string;\n minHeight?: string;\n autoResize?: boolean;\n transparentBackground?: boolean;\n [key: string]: unknown;\n };\n error?: string;\n /** Captcha settings (top-level) */\n captcha?: CaptchaSettings;\n settings?: {\n /** @deprecated Use top-level captcha instead */\n captcha?: CaptchaSettings;\n honeypot: boolean;\n allowAttachments: boolean;\n maxAttachments?: number;\n maxAttachmentSize?: number;\n successMessage?: string;\n redirectUrl?: string;\n showFormName?: boolean;\n };\n /** Access control configuration */\n accessControl?: {\n mode: string;\n passwordProtected: boolean;\n };\n /** Branding configuration */\n branding?: FormBranding;\n /** Hosted config (page settings, success page, etc.) */\n hostedConfig?: {\n pageTitle?: string;\n successMessage?: string;\n successPageType?: 'message' | 'redirect' | 'custom';\n redirectUrl?: string;\n customSuccessHtml?: string;\n [key: string]: unknown;\n };\n closesAt?: string | null;\n resourceId?: string;\n /** Available published translation language codes */\n availableLanguages?: string[];\n /** Current language applied to this response (null if base language) */\n currentLanguage?: string | null;\n /** Whether to show language switch UI */\n showLanguageSwitch?: boolean;\n}\n\n/**\n * Validation error\n */\nexport interface ValidationError {\n field: string;\n message: string;\n}\n\n/**\n * Validation response from API\n */\nexport interface ValidationResponse {\n valid: boolean;\n errors: ValidationError[];\n}\n\n/**\n * Submission response from API\n */\nexport interface SubmissionResponse {\n success: boolean;\n submissionId: string;\n message: string;\n}\n\n/**\n * Submit options\n */\nexport interface SubmitOptions {\n pageUrl?: string;\n captchaToken?: string;\n /** Callback for upload progress */\n onProgress?: (progress: UploadProgress) => void;\n}\n\n/**\n * Upload progress information\n */\nexport interface UploadProgress {\n loaded: number;\n total: number;\n percentage: number;\n}\n\n/**\n * File validation error\n */\nexport interface FileValidationError {\n field: string;\n file: string;\n error: 'size' | 'type' | 'count';\n message: string;\n}\n\n/**\n * SDK configuration\n */\nexport interface FormsSDKConfig {\n apiKey: string;\n resourceId: string;\n baseUrl?: string;\n}\n\n/**\n * Form handler options\n */\nexport interface FormHandlerOptions {\n /** Track form views for analytics (completion rate) */\n trackViews?: boolean;\n onSubmitStart?: () => void;\n onSubmitSuccess?: (response: SubmissionResponse) => void;\n onSubmitError?: (error: FormsError) => void;\n onValidationError?: (errors: ValidationError[]) => void;\n}\n\n/**\n * Forms SDK error\n */\nexport class FormsError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode: number,\n public retryAfter?: number\n ) {\n super(message);\n this.name = 'FormsError';\n }\n}\n\n/**\n * Validation error class\n */\nexport class FormValidationError extends Error {\n constructor(public errors: ValidationError[]) {\n super('Validation failed');\n this.name = 'FormValidationError';\n }\n}\n","import {\n FormsSDKConfig,\n FormStatusResponse,\n ValidationResponse,\n SubmissionResponse,\n SubmitOptions,\n FormsError,\n UploadProgress,\n} from './types';\n\n/**\n * API client for forms backend\n */\nexport class FormsApiClient {\n private baseUrl: string;\n private apiKey: string;\n private resourceId: string;\n\n constructor(config: FormsSDKConfig) {\n this.apiKey = config.apiKey;\n this.resourceId = config.resourceId;\n this.baseUrl = (config.baseUrl || 'https://api.forms.expert/api/v1').replace(/\\/$/, '');\n }\n\n /**\n * Build URL with token query parameter\n */\n private buildUrl(path: string): string {\n const separator = path.includes('?') ? '&' : '?';\n return `${this.baseUrl}${path}${separator}token=${encodeURIComponent(this.apiKey)}`;\n }\n\n /**\n * Make an API request\n */\n private async request<T>(\n method: string,\n path: string,\n body?: object\n ): Promise<T> {\n const url = this.buildUrl(path);\n\n const response = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n throw new FormsError(\n data.message || 'Request failed',\n data.code || 'UNKNOWN_ERROR',\n response.status,\n data.retryAfter\n );\n }\n\n return data;\n }\n\n /**\n * Check if form is active and get configuration\n */\n async isActive(slug: string, lang?: string): Promise<FormStatusResponse> {\n const langParam = lang ? `?lang=${encodeURIComponent(lang)}` : '';\n return this.request('GET', `/f/${this.resourceId}/${slug}/is-active${langParam}`);\n }\n\n /**\n * Validate form data without submitting\n */\n async validate(\n slug: string,\n data: Record<string, unknown>\n ): Promise<ValidationResponse> {\n return this.request('POST', `/f/${this.resourceId}/${slug}/validate`, {\n data,\n });\n }\n\n /**\n * Submit form data (supports files)\n */\n async submit(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n const url = this.buildUrl(`/f/${this.resourceId}/${slug}`);\n \n // Check if data contains files\n const hasFiles = Object.values(data).some(\n (v) => v instanceof File || (v instanceof FileList && v.length > 0)\n );\n\n if (hasFiles || options?.onProgress) {\n // Use FormData and XMLHttpRequest for file uploads with progress\n return this.submitWithFormData(url, data, options);\n }\n\n // Use regular JSON request for non-file submissions\n return this.request('POST', `/f/${this.resourceId}/${slug}`, {\n data,\n pageUrl: options?.pageUrl || (typeof window !== 'undefined' ? window.location.href : undefined),\n captchaToken: options?.captchaToken,\n });\n }\n\n /**\n * Submit with FormData (for file uploads with progress tracking)\n */\n private submitWithFormData(\n url: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n return new Promise((resolve, reject) => {\n const formData = new FormData();\n\n // Add data fields\n for (const [key, value] of Object.entries(data)) {\n if (value instanceof File) {\n formData.append(key, value);\n } else if (value instanceof FileList) {\n Array.from(value).forEach((file) => formData.append(key, file));\n } else if (value !== undefined && value !== null) {\n formData.append(`data[${key}]`, String(value));\n }\n }\n\n // Add metadata\n const pageUrl = options?.pageUrl || (typeof window !== 'undefined' ? window.location.href : '');\n if (pageUrl) {\n formData.append('pageUrl', pageUrl);\n }\n if (options?.captchaToken) {\n formData.append('captchaToken', options.captchaToken);\n }\n\n const xhr = new XMLHttpRequest();\n\n // Progress tracking\n if (options?.onProgress) {\n xhr.upload.addEventListener('progress', (event) => {\n if (event.lengthComputable) {\n options.onProgress!({\n loaded: event.loaded,\n total: event.total,\n percentage: Math.round((event.loaded / event.total) * 100),\n });\n }\n });\n }\n\n xhr.addEventListener('load', () => {\n try {\n const response = JSON.parse(xhr.responseText);\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve(response);\n } else {\n reject(new FormsError(\n response.message || 'Submission failed',\n response.code || 'UNKNOWN_ERROR',\n xhr.status,\n response.retryAfter\n ));\n }\n } catch {\n reject(new FormsError('Invalid response', 'PARSE_ERROR', xhr.status));\n }\n });\n\n xhr.addEventListener('error', () => {\n reject(new FormsError('Network error', 'NETWORK_ERROR', 0));\n });\n\n xhr.addEventListener('abort', () => {\n reject(new FormsError('Request aborted', 'ABORTED', 0));\n });\n\n xhr.open('POST', url);\n xhr.send(formData);\n });\n }\n\n /**\n * Track a form view (for analytics completion rate)\n */\n async trackView(slug: string): Promise<void> {\n const url = this.buildUrl(`/f/${this.resourceId}/${slug}/view`);\n await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }).catch(() => {});\n }\n\n /**\n * Get resource ID\n */\n getResourceId(): string {\n return this.resourceId;\n }\n\n /**\n * Get base URL\n */\n getBaseUrl(): string {\n return this.baseUrl;\n }\n}\n","import { FormsApiClient } from './api-client';\nimport {\n FormsSDKConfig,\n FormStatusResponse,\n ValidationResponse,\n SubmissionResponse,\n SubmitOptions,\n FormHandlerOptions,\n FormsError,\n FormValidationError,\n} from './types';\n\n/**\n * Form handler for a specific form\n */\nexport class FormHandler {\n private apiClient: FormsApiClient;\n private slug: string;\n private config: FormStatusResponse | null = null;\n private options: FormHandlerOptions;\n\n constructor(\n apiClient: FormsApiClient,\n slug: string,\n options: FormHandlerOptions = {}\n ) {\n this.apiClient = apiClient;\n this.slug = slug;\n this.options = options;\n }\n\n /**\n * Initialize form handler and fetch configuration\n */\n async initialize(lang?: string): Promise<FormStatusResponse> {\n this.config = await this.apiClient.isActive(this.slug, lang);\n if (this.options.trackViews) {\n this.apiClient.trackView(this.slug);\n }\n return this.config;\n }\n\n /**\n * Get cached form configuration\n */\n getConfig(): FormStatusResponse | null {\n return this.config;\n }\n\n /**\n * Check if form is active\n */\n isActive(): boolean {\n return this.config?.active ?? false;\n }\n\n /**\n * Check if captcha is required\n */\n requiresCaptcha(): boolean {\n return this.config?.settings?.captcha?.enabled ?? false;\n }\n\n /**\n * Get captcha provider\n */\n getCaptchaProvider(): 'turnstile' | 'recaptcha' | 'hcaptcha' | undefined {\n return this.config?.settings?.captcha?.provider;\n }\n\n /**\n * Get form schema\n */\n getSchema() {\n return this.config?.schema;\n }\n\n /**\n * Validate form data\n */\n async validate(data: Record<string, unknown>): Promise<ValidationResponse> {\n return this.apiClient.validate(this.slug, data);\n }\n\n /**\n * Submit form data\n */\n async submit(\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n this.options.onSubmitStart?.();\n\n try {\n // Validate first if in schema mode\n if (this.config?.mode === 'schema') {\n const validation = await this.validate(data);\n if (!validation.valid) {\n this.options.onValidationError?.(validation.errors);\n throw new FormValidationError(validation.errors);\n }\n }\n\n const response = await this.apiClient.submit(this.slug, data, options);\n this.options.onSubmitSuccess?.(response);\n return response;\n } catch (error) {\n if (error instanceof FormsError) {\n this.options.onSubmitError?.(error);\n }\n throw error;\n }\n }\n\n /**\n * Get success message from config\n */\n getSuccessMessage(): string {\n return this.config?.settings?.successMessage || 'Form submitted successfully!';\n }\n\n /**\n * Get redirect URL from config\n */\n getRedirectUrl(): string | undefined {\n return this.config?.settings?.redirectUrl;\n }\n}\n\n/**\n * Main Forms SDK class\n */\nexport class FormsSDK {\n private apiClient: FormsApiClient;\n\n constructor(config: FormsSDKConfig) {\n this.apiClient = new FormsApiClient(config);\n }\n\n /**\n * Check if form is active and get configuration\n */\n async isActive(slug: string, lang?: string): Promise<FormStatusResponse> {\n return this.apiClient.isActive(slug, lang);\n }\n\n /**\n * Validate form data without submitting\n */\n async validate(\n slug: string,\n data: Record<string, unknown>\n ): Promise<ValidationResponse> {\n return this.apiClient.validate(slug, data);\n }\n\n /**\n * Submit form data\n */\n async submit(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n return this.apiClient.submit(slug, data, options);\n }\n\n /**\n * Create a form handler for a specific form\n */\n form(slug: string, options?: FormHandlerOptions): FormHandler {\n return new FormHandler(this.apiClient, slug, options);\n }\n\n /**\n * Track a form view (for analytics completion rate)\n */\n async trackView(slug: string): Promise<void> {\n return this.apiClient.trackView(slug);\n }\n\n /**\n * Submit with retry logic for rate limits\n */\n async submitWithRetry(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions & { maxRetries?: number }\n ): Promise<SubmissionResponse> {\n const maxRetries = options?.maxRetries ?? 3;\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n return await this.submit(slug, data, options);\n } catch (error) {\n lastError = error as Error;\n\n if (error instanceof FormsError) {\n // Don't retry validation or auth errors\n if (\n [\n 'VALIDATION_ERROR',\n 'CAPTCHA_REQUIRED',\n 'ORIGIN_NOT_ALLOWED',\n ].includes(error.code)\n ) {\n throw error;\n }\n\n // Retry rate limits with backoff\n if (error.code.includes('RATE_LIMIT')) {\n const retryAfter = error.retryAfter || Math.pow(2, attempt) * 1000;\n await new Promise((resolve) => setTimeout(resolve, retryAfter));\n continue;\n }\n }\n\n // Exponential backoff for network errors\n await new Promise((resolve) =>\n setTimeout(resolve, Math.pow(2, attempt) * 1000)\n );\n }\n }\n\n throw lastError!;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../core/types.ts","../../core/api-client.ts","../../core/forms-sdk.ts"],"sourcesContent":["/**\n * Form field types\n */\nexport type BasicFieldType =\n | 'text' | 'email' | 'number' | 'textarea' | 'select'\n | 'checkbox' | 'file' | 'date' | 'hidden';\n\nexport type InteractiveFieldType =\n | 'radio' | 'multiselect' | 'rating' | 'scale' | 'toggle'\n | 'ranking' | 'imageChoice' | 'phone' | 'url' | 'password'\n | 'richText' | 'slider' | 'currency' | 'time' | 'datetime'\n | 'dateRange' | 'address' | 'name' | 'dropdown' | 'colorPicker'\n | 'location' | 'opinionScale' | 'consent';\n\nexport type LayoutFieldType = 'heading' | 'divider' | 'paragraph';\n\nexport type FormFieldType = BasicFieldType | InteractiveFieldType | LayoutFieldType;\n\nexport interface FormFieldOption {\n label: string;\n value: string;\n imageUrl?: string;\n}\n\n/**\n * Form field definition\n */\nexport interface FormField {\n name: string;\n type: FormFieldType;\n label?: string;\n placeholder?: string;\n required?: boolean;\n options?: string[] | FormFieldOption[];\n defaultValue?: unknown;\n maxFileSize?: number;\n allowedMimeTypes?: string[];\n multiple?: boolean;\n min?: number;\n max?: number;\n step?: number;\n ratingMax?: number;\n lowLabel?: string;\n highLabel?: string;\n defaultCountryCode?: string;\n currencyCode?: string;\n currencySymbol?: string;\n addressFields?: ('street' | 'street2' | 'city' | 'state' | 'zip' | 'country')[];\n nameFields?: ('prefix' | 'first' | 'middle' | 'last' | 'suffix')[];\n content?: string;\n paragraphFontSize?: number;\n consentText?: string;\n consentUrl?: string;\n maxLength?: number;\n stepId?: string;\n visibleWhen?: {\n field: string;\n operator: 'eq' | 'neq' | 'contains' | 'gt' | 'lt';\n value: unknown;\n };\n}\n\n/**\n * Secondary button configuration\n */\nexport interface SecondaryButton {\n enabled: boolean;\n text: string;\n href: string;\n openInNewTab?: boolean;\n position?: 'left' | 'right' | 'below';\n align?: 'left' | 'center' | 'right';\n marginTop?: number;\n marginBottom?: number;\n style?: 'filled' | 'outlined' | 'minimal' | 'link';\n color?: string;\n textColor?: string;\n fontSize?: number;\n}\n\n/**\n * Form styling configuration\n */\nexport interface FormStyling {\n theme: 'light' | 'dark' | 'system';\n primaryColor: string;\n backgroundColor: string;\n textColor: string;\n errorColor?: string;\n successColor?: string;\n borderRadius: 'none' | 'sm' | 'md' | 'lg';\n fontSize: 'sm' | 'md' | 'lg';\n buttonStyle: 'filled' | 'outline';\n labelPosition: 'top' | 'left' | 'floating';\n labelClassName?: string;\n fieldClassName?: string;\n buttonClassName?: string;\n customCss?: string;\n // Extended styling (matching hosted form builder)\n fontFamily?: string;\n formWidth?: 'narrow' | 'medium' | 'wide' | 'full';\n fieldLayout?: 'stacked' | 'inline';\n fieldBorderStyle?: 'full' | 'bottom';\n buttonColor?: string;\n buttonText?: string;\n buttonRadius?: 'none' | 'small' | 'medium' | 'large' | 'full';\n buttonAlign?: 'left' | 'center' | 'right';\n buttonSize?: 'small' | 'medium' | 'large';\n buttonPaddingX?: number;\n buttonPaddingY?: number;\n buttonGradient?: string;\n buttonFullWidth?: boolean;\n buttonFontSize?: number;\n fieldSpacing?: 'compact' | 'normal' | 'relaxed' | 'spacious';\n formPadding?: 'none' | 'compact' | 'normal' | 'relaxed' | 'spacious';\n labelSpacing?: 'compact' | 'normal' | 'relaxed';\n placeholderFontSize?: 'small' | 'medium' | 'large';\n headingSize?: 'small' | 'medium' | 'large' | 'extra-large';\n paragraphSize?: 'small' | 'medium' | 'large';\n formNameFontSize?: number;\n formNameFontWeight?: 'normal' | 'medium' | 'semibold' | 'bold';\n hideRequiredAsterisk?: boolean;\n logoUrl?: string;\n logoPosition?: 'top-left' | 'top-center' | 'top-right';\n coverImageUrl?: string;\n backgroundImageUrl?: string;\n backgroundOverlay?: number;\n transparentBackground?: boolean;\n secondaryButton?: SecondaryButton;\n}\n\n/**\n * Form schema\n */\nexport interface FormSchema {\n fields: FormField[];\n styling?: FormStyling;\n}\n\n/**\n * Form captcha settings\n */\nexport interface CaptchaSettings {\n enabled: boolean;\n provider?: 'turnstile' | 'recaptcha' | 'hcaptcha';\n siteKey?: string;\n}\n\n/**\n * Form branding configuration\n */\nexport interface FormBranding {\n enabled: boolean;\n text?: string;\n url?: string;\n}\n\n/**\n * Form status response from API\n */\nexport interface FormStatusResponse {\n active: boolean;\n formId?: string;\n name?: string;\n description?: string | null;\n mode?: 'free' | 'schema';\n type?: 'hosted' | 'embed' | 'both';\n layout?: 'single' | 'multi-step';\n schema?: FormSchema;\n steps?: Array<{ title: string; description?: string; fields: string[] }>;\n /** Top-level styling (full hosted styling config) */\n styling?: FormStyling;\n embedConfig?: {\n width?: string;\n height?: string;\n maxHeight?: string;\n minHeight?: string;\n autoResize?: boolean;\n transparentBackground?: boolean;\n [key: string]: unknown;\n };\n error?: string;\n /** Captcha settings (top-level) */\n captcha?: CaptchaSettings;\n settings?: {\n /** @deprecated Use top-level captcha instead */\n captcha?: CaptchaSettings;\n honeypot: boolean;\n allowAttachments: boolean;\n maxAttachments?: number;\n maxAttachmentSize?: number;\n successMessage?: string;\n redirectUrl?: string;\n showFormName?: boolean;\n };\n /** Access control configuration */\n accessControl?: {\n mode: string;\n passwordProtected: boolean;\n };\n /** Branding configuration */\n branding?: FormBranding;\n /** Hosted config (page settings, success page, etc.) */\n hostedConfig?: {\n pageTitle?: string;\n successMessage?: string;\n successPageType?: 'message' | 'redirect' | 'custom';\n redirectUrl?: string;\n customSuccessHtml?: string;\n [key: string]: unknown;\n };\n closesAt?: string | null;\n resourceId?: string;\n /** Available published translation language codes */\n availableLanguages?: string[];\n /** Current language applied to this response (null if base language) */\n currentLanguage?: string | null;\n /** Whether to show language switch UI */\n showLanguageSwitch?: boolean;\n}\n\n/**\n * Validation error\n */\nexport interface ValidationError {\n field: string;\n message: string;\n}\n\n/**\n * Validation response from API\n */\nexport interface ValidationResponse {\n valid: boolean;\n errors: ValidationError[];\n}\n\n/**\n * Submission response from API\n */\nexport interface SubmissionResponse {\n success: boolean;\n submissionId: string;\n message: string;\n}\n\n/**\n * Submit options\n */\nexport interface SubmitOptions {\n pageUrl?: string;\n captchaToken?: string;\n /** Callback for upload progress */\n onProgress?: (progress: UploadProgress) => void;\n}\n\n/**\n * Upload progress information\n */\nexport interface UploadProgress {\n loaded: number;\n total: number;\n percentage: number;\n}\n\n/**\n * File validation error\n */\nexport interface FileValidationError {\n field: string;\n file: string;\n error: 'size' | 'type' | 'count';\n message: string;\n}\n\n/**\n * SDK configuration\n */\nexport interface FormsSDKConfig {\n apiKey: string;\n resourceId: string;\n baseUrl?: string;\n}\n\n/**\n * Form handler options\n */\nexport interface FormHandlerOptions {\n /** Track form views for analytics (completion rate) */\n trackViews?: boolean;\n onSubmitStart?: () => void;\n onSubmitSuccess?: (response: SubmissionResponse) => void;\n onSubmitError?: (error: FormsError) => void;\n onValidationError?: (errors: ValidationError[]) => void;\n}\n\n/**\n * Forms SDK error\n */\nexport class FormsError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode: number,\n public retryAfter?: number\n ) {\n super(message);\n this.name = 'FormsError';\n }\n}\n\n/**\n * Validation error class\n */\nexport class FormValidationError extends Error {\n constructor(public errors: ValidationError[]) {\n super('Validation failed');\n this.name = 'FormValidationError';\n }\n}\n","import {\n FormsSDKConfig,\n FormStatusResponse,\n ValidationResponse,\n SubmissionResponse,\n SubmitOptions,\n FormsError,\n UploadProgress,\n} from './types';\n\n/**\n * API client for forms backend\n */\nexport class FormsApiClient {\n private baseUrl: string;\n private apiKey: string;\n private resourceId: string;\n\n constructor(config: FormsSDKConfig) {\n this.apiKey = config.apiKey;\n this.resourceId = config.resourceId;\n this.baseUrl = (config.baseUrl || 'https://api.forms.expert/api/v1').replace(/\\/$/, '');\n }\n\n /**\n * Build URL with token query parameter\n */\n private buildUrl(path: string): string {\n const separator = path.includes('?') ? '&' : '?';\n return `${this.baseUrl}${path}${separator}token=${encodeURIComponent(this.apiKey)}`;\n }\n\n /**\n * Make an API request\n */\n private async request<T>(\n method: string,\n path: string,\n body?: object\n ): Promise<T> {\n const url = this.buildUrl(path);\n\n const response = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n throw new FormsError(\n data.message || 'Request failed',\n data.code || 'UNKNOWN_ERROR',\n response.status,\n data.retryAfter\n );\n }\n\n return data;\n }\n\n /**\n * Check if form is active and get configuration\n */\n async isActive(slug: string, lang?: string): Promise<FormStatusResponse> {\n const langParam = lang ? `?lang=${encodeURIComponent(lang)}` : '';\n return this.request('GET', `/f/${this.resourceId}/${slug}/is-active${langParam}`);\n }\n\n /**\n * Validate form data without submitting\n */\n async validate(\n slug: string,\n data: Record<string, unknown>\n ): Promise<ValidationResponse> {\n return this.request('POST', `/f/${this.resourceId}/${slug}/validate`, {\n data,\n });\n }\n\n /**\n * Submit form data (supports files)\n */\n async submit(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n const url = this.buildUrl(`/f/${this.resourceId}/${slug}`);\n \n // Check if data contains files\n const hasFiles = Object.values(data).some(\n (v) => v instanceof File || (v instanceof FileList && v.length > 0)\n );\n\n if (hasFiles || options?.onProgress) {\n // Use FormData and XMLHttpRequest for file uploads with progress\n return this.submitWithFormData(url, data, options);\n }\n\n // Use regular JSON request for non-file submissions\n return this.request('POST', `/f/${this.resourceId}/${slug}`, {\n data,\n pageUrl: options?.pageUrl || (typeof window !== 'undefined' ? window.location.href : undefined),\n captchaToken: options?.captchaToken,\n });\n }\n\n /**\n * Submit with FormData (for file uploads with progress tracking)\n */\n private submitWithFormData(\n url: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n return new Promise((resolve, reject) => {\n const formData = new FormData();\n\n // Add data fields\n for (const [key, value] of Object.entries(data)) {\n if (value instanceof File) {\n formData.append(key, value);\n } else if (value instanceof FileList) {\n Array.from(value).forEach((file) => formData.append(key, file));\n } else if (value !== undefined && value !== null) {\n formData.append(`data[${key}]`, String(value));\n }\n }\n\n // Add metadata\n const pageUrl = options?.pageUrl || (typeof window !== 'undefined' ? window.location.href : '');\n if (pageUrl) {\n formData.append('pageUrl', pageUrl);\n }\n if (options?.captchaToken) {\n formData.append('captchaToken', options.captchaToken);\n }\n\n const xhr = new XMLHttpRequest();\n\n // Progress tracking\n if (options?.onProgress) {\n xhr.upload.addEventListener('progress', (event) => {\n if (event.lengthComputable) {\n options.onProgress!({\n loaded: event.loaded,\n total: event.total,\n percentage: Math.round((event.loaded / event.total) * 100),\n });\n }\n });\n }\n\n xhr.addEventListener('load', () => {\n try {\n const response = JSON.parse(xhr.responseText);\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve(response);\n } else {\n reject(new FormsError(\n response.message || 'Submission failed',\n response.code || 'UNKNOWN_ERROR',\n xhr.status,\n response.retryAfter\n ));\n }\n } catch {\n reject(new FormsError('Invalid response', 'PARSE_ERROR', xhr.status));\n }\n });\n\n xhr.addEventListener('error', () => {\n reject(new FormsError('Network error', 'NETWORK_ERROR', 0));\n });\n\n xhr.addEventListener('abort', () => {\n reject(new FormsError('Request aborted', 'ABORTED', 0));\n });\n\n xhr.open('POST', url);\n xhr.send(formData);\n });\n }\n\n /**\n * Track a form view (for analytics completion rate)\n */\n async trackView(slug: string): Promise<void> {\n const url = this.buildUrl(`/f/${this.resourceId}/${slug}/view`);\n await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }).catch(() => {});\n }\n\n /**\n * Get resource ID\n */\n getResourceId(): string {\n return this.resourceId;\n }\n\n /**\n * Get base URL\n */\n getBaseUrl(): string {\n return this.baseUrl;\n }\n}\n","import { FormsApiClient } from './api-client';\nimport {\n FormsSDKConfig,\n FormStatusResponse,\n ValidationResponse,\n SubmissionResponse,\n SubmitOptions,\n FormHandlerOptions,\n FormsError,\n FormValidationError,\n} from './types';\n\n/**\n * Form handler for a specific form\n */\nexport class FormHandler {\n private apiClient: FormsApiClient;\n private slug: string;\n private config: FormStatusResponse | null = null;\n private options: FormHandlerOptions;\n\n constructor(\n apiClient: FormsApiClient,\n slug: string,\n options: FormHandlerOptions = {}\n ) {\n this.apiClient = apiClient;\n this.slug = slug;\n this.options = options;\n }\n\n /**\n * Initialize form handler and fetch configuration\n */\n async initialize(lang?: string): Promise<FormStatusResponse> {\n this.config = await this.apiClient.isActive(this.slug, lang);\n if (this.options.trackViews) {\n this.apiClient.trackView(this.slug);\n }\n return this.config;\n }\n\n /**\n * Get cached form configuration\n */\n getConfig(): FormStatusResponse | null {\n return this.config;\n }\n\n /**\n * Check if form is active\n */\n isActive(): boolean {\n return this.config?.active ?? false;\n }\n\n /**\n * Check if captcha is required\n */\n requiresCaptcha(): boolean {\n return this.config?.settings?.captcha?.enabled ?? false;\n }\n\n /**\n * Get captcha provider\n */\n getCaptchaProvider(): 'turnstile' | 'recaptcha' | 'hcaptcha' | undefined {\n return this.config?.settings?.captcha?.provider;\n }\n\n /**\n * Get form schema\n */\n getSchema() {\n return this.config?.schema;\n }\n\n /**\n * Validate form data\n */\n async validate(data: Record<string, unknown>): Promise<ValidationResponse> {\n return this.apiClient.validate(this.slug, data);\n }\n\n /**\n * Submit form data\n */\n async submit(\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n this.options.onSubmitStart?.();\n\n try {\n // Validate first if in schema mode\n if (this.config?.mode === 'schema') {\n const validation = await this.validate(data);\n if (!validation.valid) {\n this.options.onValidationError?.(validation.errors);\n throw new FormValidationError(validation.errors);\n }\n }\n\n const response = await this.apiClient.submit(this.slug, data, options);\n this.options.onSubmitSuccess?.(response);\n return response;\n } catch (error) {\n if (error instanceof FormsError) {\n this.options.onSubmitError?.(error);\n }\n throw error;\n }\n }\n\n /**\n * Get success message from config\n */\n getSuccessMessage(): string {\n return this.config?.settings?.successMessage || 'Form submitted successfully!';\n }\n\n /**\n * Get redirect URL from config\n */\n getRedirectUrl(): string | undefined {\n return this.config?.settings?.redirectUrl;\n }\n}\n\n/**\n * Main Forms SDK class\n */\nexport class FormsSDK {\n private apiClient: FormsApiClient;\n\n constructor(config: FormsSDKConfig) {\n this.apiClient = new FormsApiClient(config);\n }\n\n /**\n * Check if form is active and get configuration\n */\n async isActive(slug: string, lang?: string): Promise<FormStatusResponse> {\n return this.apiClient.isActive(slug, lang);\n }\n\n /**\n * Validate form data without submitting\n */\n async validate(\n slug: string,\n data: Record<string, unknown>\n ): Promise<ValidationResponse> {\n return this.apiClient.validate(slug, data);\n }\n\n /**\n * Submit form data\n */\n async submit(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions\n ): Promise<SubmissionResponse> {\n return this.apiClient.submit(slug, data, options);\n }\n\n /**\n * Create a form handler for a specific form\n */\n form(slug: string, options?: FormHandlerOptions): FormHandler {\n return new FormHandler(this.apiClient, slug, options);\n }\n\n /**\n * Track a form view (for analytics completion rate)\n */\n async trackView(slug: string): Promise<void> {\n return this.apiClient.trackView(slug);\n }\n\n /**\n * Submit with retry logic for rate limits\n */\n async submitWithRetry(\n slug: string,\n data: Record<string, unknown>,\n options?: SubmitOptions & { maxRetries?: number }\n ): Promise<SubmissionResponse> {\n const maxRetries = options?.maxRetries ?? 3;\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n return await this.submit(slug, data, options);\n } catch (error) {\n lastError = error as Error;\n\n if (error instanceof FormsError) {\n // Don't retry validation or auth errors\n if (\n [\n 'VALIDATION_ERROR',\n 'CAPTCHA_REQUIRED',\n 'ORIGIN_NOT_ALLOWED',\n ].includes(error.code)\n ) {\n throw error;\n }\n\n // Retry rate limits with backoff\n if (error.code.includes('RATE_LIMIT')) {\n const retryAfter = error.retryAfter || Math.pow(2, attempt) * 1000;\n await new Promise((resolve) => setTimeout(resolve, retryAfter));\n continue;\n }\n }\n\n // Exponential backoff for network errors\n await new Promise((resolve) =>\n setTimeout(resolve, Math.pow(2, attempt) * 1000)\n );\n }\n }\n\n throw lastError!;\n }\n}\n"],"mappings":"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","names":[]}
@@ -815,7 +815,34 @@ function FormsExpertForm({
815
815
  const resolvedButtonText = styling.buttonText || submitText;
816
816
  const googleFonts = ["Inter", "Roboto", "Open Sans", "Lato", "Poppins", "Montserrat", "Nunito", "Source Sans Pro", "Raleway", "Ubuntu", "Playfair Display", "Merriweather"];
817
817
  const fontToLoad = styling.fontFamily?.split(",")[0]?.trim();
818
- const googleFontUrl = fontToLoad && googleFonts.includes(fontToLoad) ? `https://fonts.googleapis.com/css2?family=${fontToLoad.replace(/ /g, "+")}:wght@400;500;600;700&display=swap` : null;
818
+ const hasExistingFonts = (0, import_react2.useMemo)(() => {
819
+ if (typeof document === "undefined") return false;
820
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
821
+ for (const link of links) {
822
+ const href = link.href || "";
823
+ if (href.includes("fonts.googleapis.com") || href.includes("fonts.gstatic.com") || href.includes("typekit") || href.includes("fonts.")) {
824
+ return true;
825
+ }
826
+ }
827
+ try {
828
+ for (const sheet of document.styleSheets) {
829
+ try {
830
+ const rules = sheet.cssRules || sheet.rules;
831
+ if (rules) {
832
+ for (const rule of rules) {
833
+ if (rule instanceof CSSFontFaceRule) {
834
+ return true;
835
+ }
836
+ }
837
+ }
838
+ } catch {
839
+ }
840
+ }
841
+ } catch {
842
+ }
843
+ return false;
844
+ }, []);
845
+ const googleFontUrl = fontToLoad && googleFonts.includes(fontToLoad) && !hasExistingFonts ? `https://fonts.googleapis.com/css2?family=${fontToLoad.replace(/ /g, "+")}:wght@400;500;600;700&display=swap` : null;
819
846
  const handleSubmit = async (e) => {
820
847
  e.preventDefault();
821
848
  const result = await form.submit(captchaToken || void 0);
@@ -1018,7 +1045,7 @@ function FormsExpertForm({
1018
1045
  }
1019
1046
  }
1020
1047
  ),
1021
- form.config.settings?.showFormName !== false && form.config.name && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { style: {
1048
+ form.config.settings?.showFormName !== false && form.config.name && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: {
1022
1049
  fontSize: styling.formNameFontSize != null ? `${styling.formNameFontSize}px` : "1.5rem",
1023
1050
  fontWeight: styling.formNameFontWeight === "normal" ? 400 : styling.formNameFontWeight === "medium" ? 500 : styling.formNameFontWeight === "semibold" ? 600 : 700,
1024
1051
  marginBottom: "0.5rem",
@@ -1086,6 +1113,7 @@ function FormsExpertForm({
1086
1113
  {
1087
1114
  type: "submit",
1088
1115
  disabled: form.isLoading,
1116
+ className: styling.buttonClassName,
1089
1117
  style: {
1090
1118
  ...styling.buttonFullWidth ? { width: "100%" } : styling.buttonAlign ? {} : { width: "100%" },
1091
1119
  padding: `${btnPy} ${btnPx}`,
@@ -1229,7 +1257,7 @@ function FormFieldInput({
1229
1257
  style: { width: "1rem", height: "1rem", accentColor: styling.primaryColor }
1230
1258
  }
1231
1259
  ),
1232
- (field.label || field.type === "consent" && field.consentText) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { htmlFor: field.name, children: [
1260
+ (field.label || field.type === "consent" && field.consentText) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { htmlFor: field.name, className: styling.labelClassName, children: [
1233
1261
  field.type === "consent" && field.consentText ? field.consentUrl ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
1234
1262
  field.consentText,
1235
1263
  " ",
@@ -1244,6 +1272,7 @@ function FormFieldInput({
1244
1272
  "label",
1245
1273
  {
1246
1274
  htmlFor: field.name,
1275
+ className: styling.labelClassName,
1247
1276
  style: {
1248
1277
  display: "block",
1249
1278
  fontWeight: 500,
@@ -1267,11 +1296,12 @@ function FormFieldInput({
1267
1296
  onChange,
1268
1297
  placeholder: field.placeholder,
1269
1298
  required: field.required,
1270
- style: { ...inputStyle, minHeight: "100px", resize: "vertical" }
1299
+ style: { ...inputStyle, minHeight: "100px", resize: "vertical" },
1300
+ className: styling.fieldClassName
1271
1301
  }
1272
1302
  );
1273
1303
  } else if (field.type === "select" || field.type === "dropdown") {
1274
- fieldEl = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("select", { id: field.name, name: field.name, value: String(value || ""), onChange, required: field.required, style: inputStyle, children: [
1304
+ fieldEl = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("select", { id: field.name, name: field.name, value: String(value || ""), onChange, required: field.required, style: inputStyle, className: styling.fieldClassName, children: [
1275
1305
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "", children: "Select an option..." }),
1276
1306
  getOpts().map((opt) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: opt.value, children: opt.label }, opt.value))
1277
1307
  ] });
@@ -1415,7 +1445,8 @@ function FormFieldInput({
1415
1445
  required: field.required,
1416
1446
  accept: field.allowedMimeTypes?.join(","),
1417
1447
  multiple: field.multiple,
1418
- style: inputStyle
1448
+ style: inputStyle,
1449
+ className: styling.fieldClassName
1419
1450
  }
1420
1451
  );
1421
1452
  } else if (field.type === "name") {
@@ -1428,7 +1459,8 @@ function FormFieldInput({
1428
1459
  placeholder: labels[nf] || nf,
1429
1460
  value: nameVal[nf] || "",
1430
1461
  onChange: (e) => onValueChange(field.name, { ...nameVal, [nf]: e.target.value }),
1431
- style: { ...inputStyle, flex: 1, minWidth: "120px" }
1462
+ style: { ...inputStyle, flex: 1, minWidth: "120px" },
1463
+ className: styling.fieldClassName
1432
1464
  },
1433
1465
  nf
1434
1466
  )) });
@@ -1442,7 +1474,8 @@ function FormFieldInput({
1442
1474
  placeholder: labels[af] || af,
1443
1475
  value: addr[af] || "",
1444
1476
  onChange: (e) => onValueChange(field.name, { ...addr, [af]: e.target.value }),
1445
- style: inputStyle
1477
+ style: inputStyle,
1478
+ className: styling.fieldClassName
1446
1479
  },
1447
1480
  af
1448
1481
  )) });
@@ -1461,7 +1494,8 @@ function FormFieldInput({
1461
1494
  min: field.min,
1462
1495
  max: field.max,
1463
1496
  required: field.required,
1464
- style: inputStyle
1497
+ style: inputStyle,
1498
+ className: styling.fieldClassName
1465
1499
  }
1466
1500
  )
1467
1501
  ] });
@@ -1484,16 +1518,17 @@ function FormFieldInput({
1484
1518
  value: String(value || ""),
1485
1519
  onChange: (e) => onValueChange(field.name, e.target.value),
1486
1520
  placeholder: "#000000",
1487
- style: { ...inputStyle, flex: 1 }
1521
+ style: { ...inputStyle, flex: 1 },
1522
+ className: styling.fieldClassName
1488
1523
  }
1489
1524
  )
1490
1525
  ] });
1491
1526
  } else if (field.type === "dateRange") {
1492
1527
  const range = value || {};
1493
1528
  fieldEl = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: "0.5rem", alignItems: "center" }, children: [
1494
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "date", value: range.start || "", onChange: (e) => onValueChange(field.name, { ...range, start: e.target.value }), style: { ...inputStyle, flex: 1 } }),
1529
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "date", value: range.start || "", onChange: (e) => onValueChange(field.name, { ...range, start: e.target.value }), style: { ...inputStyle, flex: 1 }, className: styling.fieldClassName }),
1495
1530
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "to" }),
1496
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "date", value: range.end || "", onChange: (e) => onValueChange(field.name, { ...range, end: e.target.value }), style: { ...inputStyle, flex: 1 } })
1531
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "date", value: range.end || "", onChange: (e) => onValueChange(field.name, { ...range, end: e.target.value }), style: { ...inputStyle, flex: 1 }, className: styling.fieldClassName })
1497
1532
  ] });
1498
1533
  } else {
1499
1534
  const typeMap = {
@@ -1520,7 +1555,8 @@ function FormFieldInput({
1520
1555
  max: field.max,
1521
1556
  step: field.step,
1522
1557
  maxLength: field.maxLength,
1523
- style: inputStyle
1558
+ style: inputStyle,
1559
+ className: styling.fieldClassName
1524
1560
  }
1525
1561
  );
1526
1562
  }