@nodii/onboarding 0.0.1 → 0.1.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 (73) hide show
  1. package/README.md +75 -2
  2. package/dist/chunk-7NS7ENJV.js +232 -0
  3. package/dist/chunk-7NS7ENJV.js.map +1 -0
  4. package/dist/chunk-AZE3NPSY.js +186 -0
  5. package/dist/chunk-AZE3NPSY.js.map +1 -0
  6. package/dist/chunk-BMQINH6O.js +401 -0
  7. package/dist/chunk-BMQINH6O.js.map +1 -0
  8. package/dist/chunk-F75ZJERM.js +199 -0
  9. package/dist/chunk-F75ZJERM.js.map +1 -0
  10. package/dist/chunk-FIRSA7EH.js +3 -0
  11. package/dist/chunk-FIRSA7EH.js.map +1 -0
  12. package/dist/chunk-IPAKPLIF.js +1660 -0
  13. package/dist/chunk-IPAKPLIF.js.map +1 -0
  14. package/dist/chunk-JFDQZ3AC.js +3 -0
  15. package/dist/chunk-JFDQZ3AC.js.map +1 -0
  16. package/dist/chunk-JFIONONR.js +3 -0
  17. package/dist/chunk-JFIONONR.js.map +1 -0
  18. package/dist/chunk-KQTS47FC.js +546 -0
  19. package/dist/chunk-KQTS47FC.js.map +1 -0
  20. package/dist/chunk-LVJM4CTG.js +306 -0
  21. package/dist/chunk-LVJM4CTG.js.map +1 -0
  22. package/dist/chunk-SEKNVHD3.js +59 -0
  23. package/dist/chunk-SEKNVHD3.js.map +1 -0
  24. package/dist/chunk-SIMLMQPT.js +37 -0
  25. package/dist/chunk-SIMLMQPT.js.map +1 -0
  26. package/dist/chunk-U56GQITG.js +79 -0
  27. package/dist/chunk-U56GQITG.js.map +1 -0
  28. package/dist/chunk-U6AXKGYQ.js +101 -0
  29. package/dist/chunk-U6AXKGYQ.js.map +1 -0
  30. package/dist/chunk-VH6FWDWI.js +3 -0
  31. package/dist/chunk-VH6FWDWI.js.map +1 -0
  32. package/dist/chunk-XJTJPBJB.js +191 -0
  33. package/dist/chunk-XJTJPBJB.js.map +1 -0
  34. package/dist/chunk-Z4PA6C2Z.js +3 -0
  35. package/dist/chunk-Z4PA6C2Z.js.map +1 -0
  36. package/dist/client-CqE2dF9A.d.ts +86 -0
  37. package/dist/consent/index.d.ts +59 -0
  38. package/dist/consent/index.js +7 -0
  39. package/dist/consent/index.js.map +1 -0
  40. package/dist/controller-BQb62D0N.d.ts +177 -0
  41. package/dist/dynamic/index.d.ts +235 -0
  42. package/dist/dynamic/index.js +6 -0
  43. package/dist/dynamic/index.js.map +1 -0
  44. package/dist/errors-vJkye4a0.d.ts +110 -0
  45. package/dist/form/index.d.ts +64 -0
  46. package/dist/form/index.js +5 -0
  47. package/dist/form/index.js.map +1 -0
  48. package/dist/index-Dqfa890U.d.ts +83 -0
  49. package/dist/index.d.ts +16 -2
  50. package/dist/index.js +18 -2
  51. package/dist/index.js.map +1 -0
  52. package/dist/model-ESLSEbi0.d.ts +640 -0
  53. package/dist/model-mnjAEtKe.d.ts +253 -0
  54. package/dist/recaptcha/index.d.ts +102 -0
  55. package/dist/recaptcha/index.js +4 -0
  56. package/dist/recaptcha/index.js.map +1 -0
  57. package/dist/render-DC7P2BGQ.js +5 -0
  58. package/dist/render-DC7P2BGQ.js.map +1 -0
  59. package/dist/schema-BrXb9gnh.d.ts +267 -0
  60. package/dist/signup-client-BL5DfarJ.d.ts +392 -0
  61. package/dist/spec/index.d.ts +17 -0
  62. package/dist/spec/index.js +5 -0
  63. package/dist/spec/index.js.map +1 -0
  64. package/dist/submit/index.d.ts +42 -0
  65. package/dist/submit/index.js +6 -0
  66. package/dist/submit/index.js.map +1 -0
  67. package/dist/viewer/index.d.ts +4 -0
  68. package/dist/viewer/index.js +5 -0
  69. package/dist/viewer/index.js.map +1 -0
  70. package/dist/widget/index.d.ts +262 -0
  71. package/dist/widget/index.js +12 -0
  72. package/dist/widget/index.js.map +1 -0
  73. package/package.json +96 -6
@@ -0,0 +1,1660 @@
1
+ import { createContactFormState } from './chunk-U6AXKGYQ.js';
2
+ import { defaultSpecCache, fetchOnboardingSpec, diffSpecFields } from './chunk-F75ZJERM.js';
3
+ import { createSignupStepper, optionValue } from './chunk-KQTS47FC.js';
4
+ import { computePayloadFingerprint, generateIdempotencyKey, submitLead, computeSignupFingerprint, submitSignup } from './chunk-BMQINH6O.js';
5
+ import { OnboardingBotChallengeError, OnboardingRateLimitError, OnboardingSpecStaleError, OnboardingConsentStaleError, OnboardingConsentIncompleteError } from './chunk-XJTJPBJB.js';
6
+ import { loadRecaptcha, executeRecaptcha } from './chunk-7NS7ENJV.js';
7
+
8
+ // src/widget/dom-utils.ts
9
+ function cssEscape(value) {
10
+ const g = globalThis;
11
+ if (g.CSS && typeof g.CSS.escape === "function") {
12
+ return g.CSS.escape(value);
13
+ }
14
+ return value.replace(/[^A-Za-z0-9_-]/g, (ch) => `\\${ch}`);
15
+ }
16
+ function idSelector(id) {
17
+ return `#${cssEscape(id)}`;
18
+ }
19
+
20
+ // src/widget/styles.ts
21
+ var DEFAULT_WIDGET_CSS = `
22
+ .nodii-onboarding-widget {
23
+ --_font: var(--nodii-onboarding-font-family, system-ui, -apple-system, sans-serif);
24
+ --_primary: var(--nodii-onboarding-primary-color, #2563eb);
25
+ --_primary-hover: var(--nodii-onboarding-primary-hover, #1d4ed8);
26
+ --_error: var(--nodii-onboarding-error-color, #dc2626);
27
+ --_border: var(--nodii-onboarding-border-color, #d1d5db);
28
+ --_input-bg: var(--nodii-onboarding-input-bg, #fff);
29
+ --_label: var(--nodii-onboarding-label-color, #374151);
30
+ --_radius: var(--nodii-onboarding-radius, 6px);
31
+ --_spacing: var(--nodii-onboarding-spacing, 16px);
32
+
33
+ font-family: var(--_font);
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ .nodii-onboarding-widget *,
38
+ .nodii-onboarding-widget *::before,
39
+ .nodii-onboarding-widget *::after {
40
+ box-sizing: inherit;
41
+ }
42
+
43
+ .nodii-onboarding-field {
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: calc(var(--_spacing) * 0.25);
47
+ margin-bottom: var(--_spacing);
48
+ }
49
+
50
+ .nodii-onboarding-label {
51
+ font-size: 0.875rem;
52
+ font-weight: 500;
53
+ color: var(--_label);
54
+ }
55
+
56
+ .nodii-onboarding-label.required::after {
57
+ content: " *";
58
+ color: var(--_error);
59
+ }
60
+
61
+ .nodii-onboarding-input,
62
+ .nodii-onboarding-textarea {
63
+ width: 100%;
64
+ padding: calc(var(--_spacing) * 0.5) calc(var(--_spacing) * 0.75);
65
+ border: 1px solid var(--_border);
66
+ border-radius: var(--_radius);
67
+ background: var(--_input-bg);
68
+ font-family: var(--_font);
69
+ font-size: 1rem;
70
+ line-height: 1.5;
71
+ outline: none;
72
+ transition: border-color 0.15s ease;
73
+ }
74
+
75
+ .nodii-onboarding-input:focus,
76
+ .nodii-onboarding-textarea:focus {
77
+ border-color: var(--_primary);
78
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--_primary) 20%, transparent);
79
+ }
80
+
81
+ .nodii-onboarding-input.has-error,
82
+ .nodii-onboarding-textarea.has-error {
83
+ border-color: var(--_error);
84
+ }
85
+
86
+ .nodii-onboarding-textarea {
87
+ min-height: 100px;
88
+ resize: vertical;
89
+ }
90
+
91
+ .nodii-onboarding-field-error {
92
+ font-size: 0.75rem;
93
+ color: var(--_error);
94
+ margin-top: 2px;
95
+ }
96
+
97
+ .nodii-onboarding-server-error {
98
+ padding: calc(var(--_spacing) * 0.75);
99
+ border-radius: var(--_radius);
100
+ background: color-mix(in srgb, var(--_error) 8%, transparent);
101
+ border: 1px solid color-mix(in srgb, var(--_error) 30%, transparent);
102
+ color: var(--_error);
103
+ font-size: 0.875rem;
104
+ margin-bottom: var(--_spacing);
105
+ }
106
+
107
+ .nodii-onboarding-success {
108
+ padding: var(--_spacing);
109
+ border-radius: var(--_radius);
110
+ background: #f0fdf4;
111
+ border: 1px solid #bbf7d0;
112
+ color: #15803d;
113
+ font-size: 0.9375rem;
114
+ text-align: center;
115
+ }
116
+
117
+ .nodii-onboarding-submit-btn {
118
+ display: inline-flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ width: 100%;
122
+ padding: calc(var(--_spacing) * 0.625) var(--_spacing);
123
+ background: var(--_primary);
124
+ color: #fff;
125
+ border: none;
126
+ border-radius: var(--_radius);
127
+ font-family: var(--_font);
128
+ font-size: 1rem;
129
+ font-weight: 500;
130
+ cursor: pointer;
131
+ transition: background 0.15s ease;
132
+ }
133
+
134
+ .nodii-onboarding-submit-btn:hover:not(:disabled) {
135
+ background: var(--_primary-hover);
136
+ }
137
+
138
+ .nodii-onboarding-submit-btn:disabled {
139
+ opacity: 0.6;
140
+ cursor: not-allowed;
141
+ }
142
+
143
+ .nodii-onboarding-spinner {
144
+ display: inline-block;
145
+ width: 1em;
146
+ height: 1em;
147
+ border: 2px solid rgba(255,255,255,0.4);
148
+ border-top-color: #fff;
149
+ border-radius: 50%;
150
+ animation: nodii-spin 0.6s linear infinite;
151
+ margin-right: 0.5em;
152
+ }
153
+
154
+ @keyframes nodii-spin {
155
+ to { transform: rotate(360deg); }
156
+ }
157
+
158
+ .nodii-onboarding-recaptcha-notice {
159
+ font-size: 0.7rem;
160
+ color: #6b7280;
161
+ margin-top: calc(var(--_spacing) * 0.5);
162
+ text-align: center;
163
+ }
164
+
165
+ .nodii-onboarding-recaptcha-notice a {
166
+ color: inherit;
167
+ }
168
+
169
+ /* \u2500\u2500 dynamic stepper (signup form) \u2500\u2500 */
170
+
171
+ .nodii-onboarding-steps {
172
+ display: flex;
173
+ gap: calc(var(--_spacing) * 0.5);
174
+ margin-bottom: var(--_spacing);
175
+ }
176
+
177
+ .nodii-onboarding-step-dot {
178
+ flex: 1;
179
+ height: 4px;
180
+ border-radius: 2px;
181
+ background: var(--_border);
182
+ }
183
+
184
+ .nodii-onboarding-step-dot.active {
185
+ background: var(--_primary);
186
+ }
187
+
188
+ .nodii-onboarding-stage-title {
189
+ font-size: 1.0625rem;
190
+ font-weight: 600;
191
+ color: var(--_label);
192
+ margin: 0 0 var(--_spacing) 0;
193
+ }
194
+
195
+ .nodii-onboarding-nav {
196
+ display: flex;
197
+ gap: calc(var(--_spacing) * 0.5);
198
+ margin-top: var(--_spacing);
199
+ }
200
+
201
+ .nodii-onboarding-back-btn {
202
+ padding: calc(var(--_spacing) * 0.625) var(--_spacing);
203
+ background: transparent;
204
+ color: var(--_label);
205
+ border: 1px solid var(--_border);
206
+ border-radius: var(--_radius);
207
+ font-family: var(--_font);
208
+ font-size: 1rem;
209
+ cursor: pointer;
210
+ }
211
+
212
+ .nodii-onboarding-stale-banner {
213
+ padding: calc(var(--_spacing) * 0.75);
214
+ border-radius: var(--_radius);
215
+ background: #fffbeb;
216
+ border: 1px solid #fcd34d;
217
+ color: #92400e;
218
+ font-size: 0.875rem;
219
+ margin-bottom: var(--_spacing);
220
+ }
221
+
222
+ .nodii-onboarding-consent-row {
223
+ display: flex;
224
+ align-items: flex-start;
225
+ gap: calc(var(--_spacing) * 0.5);
226
+ margin-bottom: calc(var(--_spacing) * 0.5);
227
+ font-size: 0.875rem;
228
+ color: var(--_label);
229
+ }
230
+
231
+ .nodii-onboarding-consent-row input[type="checkbox"] {
232
+ margin-top: 3px;
233
+ }
234
+
235
+ .nodii-onboarding-account-section {
236
+ border-top: 1px solid var(--_border);
237
+ margin-top: var(--_spacing);
238
+ padding-top: var(--_spacing);
239
+ }
240
+
241
+ .nodii-onboarding-loading {
242
+ padding: var(--_spacing);
243
+ text-align: center;
244
+ color: var(--_label);
245
+ }
246
+
247
+ /* \u2500\u2500 D543 ConsentPack step (full-width, generous spacing) \u2500\u2500 */
248
+
249
+ .nodii-onboarding-consent-stage {
250
+ /* Full-bleed within the widget host \u2014 consent cards may span the page. */
251
+ width: 100%;
252
+ display: flex;
253
+ flex-direction: column;
254
+ gap: calc(var(--_spacing) * 2);
255
+ margin: calc(var(--_spacing) * 1.5) 0;
256
+ }
257
+
258
+ .nodii-onboarding-consent-intro {
259
+ font-size: 0.9375rem;
260
+ color: var(--_label);
261
+ margin: 0 0 calc(var(--_spacing) * 0.5) 0;
262
+ line-height: 1.6;
263
+ }
264
+
265
+ .nodii-onboarding-consent-card {
266
+ width: 100%;
267
+ border: 1px solid var(--_border);
268
+ border-radius: var(--_radius);
269
+ background: var(--_input-bg);
270
+ padding: calc(var(--_spacing) * 1.5);
271
+ display: flex;
272
+ flex-direction: column;
273
+ gap: var(--_spacing);
274
+ }
275
+
276
+ .nodii-onboarding-consent-card.is-stale {
277
+ border-color: #fcd34d;
278
+ background: #fffbeb;
279
+ }
280
+
281
+ .nodii-onboarding-consent-card.has-error {
282
+ border-color: var(--_error);
283
+ }
284
+
285
+ .nodii-onboarding-consent-title {
286
+ font-size: 1.125rem;
287
+ font-weight: 600;
288
+ color: var(--_label);
289
+ margin: 0;
290
+ }
291
+
292
+ .nodii-onboarding-consent-stale-tag {
293
+ font-size: 0.8125rem;
294
+ font-weight: 500;
295
+ color: #92400e;
296
+ }
297
+
298
+ .nodii-onboarding-consent-gate {
299
+ width: 100%;
300
+ }
301
+
302
+ .nodii-onboarding-consent-scrollbox {
303
+ width: 100%;
304
+ height: 360px;
305
+ overflow-y: auto;
306
+ border: 1px solid var(--_border);
307
+ border-radius: var(--_radius);
308
+ background: #fff;
309
+ position: relative;
310
+ }
311
+
312
+ .nodii-onboarding-consent-scrollbox:focus {
313
+ outline: none;
314
+ border-color: var(--_primary);
315
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--_primary) 20%, transparent);
316
+ }
317
+
318
+ /* Inline, same-origin document content (no iframe). Rendered by the default
319
+ viewer: sanitized HTML/markdown, escaped plain text, or PDF.js canvases. */
320
+ .nodii-onboarding-consent-doc-body {
321
+ padding: var(--_spacing);
322
+ font-size: 0.9375rem;
323
+ line-height: 1.7;
324
+ color: var(--_label);
325
+ word-break: break-word;
326
+ }
327
+
328
+ .nodii-onboarding-consent-doc-body:not(.nodii-consent-pdf) {
329
+ white-space: pre-wrap;
330
+ }
331
+
332
+ .nodii-onboarding-consent-doc-loading {
333
+ padding: var(--_spacing);
334
+ color: #6b7280;
335
+ font-size: 0.875rem;
336
+ }
337
+
338
+ .nodii-consent-pdf {
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: var(--_spacing);
342
+ white-space: normal;
343
+ }
344
+
345
+ .nodii-consent-pdf-page {
346
+ max-width: 100%;
347
+ height: auto;
348
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
349
+ }
350
+
351
+ .nodii-onboarding-consent-note {
352
+ font-size: 0.8125rem;
353
+ color: #6b7280;
354
+ margin: calc(var(--_spacing) * 0.5) 0 0 0;
355
+ }
356
+
357
+ /* A gated doc that could not be rendered inline: the gate stays unsatisfiable
358
+ (accept disabled); we surface a clear error + Retry. NO click-to-satisfy. */
359
+ .nodii-onboarding-consent-unrenderable {
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: calc(var(--_spacing) * 0.5);
363
+ padding: var(--_spacing);
364
+ border: 1px solid color-mix(in srgb, var(--_error) 30%, transparent);
365
+ border-radius: var(--_radius);
366
+ background: color-mix(in srgb, var(--_error) 6%, transparent);
367
+ }
368
+
369
+ .nodii-onboarding-consent-retry {
370
+ align-self: flex-start;
371
+ padding: calc(var(--_spacing) * 0.5) var(--_spacing);
372
+ background: transparent;
373
+ color: var(--_primary);
374
+ border: 1px solid var(--_primary);
375
+ border-radius: var(--_radius);
376
+ font-family: var(--_font);
377
+ font-size: 0.875rem;
378
+ cursor: pointer;
379
+ }
380
+
381
+ .nodii-onboarding-consent-open-row,
382
+ .nodii-onboarding-consent-preview-row {
383
+ display: flex;
384
+ }
385
+
386
+ .nodii-onboarding-consent-open {
387
+ display: inline-flex;
388
+ align-items: center;
389
+ padding: calc(var(--_spacing) * 0.625) var(--_spacing);
390
+ border: 1px solid var(--_primary);
391
+ border-radius: var(--_radius);
392
+ color: var(--_primary);
393
+ text-decoration: none;
394
+ font-size: 0.9375rem;
395
+ font-weight: 500;
396
+ }
397
+
398
+ .nodii-onboarding-consent-open:hover {
399
+ background: color-mix(in srgb, var(--_primary) 8%, transparent);
400
+ }
401
+
402
+ .nodii-onboarding-consent-preview {
403
+ font-size: 0.875rem;
404
+ color: var(--_primary);
405
+ }
406
+
407
+ .nodii-onboarding-consent-accept-row {
408
+ display: flex;
409
+ align-items: flex-start;
410
+ gap: calc(var(--_spacing) * 0.5);
411
+ }
412
+
413
+ .nodii-onboarding-consent-accept {
414
+ margin-top: 3px;
415
+ width: 1.125rem;
416
+ height: 1.125rem;
417
+ }
418
+
419
+ .nodii-onboarding-consent-accept:disabled {
420
+ cursor: not-allowed;
421
+ opacity: 0.5;
422
+ }
423
+
424
+ .nodii-onboarding-consent-accept-label {
425
+ font-size: 0.9375rem;
426
+ color: var(--_label);
427
+ line-height: 1.5;
428
+ }
429
+
430
+ .nodii-onboarding-consent-required {
431
+ color: var(--_error);
432
+ }
433
+ `;
434
+ function injectWidgetStyles() {
435
+ if (typeof document === "undefined") return;
436
+ const styleId = "__nodii_onboarding_styles__";
437
+ if (document.getElementById(styleId)) return;
438
+ const style = document.createElement("style");
439
+ style.id = styleId;
440
+ style.textContent = DEFAULT_WIDGET_CSS;
441
+ document.head.appendChild(style);
442
+ }
443
+
444
+ // src/widget/contact-form.ts
445
+ function el(tag, attrs = {}, children = []) {
446
+ const element = document.createElement(tag);
447
+ for (const [key, value] of Object.entries(attrs)) {
448
+ element.setAttribute(key, value);
449
+ }
450
+ for (const child of children) {
451
+ if (typeof child === "string") {
452
+ element.appendChild(document.createTextNode(child));
453
+ } else {
454
+ element.appendChild(child);
455
+ }
456
+ }
457
+ return element;
458
+ }
459
+ function createFieldEl(id, label, input, required) {
460
+ const labelEl = el(
461
+ "label",
462
+ { for: id, class: `nodii-onboarding-label${required ? " required" : ""}` },
463
+ [label]
464
+ );
465
+ const errorEl = el("div", {
466
+ class: "nodii-onboarding-field-error",
467
+ id: `${id}-error`,
468
+ role: "alert",
469
+ "aria-live": "polite"
470
+ });
471
+ const field = el("div", { class: "nodii-onboarding-field" }, [
472
+ labelEl,
473
+ input,
474
+ errorEl
475
+ ]);
476
+ return field;
477
+ }
478
+ function setFieldError(container, fieldId, message) {
479
+ const errorEl = container.querySelector(idSelector(`${fieldId}-error`));
480
+ if (errorEl) errorEl.textContent = message;
481
+ const inputEl = container.querySelector(idSelector(fieldId));
482
+ if (inputEl) inputEl.classList.add("has-error");
483
+ }
484
+ function clearFieldErrors(container) {
485
+ for (const el4 of container.querySelectorAll(
486
+ ".nodii-onboarding-field-error"
487
+ )) {
488
+ el4.textContent = "";
489
+ }
490
+ for (const el4 of container.querySelectorAll(".has-error")) {
491
+ el4.classList.remove("has-error");
492
+ }
493
+ }
494
+ function applyErrors(container, errors) {
495
+ clearFieldErrors(container);
496
+ for (const err of errors) {
497
+ setFieldError(container, `nodii-${err.path}`, err.message);
498
+ }
499
+ }
500
+ function mountContactForm(host, options) {
501
+ injectWidgetStyles();
502
+ const formControl = createContactFormState();
503
+ if (options.siteKey) {
504
+ loadRecaptcha(options.siteKey).catch(() => {
505
+ });
506
+ }
507
+ const wrapper = el("div", { class: "nodii-onboarding-widget" });
508
+ const nameInput = el("input", {
509
+ type: "text",
510
+ id: "nodii-name",
511
+ name: "name",
512
+ class: "nodii-onboarding-input",
513
+ placeholder: "Your name",
514
+ autocomplete: "name",
515
+ maxlength: "200",
516
+ required: "required"
517
+ });
518
+ const emailInput = el("input", {
519
+ type: "email",
520
+ id: "nodii-email",
521
+ name: "email",
522
+ class: "nodii-onboarding-input",
523
+ placeholder: "you@company.com",
524
+ autocomplete: "email",
525
+ required: "required"
526
+ });
527
+ const phoneInput = el("input", {
528
+ type: "tel",
529
+ id: "nodii-phone",
530
+ name: "phone",
531
+ class: "nodii-onboarding-input",
532
+ placeholder: "+1 555 000 0000",
533
+ autocomplete: "tel"
534
+ });
535
+ const messageInput = el("textarea", {
536
+ id: "nodii-message",
537
+ name: "message",
538
+ class: "nodii-onboarding-textarea",
539
+ placeholder: "How can we help?",
540
+ maxlength: "4000",
541
+ rows: "4"
542
+ });
543
+ const serverErrorEl = el("div", {
544
+ class: "nodii-onboarding-server-error",
545
+ role: "alert",
546
+ "aria-live": "assertive",
547
+ style: "display:none"
548
+ });
549
+ const successEl = el(
550
+ "div",
551
+ {
552
+ class: "nodii-onboarding-success",
553
+ role: "status",
554
+ style: "display:none"
555
+ },
556
+ [options.successMessage ?? "Thank you! We'll be in touch shortly."]
557
+ );
558
+ const submitBtn = el(
559
+ "button",
560
+ {
561
+ type: "submit",
562
+ class: "nodii-onboarding-submit-btn"
563
+ },
564
+ [options.submitLabel ?? "Get in touch"]
565
+ );
566
+ const recaptchaNotice = el(
567
+ "p",
568
+ { class: "nodii-onboarding-recaptcha-notice" },
569
+ [
570
+ "Protected by reCAPTCHA. ",
571
+ Object.assign(
572
+ el("a", {
573
+ href: "https://policies.google.com/privacy",
574
+ target: "_blank",
575
+ rel: "noopener noreferrer"
576
+ }),
577
+ { textContent: "Privacy" }
578
+ ),
579
+ " & ",
580
+ Object.assign(
581
+ el("a", {
582
+ href: "https://policies.google.com/terms",
583
+ target: "_blank",
584
+ rel: "noopener noreferrer"
585
+ }),
586
+ { textContent: "Terms" }
587
+ )
588
+ ]
589
+ );
590
+ const form = el("form", {
591
+ novalidate: "true",
592
+ "aria-label": "Contact us"
593
+ });
594
+ form.appendChild(createFieldEl("nodii-name", "Name", nameInput, true));
595
+ form.appendChild(createFieldEl("nodii-email", "Email", emailInput, true));
596
+ form.appendChild(
597
+ createFieldEl("nodii-phone", "Phone (optional)", phoneInput, false)
598
+ );
599
+ form.appendChild(
600
+ createFieldEl("nodii-message", "Message (optional)", messageInput, false)
601
+ );
602
+ form.appendChild(serverErrorEl);
603
+ form.appendChild(submitBtn);
604
+ form.appendChild(recaptchaNotice);
605
+ wrapper.appendChild(successEl);
606
+ wrapper.appendChild(form);
607
+ host.appendChild(wrapper);
608
+ nameInput.addEventListener("input", () => {
609
+ formControl.setValue("name", nameInput.value);
610
+ });
611
+ emailInput.addEventListener("input", () => {
612
+ formControl.setValue("email", emailInput.value);
613
+ });
614
+ phoneInput.addEventListener("input", () => {
615
+ formControl.setValue("phone", phoneInput.value || void 0);
616
+ });
617
+ messageInput.addEventListener("input", () => {
618
+ formControl.setValue("message", messageInput.value || void 0);
619
+ });
620
+ const submitLabel = options.submitLabel ?? "Get in touch";
621
+ function renderSubmitButton(submitting) {
622
+ while (submitBtn.firstChild) {
623
+ submitBtn.removeChild(submitBtn.firstChild);
624
+ }
625
+ if (submitting) {
626
+ const spinner = document.createElement("span");
627
+ spinner.className = "nodii-onboarding-spinner";
628
+ submitBtn.appendChild(spinner);
629
+ submitBtn.appendChild(document.createTextNode("Sending\u2026"));
630
+ } else {
631
+ submitBtn.appendChild(document.createTextNode(submitLabel));
632
+ }
633
+ }
634
+ const unsubscribe = formControl.subscribe((state) => {
635
+ submitBtn.disabled = state.status === "submitting";
636
+ renderSubmitButton(state.status === "submitting");
637
+ applyErrors(wrapper, state.errors);
638
+ if (state.serverError) {
639
+ serverErrorEl.textContent = state.serverError;
640
+ serverErrorEl.style.display = "";
641
+ } else {
642
+ serverErrorEl.style.display = "none";
643
+ }
644
+ if (state.status === "success") {
645
+ successEl.style.display = "";
646
+ form.style.display = "none";
647
+ }
648
+ });
649
+ let idempotencyKey = null;
650
+ let lastFingerprint = null;
651
+ form.addEventListener("submit", async (event) => {
652
+ event.preventDefault();
653
+ formControl.setValue("name", nameInput.value);
654
+ formControl.setValue("email", emailInput.value);
655
+ formControl.setValue("phone", phoneInput.value || void 0);
656
+ formControl.setValue("message", messageInput.value || void 0);
657
+ const valid = formControl.validate();
658
+ if (!valid) return;
659
+ formControl.setSubmitting();
660
+ const values = formControl.getState().values;
661
+ const fingerprint = computePayloadFingerprint(values);
662
+ if (idempotencyKey === null || fingerprint !== lastFingerprint) {
663
+ idempotencyKey = generateIdempotencyKey();
664
+ lastFingerprint = fingerprint;
665
+ }
666
+ let firstToken;
667
+ if (options.siteKey) {
668
+ try {
669
+ firstToken = await executeRecaptcha(options.siteKey, "contact_submit", {
670
+ timeoutMs: options.timeoutMs
671
+ });
672
+ } catch (recaptchaErr) {
673
+ formControl.setError(
674
+ "Could not load bot protection. Please try again."
675
+ );
676
+ options.onError?.(recaptchaErr);
677
+ return;
678
+ }
679
+ }
680
+ let usedFirst = false;
681
+ const tokenProvider = async () => {
682
+ if (!usedFirst) {
683
+ usedFirst = true;
684
+ return firstToken;
685
+ }
686
+ return executeRecaptcha(options.siteKey, "contact_submit", {
687
+ timeoutMs: options.timeoutMs
688
+ });
689
+ };
690
+ try {
691
+ await submitLead(values, {
692
+ baseUrl: options.baseUrl,
693
+ ...options.siteKey ? { botToken: tokenProvider } : {},
694
+ idempotencyKey,
695
+ fetch: options.fetch,
696
+ timeoutMs: options.timeoutMs
697
+ });
698
+ idempotencyKey = null;
699
+ lastFingerprint = null;
700
+ formControl.setSuccess();
701
+ options.onSuccess?.();
702
+ } catch (err) {
703
+ if (err instanceof OnboardingBotChallengeError) {
704
+ formControl.setError(
705
+ "Bot protection check failed. Please refresh the page and try again."
706
+ );
707
+ options.onError?.(err);
708
+ } else if (err instanceof OnboardingRateLimitError) {
709
+ const msg = err.retryAfterSeconds !== null ? `Too many requests. Please wait ${err.retryAfterSeconds}s.` : "Too many requests. Please wait a moment.";
710
+ formControl.setError(msg);
711
+ options.onError?.(err);
712
+ } else {
713
+ formControl.setError(
714
+ "Something went wrong. Please try again in a moment."
715
+ );
716
+ options.onError?.(err);
717
+ }
718
+ }
719
+ });
720
+ return {
721
+ unmount() {
722
+ unsubscribe();
723
+ host.removeChild(wrapper);
724
+ },
725
+ reset() {
726
+ idempotencyKey = null;
727
+ lastFingerprint = null;
728
+ formControl.reset();
729
+ form.style.display = "";
730
+ successEl.style.display = "none";
731
+ nameInput.value = "";
732
+ emailInput.value = "";
733
+ phoneInput.value = "";
734
+ messageInput.value = "";
735
+ }
736
+ };
737
+ }
738
+
739
+ // src/widget/consent-step.ts
740
+ var CONSENT_ID_PREFIX = "nodii-consent-doc-";
741
+ function el2(tag, attrs = {}, children = []) {
742
+ const element = document.createElement(tag);
743
+ for (const [key, value] of Object.entries(attrs)) {
744
+ element.setAttribute(key, value);
745
+ }
746
+ for (const child of children) {
747
+ if (typeof child === "string") {
748
+ element.appendChild(document.createTextNode(child));
749
+ } else {
750
+ element.appendChild(child);
751
+ }
752
+ }
753
+ return element;
754
+ }
755
+ function consentDocElementId(key) {
756
+ return `${CONSENT_ID_PREFIX}${key.replace(/[^A-Za-z0-9_-]/g, "-")}`;
757
+ }
758
+ function buildAcceptRow(control, doc) {
759
+ const id = consentDocElementId(doc.key);
760
+ const checkbox = el2("input", {
761
+ type: "checkbox",
762
+ id,
763
+ class: "nodii-onboarding-consent-accept"
764
+ });
765
+ checkbox.checked = doc.accepted;
766
+ checkbox.disabled = !doc.gateSatisfied;
767
+ checkbox.addEventListener("change", () => {
768
+ control.setAccepted(doc.key, checkbox.checked);
769
+ checkbox.checked = control.getState().documents.find((d) => d.key === doc.key)?.accepted ?? false;
770
+ });
771
+ const labelText = doc.acceptanceLevel === "checkbox" ? `I accept the ${doc.doc.title}` : `I have reviewed and accept the ${doc.doc.title}`;
772
+ const label = el2(
773
+ "label",
774
+ { for: id, class: "nodii-onboarding-consent-accept-label" },
775
+ [labelText]
776
+ );
777
+ if (doc.required) {
778
+ label.appendChild(
779
+ el2("span", { class: "nodii-onboarding-consent-required" }, [" *"])
780
+ );
781
+ }
782
+ const row = el2("div", { class: "nodii-onboarding-consent-accept-row" }, [
783
+ checkbox,
784
+ label
785
+ ]);
786
+ const errorEl = el2("div", {
787
+ class: "nodii-onboarding-field-error nodii-onboarding-consent-error",
788
+ id: `${id}-error`,
789
+ role: "alert",
790
+ "aria-live": "polite"
791
+ });
792
+ return { row, errorEl };
793
+ }
794
+ function buildOpenAffordance(control, doc) {
795
+ const id = consentDocElementId(doc.key);
796
+ const openLink = el2(
797
+ "a",
798
+ {
799
+ class: "nodii-onboarding-consent-open",
800
+ id: `${id}-open`,
801
+ href: doc.doc.downloadUrl,
802
+ target: "_blank",
803
+ rel: "noopener noreferrer"
804
+ },
805
+ [`Open ${doc.doc.title}`]
806
+ );
807
+ const markOpened = () => control.markOpened(doc.key);
808
+ openLink.addEventListener("click", markOpened);
809
+ openLink.addEventListener("keydown", (event) => {
810
+ if (event.key === "Enter") markOpened();
811
+ });
812
+ return el2("div", { class: "nodii-onboarding-consent-open-row" }, [openLink]);
813
+ }
814
+ function buildGateAffordance(control, doc, opts) {
815
+ const id = consentDocElementId(doc.key);
816
+ if (doc.acceptanceLevel === "open_then_accept") {
817
+ return buildOpenAffordance(control, doc);
818
+ }
819
+ if (doc.acceptanceLevel === "checkbox") {
820
+ return el2("div", { class: "nodii-onboarding-consent-preview-row" }, [
821
+ el2(
822
+ "a",
823
+ {
824
+ class: "nodii-onboarding-consent-preview",
825
+ id: `${id}-preview`,
826
+ href: doc.doc.downloadUrl,
827
+ target: "_blank",
828
+ rel: "noopener noreferrer"
829
+ },
830
+ [`View ${doc.doc.title}`]
831
+ )
832
+ ]);
833
+ }
834
+ const container = el2("div", {
835
+ class: "nodii-onboarding-consent-gate",
836
+ id: `${id}-gate`
837
+ });
838
+ container.appendChild(
839
+ el2("div", { class: "nodii-onboarding-consent-doc-loading" }, [
840
+ "Loading document\u2026"
841
+ ])
842
+ );
843
+ const importViewer = opts.importViewer ?? (() => import('./render-DC7P2BGQ.js'));
844
+ const loadViewer = () => {
845
+ void (async () => {
846
+ try {
847
+ const { renderConsentDocument } = await importViewer();
848
+ await renderConsentDocument(container, doc.doc, control, {
849
+ consentKey: doc.key,
850
+ fetch: opts.fetch,
851
+ timeoutMs: opts.timeoutMs
852
+ });
853
+ } catch {
854
+ while (container.firstChild) {
855
+ container.removeChild(container.firstChild);
856
+ }
857
+ const wrap = el2("div", {
858
+ class: "nodii-onboarding-consent-unrenderable",
859
+ role: "alert"
860
+ });
861
+ wrap.appendChild(
862
+ el2(
863
+ "p",
864
+ {
865
+ // DISTINCT id (NOT `${id}-error`): buildAcceptRow already owns
866
+ // `${id}-error`, and updateConsentStep's querySelector would match
867
+ // THIS paragraph first and blank it on the next state change,
868
+ // silently wiping the compliance message. A unique id keeps this
869
+ // load-failure error persistent (P1b).
870
+ class: "nodii-onboarding-field-error nodii-onboarding-consent-error",
871
+ id: `${id}-load-error`
872
+ },
873
+ [
874
+ `\u201C${doc.doc.title}\u201D could not be loaded, so it cannot be accepted yet. Please retry.`
875
+ ]
876
+ )
877
+ );
878
+ const retry = el2(
879
+ "button",
880
+ { type: "button", class: "nodii-onboarding-consent-retry" },
881
+ ["Retry loading document"]
882
+ );
883
+ retry.addEventListener("click", loadViewer);
884
+ wrap.appendChild(retry);
885
+ wrap.appendChild(
886
+ el2(
887
+ "a",
888
+ {
889
+ class: "nodii-onboarding-consent-open",
890
+ href: doc.doc.downloadUrl,
891
+ target: "_blank",
892
+ rel: "noopener noreferrer"
893
+ },
894
+ [`Open ${doc.doc.title} in a new tab`]
895
+ )
896
+ );
897
+ container.appendChild(wrap);
898
+ }
899
+ })();
900
+ };
901
+ loadViewer();
902
+ return container;
903
+ }
904
+ function buildConsentCard(control, doc, stale, opts) {
905
+ const card = el2("div", {
906
+ class: `nodii-onboarding-consent-card${stale ? " is-stale" : ""}`,
907
+ "data-consent-key": doc.key
908
+ });
909
+ card.appendChild(
910
+ el2("h4", { class: "nodii-onboarding-consent-title" }, [doc.doc.title])
911
+ );
912
+ if (stale) {
913
+ card.appendChild(
914
+ el2(
915
+ "div",
916
+ { class: "nodii-onboarding-consent-stale-tag", role: "status" },
917
+ ["Updated \u2014 please review and re-accept."]
918
+ )
919
+ );
920
+ }
921
+ card.appendChild(buildGateAffordance(control, doc, opts));
922
+ const { row, errorEl } = buildAcceptRow(control, doc);
923
+ card.appendChild(row);
924
+ card.appendChild(errorEl);
925
+ return card;
926
+ }
927
+ function renderConsentStep(host, control, state, staleKeys = [], opts = {}) {
928
+ while (host.firstChild) host.removeChild(host.firstChild);
929
+ const staleSet = new Set(staleKeys);
930
+ host.appendChild(
931
+ el2("h3", { class: "nodii-onboarding-stage-title" }, [
932
+ "Review & accept documents"
933
+ ])
934
+ );
935
+ host.appendChild(
936
+ el2("p", { class: "nodii-onboarding-consent-intro" }, [
937
+ "Please review each document below and accept to continue."
938
+ ])
939
+ );
940
+ for (const doc of state.documents) {
941
+ host.appendChild(
942
+ buildConsentCard(control, doc, staleSet.has(doc.key), opts)
943
+ );
944
+ }
945
+ }
946
+ function updateConsentStep(host, state, consentErrors) {
947
+ const errorByKey = new Map(consentErrors.map((e) => [e.path, e.message]));
948
+ for (const doc of state.documents) {
949
+ const id = consentDocElementId(doc.key);
950
+ const checkbox = host.querySelector(
951
+ idSelector(id)
952
+ );
953
+ if (checkbox) {
954
+ checkbox.disabled = !doc.gateSatisfied;
955
+ checkbox.checked = doc.accepted;
956
+ }
957
+ const errorNode = host.querySelector(idSelector(`${id}-error`));
958
+ if (errorNode) errorNode.textContent = errorByKey.get(doc.key) ?? "";
959
+ const card = host.querySelector(
960
+ `[data-consent-key="${cssEscape(doc.key)}"]`
961
+ );
962
+ if (card) {
963
+ if (errorByKey.has(doc.key)) card.classList.add("has-error");
964
+ else card.classList.remove("has-error");
965
+ }
966
+ }
967
+ }
968
+
969
+ // src/widget/signup-form.ts
970
+ var STAGE_TITLES = {
971
+ lead: "About you",
972
+ prospect: "Your business",
973
+ activation: "Choose your services"
974
+ };
975
+ function el3(tag, attrs = {}, children = []) {
976
+ const element = document.createElement(tag);
977
+ for (const [key, value] of Object.entries(attrs)) {
978
+ element.setAttribute(key, value);
979
+ }
980
+ for (const child of children) {
981
+ if (typeof child === "string") {
982
+ element.appendChild(document.createTextNode(child));
983
+ } else {
984
+ element.appendChild(child);
985
+ }
986
+ }
987
+ return element;
988
+ }
989
+ function optionLabel(option) {
990
+ if (option !== null && typeof option === "object") {
991
+ const o = option;
992
+ if (typeof o["label"] === "string") return o["label"];
993
+ }
994
+ return optionValue(option);
995
+ }
996
+ function buildFieldInput(field, currentValue, onChange) {
997
+ const id = `nodii-f-${field.key}`;
998
+ switch (field.type) {
999
+ case "textarea": {
1000
+ const input = el3("textarea", {
1001
+ id,
1002
+ class: "nodii-onboarding-textarea",
1003
+ rows: "4"
1004
+ });
1005
+ if (typeof currentValue === "string") input.value = currentValue;
1006
+ input.addEventListener("input", () => onChange(input.value));
1007
+ return input;
1008
+ }
1009
+ case "checkbox":
1010
+ case "boolean": {
1011
+ const input = el3("input", {
1012
+ type: "checkbox",
1013
+ id
1014
+ });
1015
+ input.checked = currentValue === true;
1016
+ input.addEventListener("change", () => onChange(input.checked));
1017
+ return input;
1018
+ }
1019
+ case "select":
1020
+ case "radio": {
1021
+ const select = el3("select", {
1022
+ id,
1023
+ class: "nodii-onboarding-input"
1024
+ });
1025
+ select.appendChild(el3("option", { value: "" }, ["\u2014 select \u2014"]));
1026
+ for (const opt of field.options ?? []) {
1027
+ const value = optionValue(opt);
1028
+ const optionEl = el3("option", { value }, [optionLabel(opt)]);
1029
+ select.appendChild(optionEl);
1030
+ }
1031
+ if (typeof currentValue === "string") select.value = currentValue;
1032
+ select.addEventListener("change", () => onChange(select.value));
1033
+ return select;
1034
+ }
1035
+ case "multiselect": {
1036
+ const select = el3("select", {
1037
+ id,
1038
+ class: "nodii-onboarding-input",
1039
+ multiple: "multiple"
1040
+ });
1041
+ const current = Array.isArray(currentValue) ? currentValue.map(String) : [];
1042
+ for (const opt of field.options ?? []) {
1043
+ const value = optionValue(opt);
1044
+ const optionEl = el3("option", { value }, [
1045
+ optionLabel(opt)
1046
+ ]);
1047
+ if (current.includes(value)) optionEl.selected = true;
1048
+ select.appendChild(optionEl);
1049
+ }
1050
+ select.addEventListener("change", () => {
1051
+ const values = [];
1052
+ for (const o of Array.from(select.options)) {
1053
+ if (o.selected && o.value !== "") values.push(o.value);
1054
+ }
1055
+ onChange(values);
1056
+ });
1057
+ return select;
1058
+ }
1059
+ default: {
1060
+ const typeAttr = field.type === "email" || field.type === "tel" || field.type === "number" || field.type === "date" ? field.type : "text";
1061
+ const input = el3("input", {
1062
+ type: typeAttr,
1063
+ id,
1064
+ class: "nodii-onboarding-input"
1065
+ });
1066
+ if (typeof currentValue === "string") input.value = currentValue;
1067
+ if (typeof currentValue === "number") input.value = String(currentValue);
1068
+ input.addEventListener("input", () => onChange(input.value));
1069
+ return input;
1070
+ }
1071
+ }
1072
+ }
1073
+ function fieldRow(field, input, idOverride) {
1074
+ const id = idOverride ?? `nodii-f-${field.key}`;
1075
+ const labelEl = el3(
1076
+ "label",
1077
+ {
1078
+ for: id,
1079
+ class: `nodii-onboarding-label${field.required ? " required" : ""}`
1080
+ },
1081
+ [field.label]
1082
+ );
1083
+ const errorEl = el3("div", {
1084
+ class: "nodii-onboarding-field-error",
1085
+ id: `${id}-error`,
1086
+ role: "alert",
1087
+ "aria-live": "polite"
1088
+ });
1089
+ return el3("div", { class: "nodii-onboarding-field" }, [
1090
+ labelEl,
1091
+ input,
1092
+ errorEl
1093
+ ]);
1094
+ }
1095
+ function mountSignupForm(host, options) {
1096
+ injectWidgetStyles();
1097
+ const wrapper = el3("div", { class: "nodii-onboarding-widget" });
1098
+ const loadingEl = el3("div", { class: "nodii-onboarding-loading" }, [
1099
+ "Loading signup form\u2026"
1100
+ ]);
1101
+ wrapper.appendChild(loadingEl);
1102
+ host.appendChild(wrapper);
1103
+ const cache = options.cache ?? defaultSpecCache;
1104
+ let stepper = null;
1105
+ let unsubscribe = null;
1106
+ let unmounted = false;
1107
+ let siteKey = options.siteKey ?? "";
1108
+ let idempotencyKey = null;
1109
+ let lastFingerprint = null;
1110
+ let renderedStepIndex = -1;
1111
+ let renderedStale = false;
1112
+ let renderedConsentStale = false;
1113
+ let currentSpecVersion = null;
1114
+ let staleDiffSummary = "";
1115
+ const stepArea = el3("div", {});
1116
+ const staleBanner = el3("div", {
1117
+ class: "nodii-onboarding-stale-banner",
1118
+ role: "alert",
1119
+ style: "display:none"
1120
+ });
1121
+ const serverErrorEl = el3("div", {
1122
+ class: "nodii-onboarding-server-error",
1123
+ role: "alert",
1124
+ "aria-live": "assertive",
1125
+ style: "display:none"
1126
+ });
1127
+ const successEl = el3("div", {
1128
+ class: "nodii-onboarding-success",
1129
+ role: "status",
1130
+ style: "display:none"
1131
+ });
1132
+ const stepsIndicator = el3("div", { class: "nodii-onboarding-steps" });
1133
+ const form = el3("form", { novalidate: "true", "aria-label": "Sign up" });
1134
+ function setFieldErrorsInPlace(container, errors, idPrefix) {
1135
+ for (const errEl of container.querySelectorAll(
1136
+ ".nodii-onboarding-field-error"
1137
+ )) {
1138
+ errEl.textContent = "";
1139
+ }
1140
+ for (const marked of container.querySelectorAll(".has-error")) {
1141
+ marked.classList.remove("has-error");
1142
+ }
1143
+ for (const err of errors) {
1144
+ const id = idPrefix(err.path);
1145
+ const errorNode = container.querySelector(idSelector(`${id}-error`));
1146
+ if (errorNode) errorNode.textContent = err.message;
1147
+ const inputNode = container.querySelector(idSelector(id));
1148
+ if (inputNode) inputNode.classList.add("has-error");
1149
+ }
1150
+ }
1151
+ function accountIdOf(path) {
1152
+ if (path === "email") return "nodii-account-email";
1153
+ if (path === "displayName") return "nodii-account-display-name";
1154
+ if (path === "tosAccepted") return "nodii-consent-tos";
1155
+ if (path === "privacyAccepted") return "nodii-consent-privacy";
1156
+ return `nodii-account-${path}`;
1157
+ }
1158
+ function renderStepsIndicator(state) {
1159
+ while (stepsIndicator.firstChild) {
1160
+ stepsIndicator.removeChild(stepsIndicator.firstChild);
1161
+ }
1162
+ for (let i = 0; i < state.stepCount; i++) {
1163
+ stepsIndicator.appendChild(
1164
+ el3("div", {
1165
+ class: `nodii-onboarding-step-dot${i <= state.stepIndex ? " active" : ""}`
1166
+ })
1167
+ );
1168
+ }
1169
+ }
1170
+ function renderAccountSection(control, state) {
1171
+ const section = el3("div", { class: "nodii-onboarding-account-section" });
1172
+ const emailInput = el3("input", {
1173
+ type: "email",
1174
+ id: "nodii-account-email",
1175
+ class: "nodii-onboarding-input",
1176
+ autocomplete: "email"
1177
+ });
1178
+ emailInput.value = state.credentials.email;
1179
+ emailInput.addEventListener(
1180
+ "input",
1181
+ () => control.setCredentials({ email: emailInput.value })
1182
+ );
1183
+ section.appendChild(
1184
+ fieldRow(
1185
+ {
1186
+ key: "account-email",
1187
+ label: "Account email",
1188
+ required: true
1189
+ },
1190
+ emailInput,
1191
+ "nodii-account-email"
1192
+ // label for + error id match the real input id
1193
+ )
1194
+ );
1195
+ const displayNameInput = el3("input", {
1196
+ type: "text",
1197
+ id: "nodii-account-display-name",
1198
+ class: "nodii-onboarding-input",
1199
+ autocomplete: "name",
1200
+ maxlength: "200"
1201
+ });
1202
+ displayNameInput.value = state.credentials.displayName;
1203
+ displayNameInput.addEventListener(
1204
+ "input",
1205
+ () => control.setCredentials({ displayName: displayNameInput.value })
1206
+ );
1207
+ section.appendChild(
1208
+ fieldRow(
1209
+ {
1210
+ key: "account-display-name",
1211
+ label: "Your name",
1212
+ required: true
1213
+ },
1214
+ displayNameInput,
1215
+ "nodii-account-display-name"
1216
+ )
1217
+ );
1218
+ for (const [key, label, autocomplete] of [
1219
+ ["organisationName", "Organisation name", "organization"],
1220
+ ["subdomain", "Workspace subdomain", "off"]
1221
+ ]) {
1222
+ const input = el3("input", {
1223
+ type: "text",
1224
+ id: `nodii-account-${key}`,
1225
+ class: "nodii-onboarding-input",
1226
+ autocomplete,
1227
+ maxlength: key === "subdomain" ? "63" : "200"
1228
+ });
1229
+ input.value = state.credentials[key];
1230
+ input.addEventListener(
1231
+ "input",
1232
+ () => control.setCredentials({ [key]: input.value })
1233
+ );
1234
+ section.appendChild(
1235
+ fieldRow(
1236
+ { key: `account-${key}`, label, required: true },
1237
+ input,
1238
+ `nodii-account-${key}`
1239
+ )
1240
+ );
1241
+ }
1242
+ const tosCheckbox = el3("input", {
1243
+ type: "checkbox",
1244
+ id: "nodii-consent-tos"
1245
+ });
1246
+ tosCheckbox.checked = state.consent.tosAccepted;
1247
+ tosCheckbox.addEventListener(
1248
+ "change",
1249
+ () => control.setConsent({ tosAccepted: tosCheckbox.checked })
1250
+ );
1251
+ const tosLabelChildren = ["I accept the "];
1252
+ if (options.tosUrl) {
1253
+ tosLabelChildren.push(
1254
+ el3(
1255
+ "a",
1256
+ {
1257
+ href: options.tosUrl,
1258
+ target: "_blank",
1259
+ rel: "noopener noreferrer"
1260
+ },
1261
+ ["Terms of Service"]
1262
+ )
1263
+ );
1264
+ } else {
1265
+ tosLabelChildren.push("Terms of Service");
1266
+ }
1267
+ section.appendChild(
1268
+ el3("div", { class: "nodii-onboarding-consent-row" }, [
1269
+ tosCheckbox,
1270
+ el3("label", { for: "nodii-consent-tos" }, tosLabelChildren)
1271
+ ])
1272
+ );
1273
+ section.appendChild(
1274
+ el3("div", {
1275
+ class: "nodii-onboarding-field-error",
1276
+ id: "nodii-consent-tos-error",
1277
+ role: "alert"
1278
+ })
1279
+ );
1280
+ const privacyCheckbox = el3("input", {
1281
+ type: "checkbox",
1282
+ id: "nodii-consent-privacy"
1283
+ });
1284
+ privacyCheckbox.checked = state.consent.privacyAccepted;
1285
+ privacyCheckbox.addEventListener(
1286
+ "change",
1287
+ () => control.setConsent({ privacyAccepted: privacyCheckbox.checked })
1288
+ );
1289
+ const privacyLabelChildren = ["I accept the "];
1290
+ if (options.privacyUrl) {
1291
+ privacyLabelChildren.push(
1292
+ el3(
1293
+ "a",
1294
+ {
1295
+ href: options.privacyUrl,
1296
+ target: "_blank",
1297
+ rel: "noopener noreferrer"
1298
+ },
1299
+ ["Privacy Policy"]
1300
+ )
1301
+ );
1302
+ } else {
1303
+ privacyLabelChildren.push("Privacy Policy");
1304
+ }
1305
+ section.appendChild(
1306
+ el3("div", { class: "nodii-onboarding-consent-row" }, [
1307
+ privacyCheckbox,
1308
+ el3("label", { for: "nodii-consent-privacy" }, privacyLabelChildren)
1309
+ ])
1310
+ );
1311
+ section.appendChild(
1312
+ el3("div", {
1313
+ class: "nodii-onboarding-field-error",
1314
+ id: "nodii-consent-privacy-error",
1315
+ role: "alert"
1316
+ })
1317
+ );
1318
+ return section;
1319
+ }
1320
+ function renderStep(control, state) {
1321
+ while (stepArea.firstChild) stepArea.removeChild(stepArea.firstChild);
1322
+ const step = state.currentStep;
1323
+ if (step) {
1324
+ stepArea.appendChild(
1325
+ el3("h3", { class: "nodii-onboarding-stage-title" }, [
1326
+ STAGE_TITLES[step.stage] ?? step.stage
1327
+ ])
1328
+ );
1329
+ for (const field of step.fields) {
1330
+ const input = buildFieldInput(
1331
+ field,
1332
+ state.answers[field.key],
1333
+ (value) => control.setAnswer(field.key, value)
1334
+ );
1335
+ stepArea.appendChild(fieldRow(field, input));
1336
+ }
1337
+ }
1338
+ const isLast = control.isLastStep();
1339
+ if (isLast) {
1340
+ if (!control.consent.isEmpty()) {
1341
+ const consentStage = el3("div", {
1342
+ class: "nodii-onboarding-consent-stage"
1343
+ });
1344
+ renderConsentStep(
1345
+ consentStage,
1346
+ control.consent,
1347
+ state.consentState,
1348
+ state.staleConsentKeys,
1349
+ { fetch: options.fetch, timeoutMs: options.timeoutMs }
1350
+ );
1351
+ stepArea.appendChild(consentStage);
1352
+ }
1353
+ stepArea.appendChild(renderAccountSection(control, state));
1354
+ }
1355
+ const nav = el3("div", { class: "nodii-onboarding-nav" });
1356
+ if (!control.isFirstStep()) {
1357
+ const backBtn = el3(
1358
+ "button",
1359
+ { type: "button", class: "nodii-onboarding-back-btn" },
1360
+ ["Back"]
1361
+ );
1362
+ backBtn.addEventListener("click", () => control.back());
1363
+ nav.appendChild(backBtn);
1364
+ }
1365
+ const forwardBtn = el3(
1366
+ "button",
1367
+ {
1368
+ type: isLast ? "submit" : "button",
1369
+ class: "nodii-onboarding-submit-btn",
1370
+ id: "nodii-signup-forward"
1371
+ },
1372
+ [isLast ? options.submitLabel ?? "Create account" : "Next"]
1373
+ );
1374
+ if (!isLast) {
1375
+ forwardBtn.addEventListener("click", () => control.next());
1376
+ }
1377
+ nav.appendChild(forwardBtn);
1378
+ stepArea.appendChild(nav);
1379
+ }
1380
+ async function handleSpecStale(control) {
1381
+ const prior = currentSpecVersion !== null ? cache.get(options.onboarder, currentSpecVersion) : void 0;
1382
+ const fresh = await fetchOnboardingSpec({
1383
+ baseUrl: options.baseUrl,
1384
+ onboarder: options.onboarder,
1385
+ fetch: options.fetch,
1386
+ timeoutMs: options.timeoutMs,
1387
+ cache
1388
+ });
1389
+ if (unmounted) return;
1390
+ if (prior) {
1391
+ const diff = diffSpecFields(prior, fresh);
1392
+ const parts = [];
1393
+ if (diff.added.length) parts.push(`added: ${diff.added.join(", ")}`);
1394
+ if (diff.removed.length)
1395
+ parts.push(`removed: ${diff.removed.join(", ")}`);
1396
+ if (diff.changed.length)
1397
+ parts.push(`changed: ${diff.changed.join(", ")}`);
1398
+ staleDiffSummary = parts.length ? ` (${parts.join("; ")})` : "";
1399
+ } else {
1400
+ staleDiffSummary = "";
1401
+ }
1402
+ siteKey = options.siteKey ?? fresh.botChallenge?.siteKey ?? "";
1403
+ currentSpecVersion = fresh.specVersion;
1404
+ control.applyNewSpec(fresh);
1405
+ idempotencyKey = null;
1406
+ lastFingerprint = null;
1407
+ }
1408
+ async function handleConsentStale(control) {
1409
+ const fresh = await fetchOnboardingSpec({
1410
+ baseUrl: options.baseUrl,
1411
+ onboarder: options.onboarder,
1412
+ fetch: options.fetch,
1413
+ timeoutMs: options.timeoutMs,
1414
+ cache
1415
+ });
1416
+ if (unmounted) return;
1417
+ siteKey = options.siteKey ?? fresh.botChallenge?.siteKey ?? "";
1418
+ currentSpecVersion = fresh.specVersion;
1419
+ control.applyNewConsentPack(fresh);
1420
+ idempotencyKey = null;
1421
+ lastFingerprint = null;
1422
+ }
1423
+ async function handleSubmit(control) {
1424
+ if (!control.validateAll()) return;
1425
+ control.acknowledgeSpecStale();
1426
+ control.setSubmitting();
1427
+ const body = control.buildSubmitPayload();
1428
+ const fingerprint = computeSignupFingerprint(body);
1429
+ if (idempotencyKey === null || fingerprint !== lastFingerprint) {
1430
+ idempotencyKey = generateIdempotencyKey();
1431
+ lastFingerprint = fingerprint;
1432
+ }
1433
+ let firstToken;
1434
+ if (siteKey) {
1435
+ try {
1436
+ firstToken = await executeRecaptcha(siteKey, "signup_submit", {
1437
+ timeoutMs: options.timeoutMs
1438
+ });
1439
+ } catch (recaptchaErr) {
1440
+ control.setError("Could not load bot protection. Please try again.");
1441
+ options.onError?.(recaptchaErr);
1442
+ return;
1443
+ }
1444
+ }
1445
+ let usedFirst = false;
1446
+ const tokenProvider = async () => {
1447
+ if (!usedFirst) {
1448
+ usedFirst = true;
1449
+ return firstToken;
1450
+ }
1451
+ return executeRecaptcha(siteKey, "signup_submit", {
1452
+ timeoutMs: options.timeoutMs
1453
+ });
1454
+ };
1455
+ try {
1456
+ const result = await submitSignup(body, {
1457
+ baseUrl: options.baseUrl,
1458
+ ...siteKey ? { botToken: tokenProvider } : {},
1459
+ idempotencyKey,
1460
+ fetch: options.fetch,
1461
+ timeoutMs: options.timeoutMs
1462
+ });
1463
+ idempotencyKey = null;
1464
+ lastFingerprint = null;
1465
+ control.setSuccess(result.terminalStage);
1466
+ options.onSuccess?.(result);
1467
+ } catch (err) {
1468
+ if (err instanceof OnboardingSpecStaleError) {
1469
+ try {
1470
+ await handleSpecStale(control);
1471
+ } catch (refetchErr) {
1472
+ control.setError(
1473
+ "The signup form changed and could not be reloaded. Please refresh the page."
1474
+ );
1475
+ options.onError?.(refetchErr);
1476
+ return;
1477
+ }
1478
+ options.onError?.(err);
1479
+ } else if (err instanceof OnboardingConsentStaleError) {
1480
+ try {
1481
+ await handleConsentStale(control);
1482
+ } catch (refetchErr) {
1483
+ control.setError(
1484
+ "The legal documents changed and could not be reloaded. Please refresh the page."
1485
+ );
1486
+ options.onError?.(refetchErr);
1487
+ return;
1488
+ }
1489
+ options.onError?.(err);
1490
+ } else if (err instanceof OnboardingConsentIncompleteError) {
1491
+ control.validateAll();
1492
+ control.setError("Please accept all required documents to continue.");
1493
+ options.onError?.(err);
1494
+ } else if (err instanceof OnboardingBotChallengeError) {
1495
+ control.setError(
1496
+ "Bot protection check failed. Please refresh the page and try again."
1497
+ );
1498
+ options.onError?.(err);
1499
+ } else if (err instanceof OnboardingRateLimitError) {
1500
+ control.setError(
1501
+ err.retryAfterSeconds !== null ? `Too many requests. Please wait ${err.retryAfterSeconds}s.` : "Too many requests. Please wait a moment."
1502
+ );
1503
+ options.onError?.(err);
1504
+ } else {
1505
+ control.setError("Something went wrong. Please try again in a moment.");
1506
+ options.onError?.(err);
1507
+ }
1508
+ }
1509
+ }
1510
+ function onStateChange(control, state) {
1511
+ if (state.stepIndex !== renderedStepIndex || state.specStale !== renderedStale || state.consentStale !== renderedConsentStale) {
1512
+ renderedStepIndex = state.stepIndex;
1513
+ renderedStale = state.specStale;
1514
+ renderedConsentStale = state.consentStale;
1515
+ renderStep(control, state);
1516
+ renderStepsIndicator(state);
1517
+ }
1518
+ staleBanner.style.display = state.specStale || state.consentStale ? "" : "none";
1519
+ staleBanner.textContent = state.specStale ? `This form was just updated by the provider${staleDiffSummary}. Please review your answers below and submit again.` : state.consentStale ? "One or more legal documents were updated. Please review and re-accept the updated documents below, then submit again." : "";
1520
+ setFieldErrorsInPlace(
1521
+ stepArea,
1522
+ state.errors,
1523
+ (path) => `nodii-f-${path.split(".")[0] ?? path}`
1524
+ );
1525
+ for (const err of state.accountErrors) {
1526
+ const id = accountIdOf(err.path.split(".")[0] ?? err.path);
1527
+ const errorNode = stepArea.querySelector(idSelector(`${id}-error`));
1528
+ if (errorNode) errorNode.textContent = err.message;
1529
+ const inputNode = stepArea.querySelector(idSelector(id));
1530
+ if (inputNode) inputNode.classList.add("has-error");
1531
+ }
1532
+ const consentStage = stepArea.querySelector(
1533
+ ".nodii-onboarding-consent-stage"
1534
+ );
1535
+ if (consentStage) {
1536
+ updateConsentStep(consentStage, state.consentState, state.consentErrors);
1537
+ }
1538
+ if (state.serverError) {
1539
+ serverErrorEl.textContent = state.serverError;
1540
+ serverErrorEl.style.display = "";
1541
+ } else {
1542
+ serverErrorEl.style.display = "none";
1543
+ }
1544
+ const forwardBtn = stepArea.querySelector(
1545
+ "#nodii-signup-forward"
1546
+ );
1547
+ if (forwardBtn) {
1548
+ forwardBtn.disabled = state.status === "submitting";
1549
+ }
1550
+ if (state.status === "success") {
1551
+ const message = state.terminalStage === "active" ? "Your account is ready \u2014 you're all set!" : state.terminalStage === "inactive" ? "Your account was created. Complete verification to activate it." : "Thanks for signing up! Check your email to verify your account.";
1552
+ successEl.textContent = message;
1553
+ successEl.style.display = "";
1554
+ form.style.display = "none";
1555
+ } else {
1556
+ successEl.style.display = "none";
1557
+ form.style.display = "";
1558
+ }
1559
+ }
1560
+ void (async () => {
1561
+ let spec;
1562
+ try {
1563
+ spec = await fetchOnboardingSpec({
1564
+ baseUrl: options.baseUrl,
1565
+ onboarder: options.onboarder,
1566
+ fetch: options.fetch,
1567
+ timeoutMs: options.timeoutMs,
1568
+ cache,
1569
+ // Reuse a spec already loaded for this onboarder this session (a
1570
+ // remount into the same page shouldn't re-hit the network). A 409 on
1571
+ // submit is the authoritative staleness signal that triggers refetch.
1572
+ preferCached: true
1573
+ });
1574
+ } catch (err) {
1575
+ if (unmounted) return;
1576
+ loadingEl.textContent = "The signup form could not be loaded. Please refresh the page.";
1577
+ options.onError?.(err);
1578
+ return;
1579
+ }
1580
+ if (unmounted) return;
1581
+ siteKey = options.siteKey ?? spec.botChallenge?.siteKey ?? "";
1582
+ currentSpecVersion = spec.specVersion;
1583
+ const control = createSignupStepper(spec);
1584
+ stepper = control;
1585
+ form.addEventListener("submit", (event) => {
1586
+ event.preventDefault();
1587
+ void handleSubmit(control);
1588
+ });
1589
+ form.appendChild(stepsIndicator);
1590
+ form.appendChild(staleBanner);
1591
+ form.appendChild(stepArea);
1592
+ form.appendChild(serverErrorEl);
1593
+ wrapper.removeChild(loadingEl);
1594
+ wrapper.appendChild(successEl);
1595
+ wrapper.appendChild(form);
1596
+ unsubscribe = control.subscribe((state) => onStateChange(control, state));
1597
+ onStateChange(control, control.getState());
1598
+ options.onReady?.(spec);
1599
+ })();
1600
+ return {
1601
+ unmount() {
1602
+ unmounted = true;
1603
+ unsubscribe?.();
1604
+ if (wrapper.parentNode === host) host.removeChild(wrapper);
1605
+ },
1606
+ reset() {
1607
+ idempotencyKey = null;
1608
+ lastFingerprint = null;
1609
+ renderedStepIndex = -1;
1610
+ renderedStale = false;
1611
+ if (stepper) {
1612
+ stepper.reset();
1613
+ } else {
1614
+ form.style.display = "";
1615
+ successEl.style.display = "none";
1616
+ }
1617
+ }
1618
+ };
1619
+ }
1620
+
1621
+ // src/widget/index.ts
1622
+ var OnboardingWidget = {
1623
+ /**
1624
+ * Mount the onboarding widget into a host element.
1625
+ *
1626
+ * @param el - The DOM element to render into.
1627
+ * @param options - Widget configuration. `form: "contact"` renders the
1628
+ * fixed contact form (Phase 1); `form: "signup"` fetches the D540 spec
1629
+ * and renders the dynamic stepper (Phase 2 — siteKey comes from the
1630
+ * spec's botChallenge unless overridden).
1631
+ * @returns A mounted widget control (unmount / reset).
1632
+ *
1633
+ * @example
1634
+ * ```ts
1635
+ * import { OnboardingWidget } from "@nodii/onboarding/widget";
1636
+ *
1637
+ * OnboardingWidget.mount(document.getElementById("signup-form")!, {
1638
+ * onboarder: "acme-corp",
1639
+ * form: "signup",
1640
+ * baseUrl: "https://api.acme.nucleus-cloud.in",
1641
+ * onSuccess: (r) => console.log("landed at", r.terminalStage),
1642
+ * });
1643
+ * ```
1644
+ */
1645
+ mount(el4, options) {
1646
+ if (options.form === "contact") {
1647
+ return mountContactForm(el4, options);
1648
+ }
1649
+ if (options.form === "signup") {
1650
+ return mountSignupForm(el4, options);
1651
+ }
1652
+ throw new Error(
1653
+ `OnboardingWidget.mount: unknown form "${options.form}". Supported: "contact", "signup".`
1654
+ );
1655
+ }
1656
+ };
1657
+
1658
+ export { DEFAULT_WIDGET_CSS, OnboardingWidget, consentDocElementId, cssEscape, idSelector, injectWidgetStyles, mountContactForm, mountSignupForm, renderConsentStep, updateConsentStep };
1659
+ //# sourceMappingURL=chunk-IPAKPLIF.js.map
1660
+ //# sourceMappingURL=chunk-IPAKPLIF.js.map