@codebards/ik-embeddable-form 0.0.29-qa

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.
package/README.md ADDED
@@ -0,0 +1,556 @@
1
+ # IK Embeddable Form
2
+
3
+ A production-ready, config-driven embeddable form platform built with **Preact**, **TypeScript**, **Vite**, **Tailwind CSS**, and **Shadow DOM** isolation.
4
+
5
+ > **Integrating the form on your site?** See **[INTEGRATION.md](INTEGRATION.md)** for step-by-step guides (HTML, WordPress, React), variant usage, custom JSON variants, and fallback behavior.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ 1. [Architecture Overview](#architecture-overview)
12
+ 2. [Installation](#installation)
13
+ 3. [Script Usage](#script-usage)
14
+ 4. [Config Schema](#config-schema)
15
+ 5. [Environment Setup](#environment-setup)
16
+ 6. [Build & Deployment](#build--deployment)
17
+ 7. [Versioning Strategy](#versioning-strategy)
18
+ 8. [Adding New Form Variants](#adding-new-form-variants)
19
+ 9. [Analytics Integration](#analytics-integration)
20
+ 10. [API Integration](#api-integration)
21
+
22
+ ---
23
+
24
+ ## Architecture Overview
25
+
26
+ ```
27
+ src/
28
+ ├── core/
29
+ │ ├── FormEngine/ # Orchestrates step transitions, state, API calls
30
+ │ ├── StepResolver/ # Evaluates conditions, resolves next steps
31
+ │ ├── ConfigResolver/ # Resolves FormConfig from OpenConfig + variants
32
+ │ ├── ApiClient/ # HTTP client with retries, body/response mapping
33
+ │ └── Analytics/ # GTM · Sentry · Clarity · Cookie facades
34
+
35
+ ├── components/
36
+ │ ├── Modal/ # Accessible modal with backdrop + progress bar
37
+ │ ├── FormSteps/ # StepRenderer · AutoStep · SuccessStep · FieldRenderer
38
+ │ └── Shared/ # Button · Input · Select · Textarea · Radio · Checkbox
39
+
40
+ ├── configs/
41
+ │ ├── gql-webinar.base.ts # Full webinar form superset
42
+ │ ├── gql-webinar.config.ts # Base + bundled variants registry
43
+ │ └── variants/ # Structural overrides (default, india, event)
44
+
45
+ ├── hooks/
46
+ │ ├── useFormEngine.ts # Subscribes to FormEngine state
47
+ │ └── useAnalytics.ts # Analytics action hooks
48
+
49
+ ├── styles/
50
+ │ └── base.css # Tailwind directives + Shadow DOM resets
51
+
52
+ ├── types/
53
+ │ └── index.ts # FormConfig · FormStep · ValidationRule · ApiContract · AnalyticsEvent
54
+
55
+ ├── App.tsx # Root Preact component
56
+ └── sdk/
57
+ ├── index.ts # window.IKForm global API
58
+ └── mount.ts # Shadow DOM mount/unmount/destroy
59
+ ```
60
+
61
+ ### Shadow DOM Isolation
62
+
63
+ The form is mounted inside a **Shadow DOM** attached to a `<div id="ik-form-host">` injected into `document.body`. This means:
64
+
65
+ - Global CSS on the host page **cannot** bleed into the form
66
+ - Form styles **cannot** leak out to the host page
67
+ - No class name or selector conflicts
68
+
69
+ ---
70
+
71
+ ## Installation
72
+
73
+ ### Via CDN (Recommended)
74
+
75
+ ```html
76
+ <script src="https://cdn.example.com/forms/v1.0.11/embed.js" async></script>
77
+ ```
78
+
79
+ ### Self-hosted
80
+
81
+ ```bash
82
+ # Build for production
83
+ npm run build:production
84
+
85
+ # Upload dist/embed.js to your CDN / server
86
+ ```
87
+
88
+ ### npm (for framework integration)
89
+
90
+ ```bash
91
+ npm install @codebards/ik-embeddable-form
92
+ ```
93
+
94
+ ```ts
95
+ import { IKForm } from '@codebards/ik-embeddable-form';
96
+
97
+ IKForm.open({
98
+ eventName: 'How to Nail your next Technical Interview',
99
+ webinarType: 'REGULAR',
100
+ site: 'organic',
101
+ variant: 'default',
102
+ });
103
+ ```
104
+
105
+ Full integration examples (HTML, WordPress, React, variants, custom JSON): **[INTEGRATION.md](INTEGRATION.md)**
106
+
107
+ ---
108
+
109
+ ## Script Usage
110
+
111
+ ### Basic
112
+
113
+ ```html
114
+ <script src="https://cdn.example.com/forms/v1.0.11/embed.js" async></script>
115
+ <script>
116
+ window.IKForm.open({
117
+ eventName: 'How to Nail your next Technical Interview',
118
+ webinarType: 'REGULAR',
119
+ site: 'organic', // or 'learn'
120
+ variant: 'default', // 'default' | 'india' | 'event' | content variant name
121
+ });
122
+ </script>
123
+ ```
124
+
125
+ ### Common options
126
+
127
+ ```ts
128
+ window.IKForm.open({
129
+ // Required
130
+ eventName: 'How to Nail your next Technical Interview',
131
+ webinarType: 'REGULAR',
132
+ site: 'learn',
133
+ variant: 'india',
134
+
135
+ // Optional
136
+ prefilledValues: {
137
+ email: 'user@example.com',
138
+ fullName: 'Jane Doe',
139
+ },
140
+
141
+ configOverrides: {
142
+ layout: {
143
+ leftPanel: { data: { headline: 'Campaign headline' } },
144
+ },
145
+ },
146
+
147
+ dataClickId: 'hero_cta',
148
+
149
+ // WordPress — theme already loads GTM
150
+ loadGtm: false,
151
+ loadClarity: false,
152
+
153
+ onSuccess: (result) => console.log('Submitted:', result.data),
154
+ onClose: () => console.log('Closed'),
155
+ onError: (err) => console.error(err.code, err.message),
156
+ });
157
+ ```
158
+
159
+ See **[INTEGRATION.md](INTEGRATION.md)** for WordPress, React/SPA, variant JSON, and fallback details.
160
+
161
+ ### Programmatic Control
162
+
163
+ ```ts
164
+ window.IKForm.close();
165
+ window.IKForm.destroy(); // SPA route change
166
+ console.log(window.IKForm.getVersion()); // "1.0.11"
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Config Schema
172
+
173
+ ### `FormConfig` (root)
174
+
175
+ | Property | Type | Required | Description |
176
+ |------------------|-------------------|----------|--------------------------------------------------|
177
+ | `id` | `string` | ✅ | Unique config identifier |
178
+ | `name` | `string` | ✅ | Human-readable form name |
179
+ | `version` | `string` | ✅ | Semver (e.g. `"1.2.0"`) |
180
+ | `eventName` | `string` | ✅ | Matches `OpenConfig.eventName` |
181
+ | `webinarType` | `string` | – | Matches `OpenConfig.webinarType` |
182
+ | `defaultVariant` | `string` | ✅ | Fallback variant ID |
183
+ | `variants` | `FormVariant[]` | – | Named overrides applied on top of base config |
184
+ | `steps` | `FormStep[]` | ✅ | Ordered step definitions |
185
+ | `api` | `ApiContract` | ✅ | Default submission endpoint |
186
+ | `analytics` | `AnalyticsConfig` | – | Open/close/submit event config |
187
+ | `theme` | `ThemeConfig` | – | Visual customisation |
188
+ | `i18n` | `Record<string, string>` | – | Localisation strings |
189
+ | `metadata` | `Record<string, string \| number \| boolean>` | – | Forwarded to analytics |
190
+
191
+ ### `FormStep`
192
+
193
+ | Property | Type | Description |
194
+ |------------------|----------------------------------|--------------------------------------------------|
195
+ | `id` | `string` | Unique step identifier |
196
+ | `type` | `"form" \| "auto" \| "success" \| "error" \| "info"` | Step type |
197
+ | `title` | `string?` | Step heading |
198
+ | `subtitle` | `string?` | Step sub-heading |
199
+ | `hidden` | `boolean?` | Hide from progress indicator |
200
+ | `condition` | `ConditionalRule?` | Step only reachable when condition passes |
201
+ | `autoExecute` | `AutoExecuteConfig?` | API call fired automatically on step entry |
202
+ | `fields` | `FormField[]?` | Input fields rendered in this step |
203
+ | `api` | `ApiContract?` | Per-step API call (fires on Next) |
204
+ | `nextStep` | `string \| ConditionalNextStep[]?` | Fixed or conditional next step routing |
205
+ | `allowBack` | `boolean?` | Show Back button (default: `true`) |
206
+ | `submitLabel` | `string?` | Override CTA label |
207
+ | `analyticsEvents`| `AnalyticsEvent[]?` | Events fired when step becomes active |
208
+
209
+ ### `FormField`
210
+
211
+ | Property | Type | Description |
212
+ |----------------|-------------------|--------------------------------------------------|
213
+ | `name` | `string` | Field name (key in form data) |
214
+ | `type` | `FieldType` | `text \| email \| tel \| number \| textarea \| select \| radio \| checkbox \| date \| hidden \| slot-picker` |
215
+ | `label` | `string?` | Label text |
216
+ | `placeholder` | `string?` | Placeholder text |
217
+ | `defaultValue` | `string \| number \| boolean?` | Initial value |
218
+ | `options` | `FieldOption[]?` | Options for select/radio |
219
+ | `validation` | `ValidationRule[]?` | Array of validation rules |
220
+ | `condition` | `ConditionalRule?` | Show/hide field based on other field values |
221
+ | `colSpan` | `number?` | Grid column span (1 = half, 2 = full) |
222
+
223
+ ### `ValidationRule`
224
+
225
+ | Property | Type | Description |
226
+ |--------------|------------------|---------------------------------------------------|
227
+ | `type` | `ValidationType` | `required \| email \| phone \| min \| max \| minLength \| maxLength \| pattern \| custom \| async` |
228
+ | `value` | `number \| string?` | Threshold or pattern string |
229
+ | `message` | `string` | Error message shown to user |
230
+ | `condition` | `ConditionalRule?` | Only apply this rule when condition is true |
231
+
232
+ ### `ApiContract`
233
+
234
+ | Property | Type | Description |
235
+ |-------------------|-----------------------|--------------------------------------------------|
236
+ | `endpoint` | `string` | Relative path or full URL. Supports `{fieldName}` interpolation |
237
+ | `method` | `HttpMethod` | `GET \| POST \| PUT \| PATCH \| DELETE` |
238
+ | `bodyMapping` | `Record<string,string> \| "$all"` | Map form fields → request body keys |
239
+ | `responseMapping` | `Record<string,string>?` | Map response JSON paths → form field names |
240
+ | `queryParams` | `Record<string,string>?` | Query params with `{fieldName}` interpolation|
241
+ | `authenticated` | `boolean?` | Include `Authorization: Bearer` header |
242
+ | `retries` | `number?` | Retry count (default: 1) |
243
+ | `timeoutMs` | `number?` | Request timeout (default: 10,000ms) |
244
+ | `mockResponse` | `unknown?` | Used in `local` env instead of real network call |
245
+
246
+ ### `ConditionalRule`
247
+
248
+ ```ts
249
+ {
250
+ logical?: "AND" | "OR", // default: "AND"
251
+ clauses: [
252
+ {
253
+ field: "fieldName", // dot notation supported: "address.city"
254
+ operator: "equals" | "not_equals" | "contains" | "not_contains" |
255
+ "starts_with" | "ends_with" | "greater_than" | "less_than" |
256
+ "is_empty" | "is_not_empty" | "matches_regex",
257
+ value?: "some value" // not needed for is_empty / is_not_empty
258
+ }
259
+ ]
260
+ }
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Environment Setup
266
+
267
+ | File | Used For |
268
+ |-------------------|------------------------|
269
+ | `.env` | Local development |
270
+ | `.env.staging` | Staging builds |
271
+ | `.env.production` | Production builds |
272
+
273
+ ### Required Variables
274
+
275
+ ```ini
276
+ VITE_API_BASE_URL=https://api.example.com/api
277
+ VITE_GTM_ID=GTM-XXXXXXX
278
+ VITE_SENTRY_DSN=https://xxx@sentry.io/xxx
279
+ VITE_CLARITY_ID=xxxxxxxxxx
280
+ ```
281
+
282
+ ---
283
+
284
+ ## Build & Deployment
285
+
286
+ ### Local Dev Server
287
+
288
+ ```bash
289
+ npm install
290
+ npm run dev
291
+ # Opens http://localhost:3000 with a test page
292
+ ```
293
+
294
+ ### Build Commands
295
+
296
+ ```bash
297
+ npm run build # Builds with .env (local defaults)
298
+ npm run build:staging # Builds with .env.staging
299
+ npm run build:production # Builds with .env.production (minified)
300
+ ```
301
+
302
+ ### Build Output
303
+
304
+ ```
305
+ dist/
306
+ ├── embed.js # Self-contained IIFE bundle (reference in <script>)
307
+ └── embed.js.map # Source map (only in staging/local builds)
308
+ ```
309
+
310
+ ### Deployment Checklist
311
+
312
+ 1. Run `npm run build:production`
313
+ 2. Upload `dist/embed.js` to your CDN with a versioned path:
314
+ ```
315
+ https://cdn.example.com/forms/v1.0.0/embed.js
316
+ ```
317
+ 3. Set cache headers:
318
+ - `embed.js` → `Cache-Control: public, max-age=31536000, immutable` (versioned path)
319
+ - Use a `latest` alias for convenience: `cdn.example.com/forms/latest/embed.js` with shorter TTL
320
+
321
+ 4. Reference on the host page:
322
+ ```html
323
+ <script src="https://cdn.example.com/forms/v1.0.0/embed.js" async></script>
324
+ ```
325
+
326
+ ---
327
+
328
+ ## Versioning Strategy
329
+
330
+ This project follows **Semantic Versioning** (`MAJOR.MINOR.PATCH`):
331
+
332
+ | Change Type | Version Bump | Example |
333
+ |----------------------|-------------|---------|
334
+ | Breaking API change | MAJOR | `2.0.0` |
335
+ | New form variant | MINOR | `1.1.0` |
336
+ | Bug fix / patch | PATCH | `1.0.1` |
337
+
338
+ ### Release Process
339
+
340
+ ```bash
341
+ # 1. Bump version in package.json
342
+ npm version patch # or minor / major
343
+
344
+ # 2. Build production artefact
345
+ npm run build:production
346
+
347
+ # 3. Tag the release
348
+ git tag v$(node -p "require('./package.json').version")
349
+ git push --tags
350
+
351
+ # 4. Upload dist/embed.js to CDN under versioned path
352
+ ```
353
+
354
+ The `__VERSION__` constant is automatically injected from `package.json` at build time.
355
+
356
+ ---
357
+
358
+ ## Adding New Form Variants
359
+
360
+ The form uses a **base config + variant overrides** merge pattern. See **[INTEGRATION.md](INTEGRATION.md)** for host-page usage, JSON schema, resolution order, and fallback behavior.
361
+
362
+ | Tier | Examples | Where | Deploy needed? |
363
+ |------|----------|-------|----------------|
364
+ | **Structural** | `default`, `india`, `event` | `src/configs/variants/*.variant.ts` | Yes (code review) |
365
+ | **Content** | `masterclass`, campaign names | CDN JSON at `{VITE_FORM_VARIANTS_BASE_URL}/{variant}.json` | No (upload JSON only) |
366
+
367
+ **Resolution order:** bundled variant → remote JSON → fallback to `default` (console warning).
368
+
369
+ Set `VITE_FORM_VARIANTS_BASE_URL` at build time. Local dev defaults to `/form-variants` (`public/form-variants/masterclass.json`).
370
+
371
+ ### Bundled variant files
372
+
373
+ | File | Purpose |
374
+ |------|---------|
375
+ | `default.variant.ts` | Standard flow |
376
+ | `india.variant.ts` | Hide `primaryGoal` on profile step |
377
+ | `event.variant.ts` | Hide slot picker; event copy; auto-book slot |
378
+
379
+ Base config (`gql-webinar.base.ts`) holds the full superset. Each variant file declares only what differs.
380
+
381
+ ### Adding a structural variant (engineering)
382
+
383
+ 1. Create `src/configs/variants/my-variant.variant.ts` with only the diff:
384
+
385
+ ```ts
386
+ import type { FormVariant } from '@/types';
387
+
388
+ export const myVariant: FormVariant = {
389
+ id: 'my-variant',
390
+ overrides: {
391
+ steps: {
392
+ 'profile-details': {
393
+ fields: { primaryGoal: { show: false } },
394
+ },
395
+ },
396
+ },
397
+ };
398
+ ```
399
+
400
+ 2. Register in `src/configs/variants/index.ts`
401
+ 3. Deploy embed
402
+
403
+ ---
404
+
405
+ ### Adding a content variant (no code deploy)
406
+
407
+ 1. Create `{variant}.json` — see [INTEGRATION.md → Create your own variant JSON](INTEGRATION.md#create-your-own-variant-json)
408
+ 2. Upload to `{VITE_FORM_VARIANTS_BASE_URL}/{variant}.json`
409
+ 3. Landing page: `IKForm.open({ ..., variant: 'masterclass' })`
410
+
411
+ ---
412
+
413
+ ### Option B — New FormConfig File
414
+
415
+ For a fundamentally different product flow (not a variant of GQL webinar):
416
+
417
+ 1. Create `src/configs/my-new-form.base.ts`
418
+ 2. Export a `FormConfig` with a unique `webinarType`
419
+ 3. Register in `src/configs/index.ts`
420
+
421
+ ```ts
422
+ IKForm.open({ webinarType: 'MASTERCLASS', variant: 'default', ... });
423
+ ```
424
+
425
+ ---
426
+
427
+ ## Analytics Integration
428
+
429
+ ### GTM
430
+
431
+ The embed can **load its own GTM container** (per architecture doc) so form conversions work on Lovable / Framer / plain HTML without host-page GTM.
432
+
433
+ | Setting | Default | Purpose |
434
+ |---------|---------|---------|
435
+ | `VITE_GTM_ID` | `GTM-P335R9N` (staging/prod) | Container injected when form opens |
436
+ | `loadGtm` in `IKForm.open()` | `true` | Set `false` on WordPress if the theme already loads the same container |
437
+ | `gtmContainerId` in `IKForm.open()` | build-time ID | Optional override (e.g. dedicated embed container) |
438
+
439
+ ```ts
440
+ // Self-contained LP (embed loads GTM)
441
+ IKForm.open({
442
+ eventName: 'How to Nail your next Technical Interview',
443
+ webinarType: 'REGULAR',
444
+ site: 'learn',
445
+ variant: 'default',
446
+ });
447
+
448
+ // WordPress page that already has GTM-P335R9N in the theme
449
+ IKForm.open({ ..., loadGtm: false, loadClarity: false });
450
+ ```
451
+
452
+ **dataLayer events** (WordPress parity + embed lifecycle):
453
+
454
+ | When | Event |
455
+ |------|--------|
456
+ | Contact step success | `new_webinar_registration_form_submitted` |
457
+ | Slot proceed | `wordpress_form_submitted` (`formName: "Webinar Slot Selection"`) |
458
+ | Profile step | `pa_new_webinar_registration_form_submitted` (conversion — Meta / Google Ads / LinkedIn) |
459
+ | Form open / step view | `form_open`, `form_step_view`, … |
460
+
461
+ GTM fires **marketing pixels**. **Clickstream** (`ve2lt1a8il/qa` → BigQuery) is separate — see above.
462
+
463
+ Events are pushed to `window.dataLayer`. Configure GTM triggers on:
464
+
465
+ - `form_open` — fired when modal opens
466
+ - `form_close` — fired when modal closes
467
+ - `form_step_view` — fired on each step
468
+ - `form_step_complete` — fired after Next/Submit per step
469
+ - `form_submit_success` — fired on final submission
470
+ - `form_submit_error` — fired on submission failure
471
+
472
+ ### Sentry
473
+
474
+ Install `@sentry/browser` and uncomment the placeholder code in `src/core/Analytics/sentry.ts`. The DSN is injected from `VITE_SENTRY_DSN`.
475
+
476
+ ### Clarity
477
+
478
+ Ensure the Clarity script is on the host page, or uncomment the injection block in `src/core/Analytics/clarity.ts`.
479
+
480
+ ### Custom Events
481
+
482
+ Listen on the host page:
483
+
484
+ ```js
485
+ window.addEventListener('ikform:analytics', (e) => {
486
+ console.log('IKForm event:', e.detail);
487
+ });
488
+ ```
489
+
490
+ ---
491
+
492
+ ## API Integration
493
+
494
+ All API calls are defined in `ApiContract` objects inside your form config — **no hardcoded fetch calls**.
495
+
496
+ ### Dynamic URL Interpolation
497
+
498
+ ```ts
499
+ endpoint: 'webinar/{site}/add-info'
500
+ // If form data has { site: "organic" }
501
+ // → POST https://api.example.com/api/webinar/organic/add-info
502
+ ```
503
+
504
+ ### Body Mapping
505
+
506
+ ```ts
507
+ bodyMapping: {
508
+ email: 'user.email', // form field → nested request body key
509
+ slot: 'booking.slotId',
510
+ }
511
+ // → { user: { email: "..." }, booking: { slotId: "..." } }
512
+
513
+ // OR forward all fields:
514
+ bodyMapping: '$all'
515
+ ```
516
+
517
+ ### Response Mapping
518
+
519
+ ```ts
520
+ responseMapping: {
521
+ 'data.registrationId': 'registrationId', // response path → form field
522
+ }
523
+ // Writes response.data.registrationId back into form state as "registrationId"
524
+ ```
525
+
526
+ ### Auth Token
527
+
528
+ ```ts
529
+ import { apiClient } from '@codebards/ik-embeddable-form';
530
+
531
+ apiClient.setAuthToken('Bearer your-token-here');
532
+ ```
533
+
534
+ ---
535
+
536
+ ## TypeScript Types Reference
537
+
538
+ ```ts
539
+ import type {
540
+ FormConfig,
541
+ FormStep,
542
+ FormField,
543
+ ValidationRule,
544
+ ApiContract,
545
+ AnalyticsEvent,
546
+ OpenConfig,
547
+ ConditionalRule,
548
+ IKFormSDK,
549
+ } from '@codebards/ik-embeddable-form';
550
+ ```
551
+
552
+ ---
553
+
554
+ ## License
555
+
556
+ MIT — see [LICENSE](LICENSE)