@lub-crm/forms 1.0.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.
Files changed (52) hide show
  1. package/README.md +298 -0
  2. package/dist/lub-forms.css +1 -0
  3. package/dist/lub-forms.es.js +5848 -0
  4. package/dist/lub-forms.es.js.map +1 -0
  5. package/dist/lub-forms.standalone.js +10 -0
  6. package/dist/lub-forms.standalone.js.map +1 -0
  7. package/dist/lub-forms.umd.js +227 -0
  8. package/dist/lub-forms.umd.js.map +1 -0
  9. package/package.json +68 -0
  10. package/src/api/client.ts +115 -0
  11. package/src/api/index.ts +2 -0
  12. package/src/api/types.ts +202 -0
  13. package/src/core/FormProvider.tsx +228 -0
  14. package/src/core/FormRenderer.tsx +134 -0
  15. package/src/core/LubForm.tsx +476 -0
  16. package/src/core/StepManager.tsx +199 -0
  17. package/src/core/index.ts +4 -0
  18. package/src/embed.ts +188 -0
  19. package/src/fields/CheckboxField.tsx +62 -0
  20. package/src/fields/CheckboxGroupField.tsx +57 -0
  21. package/src/fields/CountryField.tsx +43 -0
  22. package/src/fields/DateField.tsx +33 -0
  23. package/src/fields/DateTimeField.tsx +33 -0
  24. package/src/fields/DividerField.tsx +16 -0
  25. package/src/fields/FieldWrapper.tsx +60 -0
  26. package/src/fields/FileField.tsx +45 -0
  27. package/src/fields/HiddenField.tsx +18 -0
  28. package/src/fields/HtmlField.tsx +17 -0
  29. package/src/fields/NumberField.tsx +39 -0
  30. package/src/fields/RadioField.tsx +57 -0
  31. package/src/fields/RecaptchaField.tsx +137 -0
  32. package/src/fields/SelectField.tsx +49 -0
  33. package/src/fields/StateField.tsx +84 -0
  34. package/src/fields/TextField.tsx +51 -0
  35. package/src/fields/TextareaField.tsx +37 -0
  36. package/src/fields/TimeField.tsx +33 -0
  37. package/src/fields/index.ts +84 -0
  38. package/src/hooks/index.ts +4 -0
  39. package/src/hooks/useConditionalLogic.ts +59 -0
  40. package/src/hooks/useFormApi.ts +118 -0
  41. package/src/hooks/useFormDesign.ts +48 -0
  42. package/src/hooks/useMultiStep.ts +98 -0
  43. package/src/index.ts +101 -0
  44. package/src/main.tsx +40 -0
  45. package/src/styles/index.css +707 -0
  46. package/src/utils/cn.ts +6 -0
  47. package/src/utils/countries.ts +163 -0
  48. package/src/utils/css-variables.ts +63 -0
  49. package/src/utils/index.ts +3 -0
  50. package/src/validation/conditional.ts +170 -0
  51. package/src/validation/index.ts +2 -0
  52. package/src/validation/schema-builder.ts +327 -0
package/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # @lub/forms
2
+
3
+ Embeddable form library for Lub CRM. Render dynamic forms that integrate with Lub CRM for lead capture and data collection.
4
+
5
+ ## Features
6
+
7
+ - Multiple deployment options: React component, UMD script, or standalone bundle
8
+ - Multi-step form support with progress indicators
9
+ - Conditional field visibility and requirements
10
+ - Dynamic validation with Zod schemas
11
+ - CRM field mapping for automatic lead creation
12
+ - UTM parameter tracking
13
+ - Customizable styling via CSS variables
14
+ - 17+ field types including country/state selectors
15
+
16
+ ## Installation
17
+
18
+ ### NPM Package (React Apps)
19
+
20
+ ```bash
21
+ npm install @lub/forms
22
+ # or
23
+ bun add @lub/forms
24
+ ```
25
+
26
+ ### Script Tag (Standalone)
27
+
28
+ No build step required. Includes React and all dependencies:
29
+
30
+ ```html
31
+ <script src="https://forms.lub.com/v1/lub-forms.standalone.js"></script>
32
+ ```
33
+
34
+ ### Script Tag (UMD)
35
+
36
+ For pages that already have React loaded:
37
+
38
+ ```html
39
+ <script src="https://unpkg.com/react@19/umd/react.production.min.js"></script>
40
+ <script src="https://unpkg.com/react-dom@19/umd/react-dom.production.min.js"></script>
41
+ <script src="https://forms.lub.com/v1/lub-forms.umd.js"></script>
42
+ <link rel="stylesheet" href="https://forms.lub.com/v1/lub-forms.css" />
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ### React Component
48
+
49
+ ```tsx
50
+ import { LubForm } from "@lub/forms";
51
+ import "@lub/forms/styles";
52
+
53
+ function ContactPage() {
54
+ return (
55
+ <LubForm
56
+ formId="abc123"
57
+ baseUrl="https://api.lub.com"
58
+ onSuccess={(data) => {
59
+ console.log("Submission ID:", data.submission_id);
60
+ window.location.href = "/thank-you";
61
+ }}
62
+ onError={(error) => {
63
+ console.error("Submission failed:", error.message);
64
+ }}
65
+ />
66
+ );
67
+ }
68
+ ```
69
+
70
+ ### Script Tag (Auto-mount)
71
+
72
+ Forms automatically mount to elements with `data-lub-form-id`:
73
+
74
+ ```html
75
+ <div
76
+ data-lub-form-id="abc123"
77
+ data-lub-base-url="https://api.lub.com"
78
+ data-lub-class="my-custom-class"
79
+ data-lub-on-success="handleSuccess"
80
+ data-lub-on-error="handleError"
81
+ ></div>
82
+
83
+ <script>
84
+ function handleSuccess(data) {
85
+ window.location.href = "/thank-you";
86
+ }
87
+ function handleError(error) {
88
+ alert("Something went wrong");
89
+ }
90
+ </script>
91
+ ```
92
+
93
+ ### Script Tag (Manual Render)
94
+
95
+ ```html
96
+ <div id="my-form"></div>
97
+
98
+ <script>
99
+ LubForms.render("abc123", "my-form", {
100
+ baseUrl: "https://api.lub.com",
101
+ onSuccess: (data) => {
102
+ console.log("Success!", data);
103
+ },
104
+ onError: (err) => {
105
+ console.error("Error:", err);
106
+ },
107
+ designOverrides: {
108
+ primary_color: "#8b5cf6",
109
+ },
110
+ });
111
+ </script>
112
+ ```
113
+
114
+ ## Props / Options
115
+
116
+ | Prop | Type | Description |
117
+ | ------------------- | ------------------------------------------ | ------------------------------------------- |
118
+ | `formId` | `string` | Form ID from Lub CRM (required) |
119
+ | `baseUrl` | `string` | API base URL |
120
+ | `onSuccess` | `(data: SubmitFormResponse) => void` | Called on successful submission |
121
+ | `onError` | `(error: ApiError) => void` | Called on submission error |
122
+ | `onValidationError` | `(errors: Record<string, string>) => void` | Called when validation fails |
123
+ | `onStepChange` | `(step: number, total: number) => void` | Called when multi-step form changes step |
124
+ | `className` | `string` | Additional CSS class for the form container |
125
+ | `style` | `CSSProperties` | Inline styles |
126
+ | `designOverrides` | `Partial<FormDesign>` | Override form design settings |
127
+
128
+ ## Field Types
129
+
130
+ | Type | Description |
131
+ | ---------------- | --------------------------- |
132
+ | `text` | Single-line text input |
133
+ | `textarea` | Multi-line text input |
134
+ | `email` | Email input with validation |
135
+ | `phone` | Phone number input |
136
+ | `number` | Numeric input with min/max |
137
+ | `url` | URL input with validation |
138
+ | `date` | Date picker |
139
+ | `time` | Time picker |
140
+ | `datetime` | Date and time picker |
141
+ | `select` | Dropdown select |
142
+ | `radio` | Radio button group |
143
+ | `checkbox` | Single checkbox |
144
+ | `checkbox_group` | Multiple checkboxes |
145
+ | `file` | File upload |
146
+ | `hidden` | Hidden field |
147
+ | `country` | Country selector |
148
+ | `state` | State/province selector |
149
+ | `html` | Static HTML content |
150
+ | `divider` | Visual divider |
151
+ | `recaptcha` | reCAPTCHA v2/v3 |
152
+
153
+ ## Styling
154
+
155
+ Forms are styled using CSS variables. Override them via `designOverrides` or CSS:
156
+
157
+ ```css
158
+ .lub-form {
159
+ --lub-primary-color: #3b82f6;
160
+ --lub-background-color: #ffffff;
161
+ --lub-text-color: #1f2937;
162
+ --lub-border-color: #e5e7eb;
163
+ --lub-font-family: system-ui, sans-serif;
164
+ --lub-font-size: 14px;
165
+ --lub-padding: 24px;
166
+ --lub-field-spacing: 16px;
167
+ }
168
+ ```
169
+
170
+ Or via JavaScript:
171
+
172
+ ```jsx
173
+ <LubForm
174
+ formId="abc123"
175
+ designOverrides={{
176
+ primary_color: "#8b5cf6",
177
+ background_color: "#fafafa",
178
+ font_family: "Inter, sans-serif",
179
+ button_style: {
180
+ border_radius: "9999px",
181
+ full_width: true,
182
+ },
183
+ }}
184
+ />
185
+ ```
186
+
187
+ ## Conditional Logic
188
+
189
+ Fields support conditional visibility and requirements based on other field values:
190
+
191
+ ```typescript
192
+ // Show field when another field equals a value
193
+ conditional_logic: {
194
+ show_if: {
195
+ field_name: 'interest',
196
+ operator: 'equals',
197
+ value: 'other'
198
+ }
199
+ }
200
+
201
+ // Make field required based on condition
202
+ conditional_logic: {
203
+ required_if: {
204
+ field_name: 'newsletter',
205
+ operator: 'equals',
206
+ value: true
207
+ }
208
+ }
209
+ ```
210
+
211
+ **Operators:** `equals`, `not_equals`, `contains`, `not_contains`, `greater_than`, `less_than`, `is_empty`, `is_not_empty`
212
+
213
+ ## Custom Field Components
214
+
215
+ Register custom field types for advanced use cases:
216
+
217
+ ```tsx
218
+ import { registerFieldComponent } from "@lub/forms";
219
+
220
+ registerFieldComponent("rating", ({ field, register, error }) => (
221
+ <div className="rating-field">
222
+ {[1, 2, 3, 4, 5].map((value) => (
223
+ <label key={value}>
224
+ <input type="radio" value={value} {...register(field.name)} />
225
+ {"⭐".repeat(value)}
226
+ </label>
227
+ ))}
228
+ {error && <span className="error">{error}</span>}
229
+ </div>
230
+ ));
231
+ ```
232
+
233
+ ## API Client
234
+
235
+ Use the client directly for custom integrations:
236
+
237
+ ```tsx
238
+ import { LubFormsClient } from "@lub/forms";
239
+
240
+ const client = new LubFormsClient("https://api.lub.com");
241
+
242
+ // Fetch form definition
243
+ const form = await client.getForm("abc123");
244
+
245
+ // Submit form data
246
+ const response = await client.submitForm("abc123", {
247
+ data: { email: "user@example.com", name: "John" },
248
+ utm_parameters: { source: "landing-page" },
249
+ referrer: window.location.href,
250
+ });
251
+
252
+ // Confirm double opt-in
253
+ const confirmation = await client.confirmOptIn("token123");
254
+ ```
255
+
256
+ ## Global API (Script Tag)
257
+
258
+ When using the standalone or UMD build:
259
+
260
+ ```javascript
261
+ // Render a form
262
+ LubForms.render(formId, containerIdOrElement, options);
263
+
264
+ // Auto-mount all forms with data-lub-form-id
265
+ LubForms.autoMount();
266
+
267
+ // Unmount a specific form
268
+ LubForms.unmount(containerId);
269
+
270
+ // Unmount all forms
271
+ LubForms.unmountAll();
272
+
273
+ // Initialize (called automatically)
274
+ LubForms.init();
275
+ ```
276
+
277
+ ## Development
278
+
279
+ ```bash
280
+ bun install
281
+ bun run dev # Start dev server on :5174
282
+ bun run build # Build all formats
283
+ bun run lint # Run ESLint
284
+ bun run typecheck # TypeScript check
285
+ ```
286
+
287
+ ## Build Outputs
288
+
289
+ | File | Format | Use Case |
290
+ | ------------------------------ | ------ | ------------------------------ |
291
+ | `dist/lub-forms.es.js` | ESM | NPM package for React apps |
292
+ | `dist/lub-forms.umd.js` | UMD | Script tag with React on page |
293
+ | `dist/lub-forms.standalone.js` | IIFE | Drop-in script, bundles React |
294
+ | `dist/lub-forms.css` | CSS | Shared styles (UMD/ESM builds) |
295
+
296
+ ## License
297
+
298
+ MIT
@@ -0,0 +1 @@
1
+ .lub-form{--lub-bg-color: #ffffff;--lub-text-color: #1f2937;--lub-primary-color: #3b82f6;--lub-primary-hover: #2563eb;--lub-border-color: #d1d5db;--lub-error-color: #ef4444;--lub-success-color: #10b981;--lub-muted-color: #6b7280;--lub-font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--lub-font-size: 16px;--lub-padding: 24px;--lub-field-spacing: 16px;--lub-border-radius: 6px;--lub-input-height: 42px;--lub-btn-bg: var(--lub-primary-color);--lub-btn-text: #ffffff;--lub-btn-radius: var(--lub-border-radius);--lub-focus-ring: 0 0 0 3px rgba(59, 130, 246, .3);--lub-transition: .15s ease}.lub-form{font-family:var(--lub-font-family);font-size:var(--lub-font-size);color:var(--lub-text-color);background-color:var(--lub-bg-color);padding:var(--lub-padding);border-radius:var(--lub-border-radius);box-sizing:border-box;line-height:1.5}.lub-form *,.lub-form *:before,.lub-form *:after{box-sizing:border-box}.lub-form__form{display:flex;flex-direction:column;gap:var(--lub-field-spacing)}.lub-form__header{margin-bottom:var(--lub-field-spacing)}.lub-form__title{font-size:1.5em;font-weight:600;margin:0 0 .5em;color:var(--lub-text-color)}.lub-form__description{margin:0;color:var(--lub-muted-color)}.lub-form__fields{display:flex;flex-direction:column;gap:var(--lub-field-spacing)}.lub-form__fields--two_column{display:flex;flex-wrap:wrap;gap:var(--lub-field-spacing)}.lub-form__field-group{display:contents}.lub-form__field-group--row{display:flex;flex-wrap:wrap;gap:var(--lub-field-spacing);width:100%}.lub-form__field,.lub-form__field--full{width:100%}.lub-form__field--half{width:calc(50% - var(--lub-field-spacing) / 2)}.lub-form__field--third{width:calc(33.333% - var(--lub-field-spacing) * 2 / 3)}.lub-form__field--quarter{width:calc(25% - var(--lub-field-spacing) * 3 / 4)}@media(max-width:640px){.lub-form__field--half,.lub-form__field--third,.lub-form__field--quarter{width:100%}}.lub-form__field-wrapper{display:flex;flex-direction:column;gap:6px}.lub-form__label{display:flex;align-items:center;gap:4px;font-size:.875em;font-weight:500;color:var(--lub-text-color)}.lub-form__required-indicator{color:var(--lub-error-color);font-weight:400}.lub-form__input,.lub-form__textarea,.lub-form__select{width:100%;padding:10px 12px;font-family:inherit;font-size:1em;color:var(--lub-text-color);background-color:var(--lub-bg-color);border:1px solid var(--lub-border-color);border-radius:var(--lub-border-radius);transition:border-color var(--lub-transition),box-shadow var(--lub-transition);outline:none}.lub-form__input{height:var(--lub-input-height)}.lub-form__textarea{min-height:100px;resize:vertical}.lub-form__select{height:var(--lub-input-height);cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='%236b7280'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' clip-rule='evenodd'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:36px}.lub-form__input::placeholder,.lub-form__textarea::placeholder{color:var(--lub-muted-color)}.lub-form__input:focus,.lub-form__textarea:focus,.lub-form__select:focus{border-color:var(--lub-primary-color);box-shadow:var(--lub-focus-ring)}.lub-form__input--error,.lub-form__textarea--error,.lub-form__select--error{border-color:var(--lub-error-color)}.lub-form__input--error:focus,.lub-form__textarea--error:focus,.lub-form__select--error:focus{border-color:var(--lub-error-color);box-shadow:0 0 0 3px #ef44444d}.lub-form__checkbox-group,.lub-form__radio-group{display:flex;flex-direction:column;gap:8px}.lub-form__checkbox-label,.lub-form__radio-label{display:flex;align-items:flex-start;gap:10px;cursor:pointer;font-size:.9375em}.lub-form__checkbox,.lub-form__radio{width:18px;height:18px;margin:2px 0 0;flex-shrink:0;cursor:pointer;accent-color:var(--lub-primary-color)}.lub-form__checkbox-text,.lub-form__radio-text{color:var(--lub-text-color)}.lub-form__file-input{width:100%;padding:10px 12px;font-family:inherit;font-size:.9375em;color:var(--lub-text-color);background-color:var(--lub-bg-color);border:1px dashed var(--lub-border-color);border-radius:var(--lub-border-radius);cursor:pointer;transition:border-color var(--lub-transition),background-color var(--lub-transition)}.lub-form__file-input:hover{background-color:#00000005}.lub-form__file-input:focus{border-color:var(--lub-primary-color);outline:none}.lub-form__file-hint{margin:4px 0 0;font-size:.8125em;color:var(--lub-muted-color)}.lub-form__help-text{margin:0;font-size:.8125em;color:var(--lub-muted-color)}.lub-form__error{margin:0;font-size:.8125em;color:var(--lub-error-color)}.lub-form__html-content{padding:8px 0;color:var(--lub-text-color)}.lub-form__html-content a{color:var(--lub-primary-color);text-decoration:underline}.lub-form__divider{display:flex;align-items:center;gap:12px;padding:8px 0}.lub-form__divider-line{flex:1;height:1px;background-color:var(--lub-border-color);border:none;margin:0}.lub-form__divider-label{font-size:.875em;color:var(--lub-muted-color);white-space:nowrap}.lub-form__consent{padding-top:var(--lub-field-spacing)}.lub-form__consent-label{display:flex;align-items:flex-start;gap:10px;cursor:pointer}.lub-form__consent-checkbox{width:18px;height:18px;margin-top:2px;flex-shrink:0;cursor:pointer;accent-color:var(--lub-primary-color)}.lub-form__consent-text{font-size:.875em;color:var(--lub-text-color)}.lub-form__consent-text a{color:var(--lub-primary-color);text-decoration:underline}.lub-form__actions{display:flex;justify-content:flex-end;padding-top:var(--lub-field-spacing)}.lub-form__button{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:12px 24px;font-family:inherit;font-size:1em;font-weight:500;border:none;border-radius:var(--lub-btn-radius);cursor:pointer;transition:background-color var(--lub-transition),opacity var(--lub-transition)}.lub-form__button--primary{color:var(--lub-btn-text);background-color:var(--lub-btn-bg)}.lub-form__button--primary:hover:not(:disabled){background-color:var(--lub-primary-hover)}.lub-form__button--secondary{color:var(--lub-text-color);background-color:transparent;border:1px solid var(--lub-border-color)}.lub-form__button--secondary:hover:not(:disabled){background-color:#0000000d}.lub-form__button:disabled{opacity:.6;cursor:not-allowed}.lub-form__button-loading{display:inline-flex;align-items:center;gap:8px}.lub-form__navigation{display:flex;align-items:center;gap:12px;padding-top:var(--lub-field-spacing)}.lub-form__navigation-spacer{flex:1}.lub-form__steps{padding-bottom:var(--lub-field-spacing)}.lub-form__steps-list{display:flex;align-items:center;list-style:none;margin:0;padding:0;gap:0}.lub-form__step{display:flex;align-items:center;flex:1}.lub-form__step:last-child{flex:0 0 auto}.lub-form__step-indicator,.lub-form__step-button{display:flex;align-items:center;gap:8px;padding:8px;background:none;border:none;font-family:inherit;font-size:.875em;color:var(--lub-muted-color)}.lub-form__step-button{cursor:pointer}.lub-form__step-button:hover{color:var(--lub-primary-color)}.lub-form__step-number{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background-color:var(--lub-border-color);color:var(--lub-muted-color);font-weight:500;font-size:.8125em}.lub-form__step--active .lub-form__step-number{background-color:var(--lub-primary-color);color:#fff}.lub-form__step--completed .lub-form__step-number{background-color:var(--lub-success-color);color:#fff}.lub-form__step-name{display:none}@media(min-width:640px){.lub-form__step-name{display:inline}}.lub-form__step--active .lub-form__step-indicator,.lub-form__step--active .lub-form__step-button{color:var(--lub-text-color)}.lub-form__step-connector{flex:1;height:2px;background-color:var(--lub-border-color);margin:0 8px}.lub-form__step-connector--completed{background-color:var(--lub-success-color)}.lub-form__check-icon{width:16px;height:16px}.lub-form__step-description{margin:12px 0 0;font-size:.875em;color:var(--lub-muted-color)}.lub-form__spinner{width:18px;height:18px;animation:lub-spin 1s linear infinite}.lub-form__spinner-track{opacity:.25}.lub-form__spinner-head{opacity:1}@keyframes lub-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.lub-form--loading .lub-form__skeleton{display:flex;flex-direction:column;gap:var(--lub-field-spacing)}.lub-form__skeleton-title,.lub-form__skeleton-field,.lub-form__skeleton-button{background:linear-gradient(90deg,var(--lub-border-color) 25%,rgba(0,0,0,.05) 50%,var(--lub-border-color) 75%);background-size:200% 100%;animation:lub-shimmer 1.5s infinite;border-radius:var(--lub-border-radius)}.lub-form__skeleton-title{height:28px;width:60%;margin-bottom:8px}.lub-form__skeleton-field{height:var(--lub-input-height)}.lub-form__skeleton-field--short{width:50%}.lub-form__skeleton-button{height:46px;width:120px;margin-left:auto}@keyframes lub-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.lub-form--success .lub-form__success{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:var(--lub-padding);min-height:200px}.lub-form__success-icon{width:64px;height:64px;color:var(--lub-success-color);margin-bottom:16px}.lub-form__success-message{font-size:1.125em;color:var(--lub-text-color);margin:0}.lub-form--error .lub-form__error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:var(--lub-padding);min-height:150px}.lub-form__error-message{font-size:1em;color:var(--lub-error-color);margin:0}.lub-form__recaptcha{display:flex;justify-content:flex-start;padding:8px 0}.lub-form__recaptcha-widget{transform-origin:left top}.lub-form[data-theme=dark]{--lub-bg-color: #1f2937;--lub-text-color: #f9fafb;--lub-border-color: #4b5563;--lub-muted-color: #9ca3af}