hs-uix 1.6.5 → 1.7.0

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.
@@ -0,0 +1,498 @@
1
+ import type { ReactElement, ReactNode, Ref } from "react";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Field types
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export type FormBuilderFieldType =
8
+ | "text"
9
+ | "password"
10
+ | "textarea"
11
+ | "number"
12
+ | "stepper"
13
+ | "currency"
14
+ | "date"
15
+ | "time"
16
+ | "datetime"
17
+ | "select"
18
+ | "multiselect"
19
+ | "toggle"
20
+ | "checkbox"
21
+ | "checkboxGroup"
22
+ | "radioGroup"
23
+ | "display"
24
+ | "slot"
25
+ | "repeater"
26
+ | "fieldGroup"
27
+ | "crmPropertyList"
28
+ | "crmAssociationPropertyList"
29
+ | (string & {}); // custom field types via fieldTypes plugin
30
+
31
+ export interface FormBuilderOption {
32
+ label: string;
33
+ value: string | number | boolean;
34
+ description?: string;
35
+ initialIsChecked?: boolean;
36
+ readonly?: boolean;
37
+ }
38
+
39
+ export interface FormBuilderDateValue {
40
+ year: number;
41
+ month: number;
42
+ date: number;
43
+ }
44
+
45
+ export interface FormBuilderTimeValue {
46
+ hours: number;
47
+ minutes: number;
48
+ }
49
+
50
+ export interface FormBuilderDateTimeValue {
51
+ date?: FormBuilderDateValue;
52
+ time?: FormBuilderTimeValue;
53
+ }
54
+
55
+ export interface FormBuilderValidationContext {
56
+ signal?: AbortSignal;
57
+ }
58
+
59
+ export type FormBuilderValidatorResult = true | string;
60
+
61
+ export type FormBuilderValidator = (
62
+ value: unknown,
63
+ allValues: Record<string, unknown>,
64
+ context?: FormBuilderValidationContext
65
+ ) => FormBuilderValidatorResult | Promise<FormBuilderValidatorResult>;
66
+
67
+ export interface FormBuilderDependsOnConfig {
68
+ field: string;
69
+ display?: "grouped" | "inline";
70
+ label?: string;
71
+ message?: string | ((parentLabel: string) => string);
72
+ }
73
+
74
+ export interface FormBuilderRepeaterProps {
75
+ addLabel?: string;
76
+ removeLabel?: string;
77
+ renderAdd?: (props: { onClick: () => void; count: number }) => ReactNode;
78
+ renderRemove?: (props: { index: number; onClick: () => void }) => ReactNode;
79
+ reorderable?: boolean;
80
+ moveUpLabel?: string;
81
+ moveDownLabel?: string;
82
+ renderMoveUp?: (props: { index: number; disabled: boolean; onClick: () => void }) => ReactNode;
83
+ renderMoveDown?: (props: { index: number; disabled: boolean; onClick: () => void }) => ReactNode;
84
+ }
85
+
86
+ export interface FormBuilderLabels {
87
+ submit?: string;
88
+ cancel?: string;
89
+ back?: string;
90
+ next?: string;
91
+ required?: string | ((fieldLabel: string) => string); // validation: "{label} is required"
92
+ invalidFormat?: string; // validation: "Invalid format"
93
+ minLength?: string | ((min: number) => string); // validation: "Must be at least {n} characters"
94
+ maxLength?: string | ((max: number) => string); // validation: "Must be no more than {n} characters"
95
+ minValue?: string | ((min: number) => string); // validation: "Must be at least {n}"
96
+ maxValue?: string | ((max: number) => string); // validation: "Must be no more than {n}"
97
+ dependentProperties?: string; // heading for dependent field groups (default "Dependent properties")
98
+ repeaterAdd?: string; // default add button label for repeater fields (default "Add")
99
+ repeaterRemove?: string; // default remove button label for repeater fields (default "Remove")
100
+ }
101
+
102
+ export interface FormBuilderAlertConfig {
103
+ addAlert?: (alert: { type: string; title?: string; message?: string }) => void;
104
+ readOnlyTitle?: string;
105
+ errorTitle?: string;
106
+ successTitle?: string;
107
+ }
108
+
109
+ export interface FormBuilderButtonsRenderContext {
110
+ isMultiStep: boolean;
111
+ isFirstStep: boolean;
112
+ isLastStep: boolean;
113
+ currentStep: number;
114
+ totalSteps: number;
115
+ disabled: boolean;
116
+ loading: boolean;
117
+ labels: Required<Pick<FormBuilderLabels, "submit" | "cancel" | "back" | "next">>;
118
+ onBack: () => void;
119
+ onNext: () => void;
120
+ onCancel?: () => void;
121
+ onSubmit: (e?: unknown) => Promise<void>;
122
+ }
123
+
124
+ export type FormBuilderSubmitAlign = "start" | "end" | "between";
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Layout types
128
+ // ---------------------------------------------------------------------------
129
+
130
+ export type FormBuilderLayoutEntry = string | { field: string; flex?: number };
131
+ export type FormBuilderLayout = FormBuilderLayoutEntry[][];
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Field definition
135
+ // ---------------------------------------------------------------------------
136
+
137
+ export interface FormBuilderField {
138
+ name: string;
139
+ type: FormBuilderFieldType;
140
+ label: string;
141
+
142
+ // Common optional props (passed to most HubSpot components)
143
+ description?: string;
144
+ placeholder?: string;
145
+ tooltip?: string;
146
+ required?: boolean | ((values: Record<string, unknown>) => boolean);
147
+ readOnly?: boolean;
148
+ /** Escape hatch: when true, this field stays editable even if FormBuilder-level `readOnly` is set. */
149
+ alwaysEditable?: boolean;
150
+ disabled?: boolean | ((values: Record<string, unknown>) => boolean);
151
+ defaultValue?: unknown;
152
+
153
+ // Validation (validate may return a Promise for async validation)
154
+ validate?: FormBuilderValidator;
155
+ validators?: FormBuilderValidator[];
156
+ useDefaultValidators?: boolean;
157
+ validateDebounce?: number;
158
+ pattern?: RegExp;
159
+ patternMessage?: string;
160
+ minLength?: number;
161
+ maxLength?: number;
162
+ min?: number | FormBuilderDateValue | FormBuilderTimeValue;
163
+ max?: number | FormBuilderDateValue | FormBuilderTimeValue;
164
+ minValidationMessage?: string;
165
+ maxValidationMessage?: string;
166
+
167
+ // Field-level loading indicator
168
+ loading?: boolean;
169
+
170
+ // Conditional visibility
171
+ visible?: (values: Record<string, unknown>) => boolean;
172
+
173
+ // Dependent properties grouping
174
+ dependsOnConfig?: FormBuilderDependsOnConfig;
175
+
176
+ // Layout
177
+ colSpan?: number;
178
+ width?: "full";
179
+
180
+ // Field grouping (non-collapsible divider groups)
181
+ group?: string;
182
+
183
+ // Value transforms (storage ↔ display)
184
+ transformIn?: (rawValue: unknown) => unknown; // storage → display (on load)
185
+ transformOut?: (displayValue: unknown) => unknown; // display → storage (on save)
186
+
187
+ // Debounce onChange callback (ms) — useful for search-as-you-type fields
188
+ debounce?: number;
189
+
190
+ // Pass-through to underlying HubSpot component
191
+ fieldProps?: Record<string, unknown>;
192
+
193
+ // CrmPropertyList props (type: "crmPropertyList")
194
+ properties?: string[];
195
+ direction?: "column" | "row";
196
+ objectId?: string;
197
+ objectTypeId?: string;
198
+
199
+ // CrmAssociationPropertyList props (type: "crmAssociationPropertyList")
200
+ associationLabels?: string[];
201
+ filters?: Array<{ operator: string; property: string; value: string }>;
202
+ sort?: Array<{ columnName: string; direction: 1 | -1 }>;
203
+
204
+ // Select / MultiSelect / ToggleGroup
205
+ options?:
206
+ | FormBuilderOption[]
207
+ | ((values: Record<string, unknown>) => FormBuilderOption[]);
208
+
209
+ // Select / Checkbox / ToggleGroup
210
+ variant?: "input" | "transparent" | "default" | "small" | "sm";
211
+
212
+ // Currency
213
+ currency?: string;
214
+
215
+ // TextArea
216
+ rows?: number;
217
+ cols?: number;
218
+ resize?: "vertical" | "horizontal" | "both" | "none";
219
+
220
+ // Number / Stepper / Currency
221
+ precision?: number;
222
+ formatStyle?: "decimal" | "percentage";
223
+
224
+ // Stepper
225
+ stepSize?: number;
226
+ minValueReachedTooltip?: string;
227
+ maxValueReachedTooltip?: string;
228
+
229
+ // Toggle
230
+ size?: "xs" | "sm" | "md";
231
+ labelDisplay?: "inline" | "top" | "hidden";
232
+ textChecked?: string;
233
+ textUnchecked?: string;
234
+
235
+ // Checkbox
236
+ inline?: boolean;
237
+
238
+ // Date / Time
239
+ format?: "short" | "long" | "medium" | "standard" | "YYYY-MM-DD" | "L" | "LL" | "ll";
240
+ timezone?: "userTz" | "portalTz";
241
+ clearButtonLabel?: string;
242
+ todayButtonLabel?: string;
243
+
244
+ // Time
245
+ interval?: number;
246
+
247
+ // Field-level side effects (cross-field updates)
248
+ onFieldChange?: (
249
+ value: unknown,
250
+ allValues: Record<string, unknown>,
251
+ helpers: {
252
+ setFieldValue: (name: string, value: unknown) => void;
253
+ setFieldError: (name: string, message: string) => void;
254
+ }
255
+ ) => void;
256
+
257
+ // Repeater field / fieldGroup field factory
258
+ fields?: FormBuilderField[] | ((item: { key: string; label?: string; [k: string]: unknown }) => FormBuilderField[]);
259
+ repeaterProps?: FormBuilderRepeaterProps;
260
+
261
+ // fieldGroup props (type: "fieldGroup") — fixed structured groups
262
+ items?: Array<{ key: string; label?: string; [k: string]: unknown }>;
263
+ showItemLabel?: boolean;
264
+
265
+ // Custom render escape hatch
266
+ render?: (props: {
267
+ value?: unknown;
268
+ onChange?: (v: unknown) => void;
269
+ error?: boolean;
270
+ values: Record<string, unknown>;
271
+ /** @deprecated Use `values` instead */
272
+ allValues: Record<string, unknown>;
273
+ setFieldValue?: (name: string, value: unknown) => void;
274
+ setFieldError?: (name: string, message: string) => void;
275
+ }) => ReactNode;
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Custom field type plugin
280
+ // ---------------------------------------------------------------------------
281
+
282
+ export interface FieldTypePlugin {
283
+ render: (props: {
284
+ value: unknown;
285
+ onChange: (v: unknown) => void;
286
+ error: boolean;
287
+ field: FormBuilderField;
288
+ values: Record<string, unknown>;
289
+ /** @deprecated Use `values` instead */
290
+ allValues: Record<string, unknown>;
291
+ }) => ReactNode;
292
+ getEmptyValue?: () => unknown;
293
+ isEmpty?: (value: unknown) => boolean;
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // Section definition (accordion grouping)
298
+ // ---------------------------------------------------------------------------
299
+
300
+ export interface FormBuilderSectionContext {
301
+ values: Record<string, unknown>;
302
+ errors: Record<string, string>;
303
+ }
304
+
305
+ export interface FormBuilderSection {
306
+ id: string;
307
+ label: string;
308
+ fields: string[];
309
+ defaultOpen?: boolean;
310
+ info?: string;
311
+ columns?: number;
312
+ renderBefore?: (context: FormBuilderSectionContext) => ReactNode;
313
+ renderAfter?: (context: FormBuilderSectionContext) => ReactNode;
314
+ }
315
+
316
+ export interface FormBuilderGroupOptions {
317
+ /** Override the displayed group label (defaults to a start-cased group key). */
318
+ label?: string;
319
+ /** Show the group header label. Defaults to true. */
320
+ showLabel?: boolean;
321
+ /** Optional microcopy rendered underneath the group label. Ignored when `renderHeader` is provided or `showLabel` is false. */
322
+ description?: string;
323
+ /** Show the divider that separates this group from the previous one. Defaults to true. */
324
+ showDivider?: boolean;
325
+ /** Custom header renderer. Receives group name, fields in the group, and current form values. */
326
+ renderHeader?: (
327
+ group: string,
328
+ fields: FormBuilderField[],
329
+ values: Record<string, unknown>
330
+ ) => ReactNode;
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Multi-step definition
335
+ // ---------------------------------------------------------------------------
336
+
337
+ export interface FormBuilderStep {
338
+ title: string;
339
+ fields?: string[];
340
+ description?: string;
341
+ render?: (props: {
342
+ values: Record<string, unknown>;
343
+ goNext: () => void;
344
+ goBack: () => void;
345
+ goTo: (step: number) => void;
346
+ }) => ReactNode;
347
+ validate?: (values: Record<string, unknown>) => true | Record<string, string>;
348
+ }
349
+
350
+ // ---------------------------------------------------------------------------
351
+ // Ref API
352
+ // ---------------------------------------------------------------------------
353
+
354
+ export interface FormBuilderRef {
355
+ submit: () => Promise<void>;
356
+ validate: () => { valid: boolean; errors: Record<string, string> };
357
+ reset: () => void;
358
+ getValues: () => Record<string, unknown>;
359
+ isDirty: () => boolean;
360
+ setFieldValue: (name: string, value: unknown) => void;
361
+ setFieldError: (name: string, message: string) => void;
362
+ setErrors: (errors: Record<string, string>) => void;
363
+ }
364
+
365
+ // ---------------------------------------------------------------------------
366
+ // Component props
367
+ // ---------------------------------------------------------------------------
368
+
369
+ export interface FormBuilderProps {
370
+ // Core
371
+ fields: FormBuilderField[];
372
+ onSubmit: (
373
+ values: Record<string, unknown>,
374
+ helpers: { reset: () => void; rawValues: Record<string, unknown> }
375
+ ) => void | Promise<unknown>;
376
+
377
+ // Value transforms
378
+ transformValues?: (values: Record<string, unknown>) => Record<string, unknown>;
379
+ transformInitialValues?: (rawValues: Record<string, unknown>) => Record<string, unknown>;
380
+ onBeforeSubmit?: (values: Record<string, unknown>) => boolean | Promise<boolean>;
381
+ onSubmitSuccess?: (
382
+ result: unknown,
383
+ helpers: { reset: () => void; values: Record<string, unknown> }
384
+ ) => void;
385
+ onSubmitError?: (
386
+ error: unknown,
387
+ helpers: { values: Record<string, unknown> }
388
+ ) => void;
389
+ resetOnSuccess?: boolean;
390
+
391
+ // Initial / controlled values
392
+ initialValues?: Record<string, unknown>;
393
+ values?: Record<string, unknown>;
394
+ onChange?: (values: Record<string, unknown>) => void;
395
+ errors?: Record<string, string>;
396
+ onFieldChange?: (
397
+ name: string,
398
+ value: unknown,
399
+ allValues: Record<string, unknown>
400
+ ) => void;
401
+
402
+ // Validation
403
+ validateOnChange?: boolean;
404
+ validateOnBlur?: boolean;
405
+ validateOnSubmit?: boolean;
406
+ onValidationChange?: (errors: Record<string, string>) => void;
407
+ /** Called when submit-time validation blocks submission. Lets callers surface their own toast/alert and inspect which field/section is invalid. */
408
+ onValidationFail?: (info: {
409
+ errors: Record<string, string>;
410
+ fields: { name: string; label?: string; sectionId?: string }[];
411
+ firstInvalidField?: { name: string; label?: string; sectionId?: string };
412
+ }) => void;
413
+ /** When true, FormBuilder auto-opens the accordion section containing the first invalid field on submit-time validation failure. */
414
+ openSectionOnValidationFail?: boolean;
415
+
416
+ // Multi-step
417
+ steps?: FormBuilderStep[];
418
+ step?: number;
419
+ onStepChange?: (step: number) => void;
420
+ showStepIndicator?: boolean;
421
+ validateStepOnNext?: boolean;
422
+
423
+ // Buttons / actions
424
+ submitVariant?: "primary" | "secondary";
425
+ showCancel?: boolean;
426
+ onCancel?: () => void;
427
+ submitPosition?: "bottom" | "none";
428
+ /**
429
+ * Controls the default single-step action-row alignment.
430
+ * Defaults to `"between"` when `showCancel` is true, otherwise `"start"`.
431
+ * Ignored when `steps` are provided; use `renderButtons` for custom multi-step layouts.
432
+ */
433
+ submitAlign?: FormBuilderSubmitAlign;
434
+ loading?: boolean;
435
+ disabled?: boolean;
436
+ labels?: FormBuilderLabels;
437
+ renderButtons?: (context: FormBuilderButtonsRenderContext) => ReactNode;
438
+
439
+ // Appearance / layout
440
+ columns?: number;
441
+ columnWidth?: number;
442
+ maxColumns?: number;
443
+ layout?: FormBuilderLayout;
444
+ sections?: FormBuilderSection[];
445
+ groups?: Record<string, FormBuilderGroupOptions>;
446
+ /**
447
+ * Gap between fields (and between rows in grid layouts).
448
+ * HubSpot spacing token: `"flush" | "extra-small" | "small" | "medium" | "large" | "extra-large"`
449
+ * or shorthand: `"xs" | "sm" | "md" | "lg" | "xl"`. Defaults to `"sm"`.
450
+ */
451
+ gap?: string;
452
+ showRequiredIndicator?: boolean;
453
+ noFormWrapper?: boolean;
454
+ autoComplete?: string;
455
+ formProps?: Record<string, unknown>;
456
+ fieldTypes?: Record<string, FieldTypePlugin>;
457
+
458
+ // States
459
+ error?: string | boolean;
460
+ success?: string;
461
+ readOnly?: boolean;
462
+ readOnlyMessage?: string;
463
+ alerts?: FormBuilderAlertConfig;
464
+ showReadOnlyAlert?: boolean;
465
+ showInlineAlerts?: boolean;
466
+ renderReadOnlyAlert?: (context: { title: string; message: string }) => ReactNode; // custom readOnly alert renderer
467
+ renderFieldError?: (error: string, field: FormBuilderField) => ReactNode; // custom field error renderer
468
+ defaultCurrency?: string; // form-level default ISO 4217 currency code (default "USD")
469
+
470
+ // Auto-save
471
+ autoSave?: {
472
+ debounce?: number;
473
+ onAutoSave: (values: Record<string, unknown>) => void;
474
+ };
475
+
476
+ // Events
477
+ onDirtyChange?: (isDirty: boolean) => void;
478
+
479
+ // Ref
480
+ ref?: Ref<FormBuilderRef>;
481
+ }
482
+
483
+ export declare function FormBuilder(props: FormBuilderProps): ReactElement | null;
484
+
485
+ // ---------------------------------------------------------------------------
486
+ // CRM Integration utilities
487
+ // ---------------------------------------------------------------------------
488
+
489
+ /**
490
+ * Maps CRM property values (from useCrmProperties) to form initial values.
491
+ * `properties` is the flat { propertyName: value } object.
492
+ * Without `mapping`: direct pass-through (field names === CRM property names).
493
+ * With `mapping`: maps { formFieldName: "crmPropertyName" }.
494
+ */
495
+ export declare function useFormPrefill(
496
+ properties: Record<string, unknown> | undefined,
497
+ mapping?: Record<string, string>
498
+ ): Record<string, unknown>;