@nodii/onboarding 0.0.1 → 0.1.0-dev.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 (50) hide show
  1. package/dist/chunk-34BQX2KR.js +164 -0
  2. package/dist/chunk-34BQX2KR.js.map +1 -0
  3. package/dist/chunk-4SWOVHZ2.js +434 -0
  4. package/dist/chunk-4SWOVHZ2.js.map +1 -0
  5. package/dist/chunk-7NS7ENJV.js +232 -0
  6. package/dist/chunk-7NS7ENJV.js.map +1 -0
  7. package/dist/chunk-AFCJLUPK.js +92 -0
  8. package/dist/chunk-AFCJLUPK.js.map +1 -0
  9. package/dist/chunk-AW7W3HAC.js +154 -0
  10. package/dist/chunk-AW7W3HAC.js.map +1 -0
  11. package/dist/chunk-BIFANV7M.js +22 -0
  12. package/dist/chunk-BIFANV7M.js.map +1 -0
  13. package/dist/chunk-BQCTDU5V.js +327 -0
  14. package/dist/chunk-BQCTDU5V.js.map +1 -0
  15. package/dist/chunk-JFIONONR.js +3 -0
  16. package/dist/chunk-JFIONONR.js.map +1 -0
  17. package/dist/chunk-SWMIIUXR.js +1162 -0
  18. package/dist/chunk-SWMIIUXR.js.map +1 -0
  19. package/dist/chunk-VH6FWDWI.js +3 -0
  20. package/dist/chunk-VH6FWDWI.js.map +1 -0
  21. package/dist/chunk-Z4PA6C2Z.js +3 -0
  22. package/dist/chunk-Z4PA6C2Z.js.map +1 -0
  23. package/dist/chunk-ZDY2I6KY.js +3 -0
  24. package/dist/chunk-ZDY2I6KY.js.map +1 -0
  25. package/dist/dynamic/index.d.ts +184 -0
  26. package/dist/dynamic/index.js +4 -0
  27. package/dist/dynamic/index.js.map +1 -0
  28. package/dist/form/index.d.ts +64 -0
  29. package/dist/form/index.js +5 -0
  30. package/dist/form/index.js.map +1 -0
  31. package/dist/index.d.ts +10 -2
  32. package/dist/index.js +14 -2
  33. package/dist/index.js.map +1 -0
  34. package/dist/model-IEyDI54w.d.ts +487 -0
  35. package/dist/recaptcha/index.d.ts +102 -0
  36. package/dist/recaptcha/index.js +4 -0
  37. package/dist/recaptcha/index.js.map +1 -0
  38. package/dist/schema-TfyBU6JW.d.ts +107 -0
  39. package/dist/signup-client-Bl6eUrxR.d.ts +259 -0
  40. package/dist/spec/index.d.ts +88 -0
  41. package/dist/spec/index.js +4 -0
  42. package/dist/spec/index.js.map +1 -0
  43. package/dist/submit/index.d.ts +106 -0
  44. package/dist/submit/index.js +6 -0
  45. package/dist/submit/index.js.map +1 -0
  46. package/dist/widget/index.d.ts +219 -0
  47. package/dist/widget/index.js +10 -0
  48. package/dist/widget/index.js.map +1 -0
  49. package/package.json +77 -6
  50. package/README.md +0 -5
@@ -0,0 +1,1162 @@
1
+ import { createContactFormState } from './chunk-AFCJLUPK.js';
2
+ import { defaultSpecCache, fetchOnboardingSpec, diffSpecFields } from './chunk-34BQX2KR.js';
3
+ import { createSignupStepper, optionValue } from './chunk-4SWOVHZ2.js';
4
+ import { computePayloadFingerprint, generateIdempotencyKey, submitLead, computeSignupFingerprint, submitSignup } from './chunk-BQCTDU5V.js';
5
+ import { OnboardingBotChallengeError, OnboardingRateLimitError, OnboardingSpecStaleError } from './chunk-AW7W3HAC.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
+ function injectWidgetStyles() {
248
+ if (typeof document === "undefined") return;
249
+ const styleId = "__nodii_onboarding_styles__";
250
+ if (document.getElementById(styleId)) return;
251
+ const style = document.createElement("style");
252
+ style.id = styleId;
253
+ style.textContent = DEFAULT_WIDGET_CSS;
254
+ document.head.appendChild(style);
255
+ }
256
+
257
+ // src/widget/contact-form.ts
258
+ function el(tag, attrs = {}, children = []) {
259
+ const element = document.createElement(tag);
260
+ for (const [key, value] of Object.entries(attrs)) {
261
+ element.setAttribute(key, value);
262
+ }
263
+ for (const child of children) {
264
+ if (typeof child === "string") {
265
+ element.appendChild(document.createTextNode(child));
266
+ } else {
267
+ element.appendChild(child);
268
+ }
269
+ }
270
+ return element;
271
+ }
272
+ function createFieldEl(id, label, input, required) {
273
+ const labelEl = el(
274
+ "label",
275
+ { for: id, class: `nodii-onboarding-label${required ? " required" : ""}` },
276
+ [label]
277
+ );
278
+ const errorEl = el("div", {
279
+ class: "nodii-onboarding-field-error",
280
+ id: `${id}-error`,
281
+ role: "alert",
282
+ "aria-live": "polite"
283
+ });
284
+ const field = el("div", { class: "nodii-onboarding-field" }, [
285
+ labelEl,
286
+ input,
287
+ errorEl
288
+ ]);
289
+ return field;
290
+ }
291
+ function setFieldError(container, fieldId, message) {
292
+ const errorEl = container.querySelector(idSelector(`${fieldId}-error`));
293
+ if (errorEl) errorEl.textContent = message;
294
+ const inputEl = container.querySelector(idSelector(fieldId));
295
+ if (inputEl) inputEl.classList.add("has-error");
296
+ }
297
+ function clearFieldErrors(container) {
298
+ for (const el3 of container.querySelectorAll(
299
+ ".nodii-onboarding-field-error"
300
+ )) {
301
+ el3.textContent = "";
302
+ }
303
+ for (const el3 of container.querySelectorAll(".has-error")) {
304
+ el3.classList.remove("has-error");
305
+ }
306
+ }
307
+ function applyErrors(container, errors) {
308
+ clearFieldErrors(container);
309
+ for (const err of errors) {
310
+ setFieldError(container, `nodii-${err.path}`, err.message);
311
+ }
312
+ }
313
+ function mountContactForm(host, options) {
314
+ injectWidgetStyles();
315
+ const formControl = createContactFormState();
316
+ loadRecaptcha(options.siteKey).catch(() => {
317
+ });
318
+ const wrapper = el("div", { class: "nodii-onboarding-widget" });
319
+ const nameInput = el("input", {
320
+ type: "text",
321
+ id: "nodii-name",
322
+ name: "name",
323
+ class: "nodii-onboarding-input",
324
+ placeholder: "Your name",
325
+ autocomplete: "name",
326
+ maxlength: "200",
327
+ required: "required"
328
+ });
329
+ const emailInput = el("input", {
330
+ type: "email",
331
+ id: "nodii-email",
332
+ name: "email",
333
+ class: "nodii-onboarding-input",
334
+ placeholder: "you@company.com",
335
+ autocomplete: "email",
336
+ required: "required"
337
+ });
338
+ const phoneInput = el("input", {
339
+ type: "tel",
340
+ id: "nodii-phone",
341
+ name: "phone",
342
+ class: "nodii-onboarding-input",
343
+ placeholder: "+1 555 000 0000",
344
+ autocomplete: "tel"
345
+ });
346
+ const messageInput = el("textarea", {
347
+ id: "nodii-message",
348
+ name: "message",
349
+ class: "nodii-onboarding-textarea",
350
+ placeholder: "How can we help?",
351
+ maxlength: "4000",
352
+ rows: "4"
353
+ });
354
+ const serverErrorEl = el("div", {
355
+ class: "nodii-onboarding-server-error",
356
+ role: "alert",
357
+ "aria-live": "assertive",
358
+ style: "display:none"
359
+ });
360
+ const successEl = el(
361
+ "div",
362
+ {
363
+ class: "nodii-onboarding-success",
364
+ role: "status",
365
+ style: "display:none"
366
+ },
367
+ [options.successMessage ?? "Thank you! We'll be in touch shortly."]
368
+ );
369
+ const submitBtn = el(
370
+ "button",
371
+ {
372
+ type: "submit",
373
+ class: "nodii-onboarding-submit-btn"
374
+ },
375
+ [options.submitLabel ?? "Get in touch"]
376
+ );
377
+ const recaptchaNotice = el(
378
+ "p",
379
+ { class: "nodii-onboarding-recaptcha-notice" },
380
+ [
381
+ "Protected by reCAPTCHA. ",
382
+ Object.assign(
383
+ el("a", {
384
+ href: "https://policies.google.com/privacy",
385
+ target: "_blank",
386
+ rel: "noopener noreferrer"
387
+ }),
388
+ { textContent: "Privacy" }
389
+ ),
390
+ " & ",
391
+ Object.assign(
392
+ el("a", {
393
+ href: "https://policies.google.com/terms",
394
+ target: "_blank",
395
+ rel: "noopener noreferrer"
396
+ }),
397
+ { textContent: "Terms" }
398
+ )
399
+ ]
400
+ );
401
+ const form = el("form", {
402
+ novalidate: "true",
403
+ "aria-label": "Contact us"
404
+ });
405
+ form.appendChild(createFieldEl("nodii-name", "Name", nameInput, true));
406
+ form.appendChild(createFieldEl("nodii-email", "Email", emailInput, true));
407
+ form.appendChild(
408
+ createFieldEl("nodii-phone", "Phone (optional)", phoneInput, false)
409
+ );
410
+ form.appendChild(
411
+ createFieldEl("nodii-message", "Message (optional)", messageInput, false)
412
+ );
413
+ form.appendChild(serverErrorEl);
414
+ form.appendChild(submitBtn);
415
+ form.appendChild(recaptchaNotice);
416
+ wrapper.appendChild(successEl);
417
+ wrapper.appendChild(form);
418
+ host.appendChild(wrapper);
419
+ nameInput.addEventListener("input", () => {
420
+ formControl.setValue("name", nameInput.value);
421
+ });
422
+ emailInput.addEventListener("input", () => {
423
+ formControl.setValue("email", emailInput.value);
424
+ });
425
+ phoneInput.addEventListener("input", () => {
426
+ formControl.setValue("phone", phoneInput.value || void 0);
427
+ });
428
+ messageInput.addEventListener("input", () => {
429
+ formControl.setValue("message", messageInput.value || void 0);
430
+ });
431
+ const submitLabel = options.submitLabel ?? "Get in touch";
432
+ function renderSubmitButton(submitting) {
433
+ while (submitBtn.firstChild) {
434
+ submitBtn.removeChild(submitBtn.firstChild);
435
+ }
436
+ if (submitting) {
437
+ const spinner = document.createElement("span");
438
+ spinner.className = "nodii-onboarding-spinner";
439
+ submitBtn.appendChild(spinner);
440
+ submitBtn.appendChild(document.createTextNode("Sending\u2026"));
441
+ } else {
442
+ submitBtn.appendChild(document.createTextNode(submitLabel));
443
+ }
444
+ }
445
+ const unsubscribe = formControl.subscribe((state) => {
446
+ submitBtn.disabled = state.status === "submitting";
447
+ renderSubmitButton(state.status === "submitting");
448
+ applyErrors(wrapper, state.errors);
449
+ if (state.serverError) {
450
+ serverErrorEl.textContent = state.serverError;
451
+ serverErrorEl.style.display = "";
452
+ } else {
453
+ serverErrorEl.style.display = "none";
454
+ }
455
+ if (state.status === "success") {
456
+ successEl.style.display = "";
457
+ form.style.display = "none";
458
+ }
459
+ });
460
+ let idempotencyKey = null;
461
+ let lastFingerprint = null;
462
+ form.addEventListener("submit", async (event) => {
463
+ event.preventDefault();
464
+ formControl.setValue("name", nameInput.value);
465
+ formControl.setValue("email", emailInput.value);
466
+ formControl.setValue("phone", phoneInput.value || void 0);
467
+ formControl.setValue("message", messageInput.value || void 0);
468
+ const valid = formControl.validate();
469
+ if (!valid) return;
470
+ formControl.setSubmitting();
471
+ const values = formControl.getState().values;
472
+ const fingerprint = computePayloadFingerprint(values);
473
+ if (idempotencyKey === null || fingerprint !== lastFingerprint) {
474
+ idempotencyKey = generateIdempotencyKey();
475
+ lastFingerprint = fingerprint;
476
+ }
477
+ let firstToken;
478
+ try {
479
+ firstToken = await executeRecaptcha(options.siteKey, "contact_submit", {
480
+ timeoutMs: options.timeoutMs
481
+ });
482
+ } catch (recaptchaErr) {
483
+ formControl.setError("Could not load bot protection. Please try again.");
484
+ options.onError?.(recaptchaErr);
485
+ return;
486
+ }
487
+ let usedFirst = false;
488
+ const tokenProvider = async () => {
489
+ if (!usedFirst) {
490
+ usedFirst = true;
491
+ return firstToken;
492
+ }
493
+ return executeRecaptcha(options.siteKey, "contact_submit", {
494
+ timeoutMs: options.timeoutMs
495
+ });
496
+ };
497
+ try {
498
+ await submitLead(values, {
499
+ baseUrl: options.baseUrl,
500
+ botToken: tokenProvider,
501
+ idempotencyKey,
502
+ fetch: options.fetch,
503
+ timeoutMs: options.timeoutMs
504
+ });
505
+ idempotencyKey = null;
506
+ lastFingerprint = null;
507
+ formControl.setSuccess();
508
+ options.onSuccess?.();
509
+ } catch (err) {
510
+ if (err instanceof OnboardingBotChallengeError) {
511
+ formControl.setError(
512
+ "Bot protection check failed. Please refresh the page and try again."
513
+ );
514
+ options.onError?.(err);
515
+ } else if (err instanceof OnboardingRateLimitError) {
516
+ const msg = err.retryAfterSeconds !== null ? `Too many requests. Please wait ${err.retryAfterSeconds}s.` : "Too many requests. Please wait a moment.";
517
+ formControl.setError(msg);
518
+ options.onError?.(err);
519
+ } else {
520
+ formControl.setError(
521
+ "Something went wrong. Please try again in a moment."
522
+ );
523
+ options.onError?.(err);
524
+ }
525
+ }
526
+ });
527
+ return {
528
+ unmount() {
529
+ unsubscribe();
530
+ host.removeChild(wrapper);
531
+ },
532
+ reset() {
533
+ idempotencyKey = null;
534
+ lastFingerprint = null;
535
+ formControl.reset();
536
+ form.style.display = "";
537
+ successEl.style.display = "none";
538
+ nameInput.value = "";
539
+ emailInput.value = "";
540
+ phoneInput.value = "";
541
+ messageInput.value = "";
542
+ }
543
+ };
544
+ }
545
+
546
+ // src/widget/signup-form.ts
547
+ var STAGE_TITLES = {
548
+ lead: "About you",
549
+ prospect: "Your business",
550
+ activation: "Choose your services"
551
+ };
552
+ function el2(tag, attrs = {}, children = []) {
553
+ const element = document.createElement(tag);
554
+ for (const [key, value] of Object.entries(attrs)) {
555
+ element.setAttribute(key, value);
556
+ }
557
+ for (const child of children) {
558
+ if (typeof child === "string") {
559
+ element.appendChild(document.createTextNode(child));
560
+ } else {
561
+ element.appendChild(child);
562
+ }
563
+ }
564
+ return element;
565
+ }
566
+ function optionLabel(option) {
567
+ if (option !== null && typeof option === "object") {
568
+ const o = option;
569
+ if (typeof o["label"] === "string") return o["label"];
570
+ }
571
+ return optionValue(option);
572
+ }
573
+ function buildFieldInput(field, currentValue, onChange) {
574
+ const id = `nodii-f-${field.key}`;
575
+ switch (field.type) {
576
+ case "textarea": {
577
+ const input = el2("textarea", {
578
+ id,
579
+ class: "nodii-onboarding-textarea",
580
+ rows: "4"
581
+ });
582
+ if (typeof currentValue === "string") input.value = currentValue;
583
+ input.addEventListener("input", () => onChange(input.value));
584
+ return input;
585
+ }
586
+ case "checkbox":
587
+ case "boolean": {
588
+ const input = el2("input", {
589
+ type: "checkbox",
590
+ id
591
+ });
592
+ input.checked = currentValue === true;
593
+ input.addEventListener("change", () => onChange(input.checked));
594
+ return input;
595
+ }
596
+ case "select":
597
+ case "radio": {
598
+ const select = el2("select", {
599
+ id,
600
+ class: "nodii-onboarding-input"
601
+ });
602
+ select.appendChild(el2("option", { value: "" }, ["\u2014 select \u2014"]));
603
+ for (const opt of field.options ?? []) {
604
+ const value = optionValue(opt);
605
+ const optionEl = el2("option", { value }, [optionLabel(opt)]);
606
+ select.appendChild(optionEl);
607
+ }
608
+ if (typeof currentValue === "string") select.value = currentValue;
609
+ select.addEventListener("change", () => onChange(select.value));
610
+ return select;
611
+ }
612
+ case "multiselect": {
613
+ const select = el2("select", {
614
+ id,
615
+ class: "nodii-onboarding-input",
616
+ multiple: "multiple"
617
+ });
618
+ const current = Array.isArray(currentValue) ? currentValue.map(String) : [];
619
+ for (const opt of field.options ?? []) {
620
+ const value = optionValue(opt);
621
+ const optionEl = el2("option", { value }, [
622
+ optionLabel(opt)
623
+ ]);
624
+ if (current.includes(value)) optionEl.selected = true;
625
+ select.appendChild(optionEl);
626
+ }
627
+ select.addEventListener("change", () => {
628
+ const values = [];
629
+ for (const o of Array.from(select.options)) {
630
+ if (o.selected && o.value !== "") values.push(o.value);
631
+ }
632
+ onChange(values);
633
+ });
634
+ return select;
635
+ }
636
+ default: {
637
+ const typeAttr = field.type === "email" || field.type === "tel" || field.type === "number" || field.type === "date" ? field.type : "text";
638
+ const input = el2("input", {
639
+ type: typeAttr,
640
+ id,
641
+ class: "nodii-onboarding-input"
642
+ });
643
+ if (typeof currentValue === "string") input.value = currentValue;
644
+ if (typeof currentValue === "number") input.value = String(currentValue);
645
+ input.addEventListener("input", () => onChange(input.value));
646
+ return input;
647
+ }
648
+ }
649
+ }
650
+ function fieldRow(field, input, idOverride) {
651
+ const id = idOverride ?? `nodii-f-${field.key}`;
652
+ const labelEl = el2(
653
+ "label",
654
+ {
655
+ for: id,
656
+ class: `nodii-onboarding-label${field.required ? " required" : ""}`
657
+ },
658
+ [field.label]
659
+ );
660
+ const errorEl = el2("div", {
661
+ class: "nodii-onboarding-field-error",
662
+ id: `${id}-error`,
663
+ role: "alert",
664
+ "aria-live": "polite"
665
+ });
666
+ return el2("div", { class: "nodii-onboarding-field" }, [
667
+ labelEl,
668
+ input,
669
+ errorEl
670
+ ]);
671
+ }
672
+ function mountSignupForm(host, options) {
673
+ injectWidgetStyles();
674
+ const wrapper = el2("div", { class: "nodii-onboarding-widget" });
675
+ const loadingEl = el2("div", { class: "nodii-onboarding-loading" }, [
676
+ "Loading signup form\u2026"
677
+ ]);
678
+ wrapper.appendChild(loadingEl);
679
+ host.appendChild(wrapper);
680
+ const cache = options.cache ?? defaultSpecCache;
681
+ let stepper = null;
682
+ let unsubscribe = null;
683
+ let unmounted = false;
684
+ let siteKey = options.siteKey ?? "";
685
+ let idempotencyKey = null;
686
+ let lastFingerprint = null;
687
+ let renderedStepIndex = -1;
688
+ let renderedStale = false;
689
+ let currentSpecVersion = null;
690
+ let staleDiffSummary = "";
691
+ const stepArea = el2("div", {});
692
+ const staleBanner = el2("div", {
693
+ class: "nodii-onboarding-stale-banner",
694
+ role: "alert",
695
+ style: "display:none"
696
+ });
697
+ const serverErrorEl = el2("div", {
698
+ class: "nodii-onboarding-server-error",
699
+ role: "alert",
700
+ "aria-live": "assertive",
701
+ style: "display:none"
702
+ });
703
+ const successEl = el2("div", {
704
+ class: "nodii-onboarding-success",
705
+ role: "status",
706
+ style: "display:none"
707
+ });
708
+ const stepsIndicator = el2("div", { class: "nodii-onboarding-steps" });
709
+ const form = el2("form", { novalidate: "true", "aria-label": "Sign up" });
710
+ function setFieldErrorsInPlace(container, errors, idPrefix) {
711
+ for (const errEl of container.querySelectorAll(
712
+ ".nodii-onboarding-field-error"
713
+ )) {
714
+ errEl.textContent = "";
715
+ }
716
+ for (const marked of container.querySelectorAll(".has-error")) {
717
+ marked.classList.remove("has-error");
718
+ }
719
+ for (const err of errors) {
720
+ const id = idPrefix(err.path);
721
+ const errorNode = container.querySelector(idSelector(`${id}-error`));
722
+ if (errorNode) errorNode.textContent = err.message;
723
+ const inputNode = container.querySelector(idSelector(id));
724
+ if (inputNode) inputNode.classList.add("has-error");
725
+ }
726
+ }
727
+ function accountIdOf(path) {
728
+ if (path === "email") return "nodii-account-email";
729
+ if (path === "password") return "nodii-account-password";
730
+ if (path === "tosAccepted") return "nodii-consent-tos";
731
+ if (path === "privacyAccepted") return "nodii-consent-privacy";
732
+ return `nodii-account-${path}`;
733
+ }
734
+ function renderStepsIndicator(state) {
735
+ while (stepsIndicator.firstChild) {
736
+ stepsIndicator.removeChild(stepsIndicator.firstChild);
737
+ }
738
+ for (let i = 0; i < state.stepCount; i++) {
739
+ stepsIndicator.appendChild(
740
+ el2("div", {
741
+ class: `nodii-onboarding-step-dot${i <= state.stepIndex ? " active" : ""}`
742
+ })
743
+ );
744
+ }
745
+ }
746
+ function renderAccountSection(control, state) {
747
+ const section = el2("div", { class: "nodii-onboarding-account-section" });
748
+ const emailInput = el2("input", {
749
+ type: "email",
750
+ id: "nodii-account-email",
751
+ class: "nodii-onboarding-input",
752
+ autocomplete: "email"
753
+ });
754
+ emailInput.value = state.credentials.email;
755
+ emailInput.addEventListener(
756
+ "input",
757
+ () => control.setCredentials({ email: emailInput.value })
758
+ );
759
+ section.appendChild(
760
+ fieldRow(
761
+ {
762
+ key: "account-email",
763
+ label: "Account email",
764
+ required: true
765
+ },
766
+ emailInput,
767
+ "nodii-account-email"
768
+ // label for + error id match the real input id
769
+ )
770
+ );
771
+ const passwordInput = el2("input", {
772
+ type: "password",
773
+ id: "nodii-account-password",
774
+ class: "nodii-onboarding-input",
775
+ autocomplete: "new-password",
776
+ minlength: "12",
777
+ maxlength: "256"
778
+ });
779
+ passwordInput.value = state.credentials.password;
780
+ passwordInput.addEventListener(
781
+ "input",
782
+ () => control.setCredentials({ password: passwordInput.value })
783
+ );
784
+ section.appendChild(
785
+ fieldRow(
786
+ {
787
+ key: "account-password",
788
+ label: "Password (12+ characters)",
789
+ required: true
790
+ },
791
+ passwordInput,
792
+ "nodii-account-password"
793
+ // label for + error id match the real input id
794
+ )
795
+ );
796
+ const tosCheckbox = el2("input", {
797
+ type: "checkbox",
798
+ id: "nodii-consent-tos"
799
+ });
800
+ tosCheckbox.checked = state.consent.tosAccepted;
801
+ tosCheckbox.addEventListener(
802
+ "change",
803
+ () => control.setConsent({ tosAccepted: tosCheckbox.checked })
804
+ );
805
+ const tosLabelChildren = ["I accept the "];
806
+ if (options.tosUrl) {
807
+ tosLabelChildren.push(
808
+ el2(
809
+ "a",
810
+ {
811
+ href: options.tosUrl,
812
+ target: "_blank",
813
+ rel: "noopener noreferrer"
814
+ },
815
+ ["Terms of Service"]
816
+ )
817
+ );
818
+ } else {
819
+ tosLabelChildren.push("Terms of Service");
820
+ }
821
+ section.appendChild(
822
+ el2("div", { class: "nodii-onboarding-consent-row" }, [
823
+ tosCheckbox,
824
+ el2("label", { for: "nodii-consent-tos" }, tosLabelChildren)
825
+ ])
826
+ );
827
+ section.appendChild(
828
+ el2("div", {
829
+ class: "nodii-onboarding-field-error",
830
+ id: "nodii-consent-tos-error",
831
+ role: "alert"
832
+ })
833
+ );
834
+ const privacyCheckbox = el2("input", {
835
+ type: "checkbox",
836
+ id: "nodii-consent-privacy"
837
+ });
838
+ privacyCheckbox.checked = state.consent.privacyAccepted;
839
+ privacyCheckbox.addEventListener(
840
+ "change",
841
+ () => control.setConsent({ privacyAccepted: privacyCheckbox.checked })
842
+ );
843
+ const privacyLabelChildren = ["I accept the "];
844
+ if (options.privacyUrl) {
845
+ privacyLabelChildren.push(
846
+ el2(
847
+ "a",
848
+ {
849
+ href: options.privacyUrl,
850
+ target: "_blank",
851
+ rel: "noopener noreferrer"
852
+ },
853
+ ["Privacy Policy"]
854
+ )
855
+ );
856
+ } else {
857
+ privacyLabelChildren.push("Privacy Policy");
858
+ }
859
+ section.appendChild(
860
+ el2("div", { class: "nodii-onboarding-consent-row" }, [
861
+ privacyCheckbox,
862
+ el2("label", { for: "nodii-consent-privacy" }, privacyLabelChildren)
863
+ ])
864
+ );
865
+ section.appendChild(
866
+ el2("div", {
867
+ class: "nodii-onboarding-field-error",
868
+ id: "nodii-consent-privacy-error",
869
+ role: "alert"
870
+ })
871
+ );
872
+ return section;
873
+ }
874
+ function renderStep(control, state) {
875
+ while (stepArea.firstChild) stepArea.removeChild(stepArea.firstChild);
876
+ const step = state.currentStep;
877
+ if (step) {
878
+ stepArea.appendChild(
879
+ el2("h3", { class: "nodii-onboarding-stage-title" }, [
880
+ STAGE_TITLES[step.stage] ?? step.stage
881
+ ])
882
+ );
883
+ for (const field of step.fields) {
884
+ const input = buildFieldInput(
885
+ field,
886
+ state.answers[field.key],
887
+ (value) => control.setAnswer(field.key, value)
888
+ );
889
+ stepArea.appendChild(fieldRow(field, input));
890
+ }
891
+ }
892
+ const isLast = control.isLastStep();
893
+ if (isLast) {
894
+ stepArea.appendChild(renderAccountSection(control, state));
895
+ }
896
+ const nav = el2("div", { class: "nodii-onboarding-nav" });
897
+ if (!control.isFirstStep()) {
898
+ const backBtn = el2(
899
+ "button",
900
+ { type: "button", class: "nodii-onboarding-back-btn" },
901
+ ["Back"]
902
+ );
903
+ backBtn.addEventListener("click", () => control.back());
904
+ nav.appendChild(backBtn);
905
+ }
906
+ const forwardBtn = el2(
907
+ "button",
908
+ {
909
+ type: isLast ? "submit" : "button",
910
+ class: "nodii-onboarding-submit-btn",
911
+ id: "nodii-signup-forward"
912
+ },
913
+ [isLast ? options.submitLabel ?? "Create account" : "Next"]
914
+ );
915
+ if (!isLast) {
916
+ forwardBtn.addEventListener("click", () => control.next());
917
+ }
918
+ nav.appendChild(forwardBtn);
919
+ stepArea.appendChild(nav);
920
+ }
921
+ async function handleSpecStale(control) {
922
+ const prior = currentSpecVersion !== null ? cache.get(options.onboarder, currentSpecVersion) : void 0;
923
+ const fresh = await fetchOnboardingSpec({
924
+ baseUrl: options.baseUrl,
925
+ onboarder: options.onboarder,
926
+ fetch: options.fetch,
927
+ timeoutMs: options.timeoutMs,
928
+ cache
929
+ });
930
+ if (unmounted) return;
931
+ if (prior) {
932
+ const diff = diffSpecFields(prior, fresh);
933
+ const parts = [];
934
+ if (diff.added.length) parts.push(`added: ${diff.added.join(", ")}`);
935
+ if (diff.removed.length)
936
+ parts.push(`removed: ${diff.removed.join(", ")}`);
937
+ if (diff.changed.length)
938
+ parts.push(`changed: ${diff.changed.join(", ")}`);
939
+ staleDiffSummary = parts.length ? ` (${parts.join("; ")})` : "";
940
+ } else {
941
+ staleDiffSummary = "";
942
+ }
943
+ siteKey = options.siteKey ?? fresh.botChallenge.siteKey;
944
+ currentSpecVersion = fresh.specVersion;
945
+ control.applyNewSpec(fresh);
946
+ idempotencyKey = null;
947
+ lastFingerprint = null;
948
+ }
949
+ async function handleSubmit(control) {
950
+ if (!control.validateAll()) return;
951
+ control.acknowledgeSpecStale();
952
+ control.setSubmitting();
953
+ const body = control.buildSubmitPayload();
954
+ const fingerprint = computeSignupFingerprint(body);
955
+ if (idempotencyKey === null || fingerprint !== lastFingerprint) {
956
+ idempotencyKey = generateIdempotencyKey();
957
+ lastFingerprint = fingerprint;
958
+ }
959
+ let firstToken;
960
+ try {
961
+ firstToken = await executeRecaptcha(siteKey, "signup_submit", {
962
+ timeoutMs: options.timeoutMs
963
+ });
964
+ } catch (recaptchaErr) {
965
+ control.setError("Could not load bot protection. Please try again.");
966
+ options.onError?.(recaptchaErr);
967
+ return;
968
+ }
969
+ let usedFirst = false;
970
+ const tokenProvider = async () => {
971
+ if (!usedFirst) {
972
+ usedFirst = true;
973
+ return firstToken;
974
+ }
975
+ return executeRecaptcha(siteKey, "signup_submit", {
976
+ timeoutMs: options.timeoutMs
977
+ });
978
+ };
979
+ try {
980
+ const result = await submitSignup(body, {
981
+ baseUrl: options.baseUrl,
982
+ botToken: tokenProvider,
983
+ idempotencyKey,
984
+ fetch: options.fetch,
985
+ timeoutMs: options.timeoutMs
986
+ });
987
+ idempotencyKey = null;
988
+ lastFingerprint = null;
989
+ control.setSuccess(result.terminalStage);
990
+ options.onSuccess?.(result);
991
+ } catch (err) {
992
+ if (err instanceof OnboardingSpecStaleError) {
993
+ try {
994
+ await handleSpecStale(control);
995
+ } catch (refetchErr) {
996
+ control.setError(
997
+ "The signup form changed and could not be reloaded. Please refresh the page."
998
+ );
999
+ options.onError?.(refetchErr);
1000
+ return;
1001
+ }
1002
+ options.onError?.(err);
1003
+ } else if (err instanceof OnboardingBotChallengeError) {
1004
+ control.setError(
1005
+ "Bot protection check failed. Please refresh the page and try again."
1006
+ );
1007
+ options.onError?.(err);
1008
+ } else if (err instanceof OnboardingRateLimitError) {
1009
+ control.setError(
1010
+ err.retryAfterSeconds !== null ? `Too many requests. Please wait ${err.retryAfterSeconds}s.` : "Too many requests. Please wait a moment."
1011
+ );
1012
+ options.onError?.(err);
1013
+ } else {
1014
+ control.setError("Something went wrong. Please try again in a moment.");
1015
+ options.onError?.(err);
1016
+ }
1017
+ }
1018
+ }
1019
+ function onStateChange(control, state) {
1020
+ if (state.stepIndex !== renderedStepIndex || state.specStale !== renderedStale) {
1021
+ renderedStepIndex = state.stepIndex;
1022
+ renderedStale = state.specStale;
1023
+ renderStep(control, state);
1024
+ renderStepsIndicator(state);
1025
+ }
1026
+ staleBanner.style.display = state.specStale ? "" : "none";
1027
+ staleBanner.textContent = state.specStale ? `This form was just updated by the provider${staleDiffSummary}. Please review your answers below and submit again.` : "";
1028
+ setFieldErrorsInPlace(
1029
+ stepArea,
1030
+ state.errors,
1031
+ (path) => `nodii-f-${path.split(".")[0] ?? path}`
1032
+ );
1033
+ for (const err of state.accountErrors) {
1034
+ const id = accountIdOf(err.path.split(".")[0] ?? err.path);
1035
+ const errorNode = stepArea.querySelector(idSelector(`${id}-error`));
1036
+ if (errorNode) errorNode.textContent = err.message;
1037
+ const inputNode = stepArea.querySelector(idSelector(id));
1038
+ if (inputNode) inputNode.classList.add("has-error");
1039
+ }
1040
+ if (state.serverError) {
1041
+ serverErrorEl.textContent = state.serverError;
1042
+ serverErrorEl.style.display = "";
1043
+ } else {
1044
+ serverErrorEl.style.display = "none";
1045
+ }
1046
+ const forwardBtn = stepArea.querySelector(
1047
+ "#nodii-signup-forward"
1048
+ );
1049
+ if (forwardBtn) {
1050
+ forwardBtn.disabled = state.status === "submitting";
1051
+ }
1052
+ if (state.status === "success") {
1053
+ 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.";
1054
+ successEl.textContent = message;
1055
+ successEl.style.display = "";
1056
+ form.style.display = "none";
1057
+ } else {
1058
+ successEl.style.display = "none";
1059
+ form.style.display = "";
1060
+ }
1061
+ }
1062
+ void (async () => {
1063
+ let spec;
1064
+ try {
1065
+ spec = await fetchOnboardingSpec({
1066
+ baseUrl: options.baseUrl,
1067
+ onboarder: options.onboarder,
1068
+ fetch: options.fetch,
1069
+ timeoutMs: options.timeoutMs,
1070
+ cache,
1071
+ // Reuse a spec already loaded for this onboarder this session (a
1072
+ // remount into the same page shouldn't re-hit the network). A 409 on
1073
+ // submit is the authoritative staleness signal that triggers refetch.
1074
+ preferCached: true
1075
+ });
1076
+ } catch (err) {
1077
+ if (unmounted) return;
1078
+ loadingEl.textContent = "The signup form could not be loaded. Please refresh the page.";
1079
+ options.onError?.(err);
1080
+ return;
1081
+ }
1082
+ if (unmounted) return;
1083
+ siteKey = options.siteKey ?? spec.botChallenge.siteKey;
1084
+ currentSpecVersion = spec.specVersion;
1085
+ const control = createSignupStepper(spec);
1086
+ stepper = control;
1087
+ form.addEventListener("submit", (event) => {
1088
+ event.preventDefault();
1089
+ void handleSubmit(control);
1090
+ });
1091
+ form.appendChild(stepsIndicator);
1092
+ form.appendChild(staleBanner);
1093
+ form.appendChild(stepArea);
1094
+ form.appendChild(serverErrorEl);
1095
+ wrapper.removeChild(loadingEl);
1096
+ wrapper.appendChild(successEl);
1097
+ wrapper.appendChild(form);
1098
+ unsubscribe = control.subscribe((state) => onStateChange(control, state));
1099
+ onStateChange(control, control.getState());
1100
+ options.onReady?.(spec);
1101
+ })();
1102
+ return {
1103
+ unmount() {
1104
+ unmounted = true;
1105
+ unsubscribe?.();
1106
+ if (wrapper.parentNode === host) host.removeChild(wrapper);
1107
+ },
1108
+ reset() {
1109
+ idempotencyKey = null;
1110
+ lastFingerprint = null;
1111
+ renderedStepIndex = -1;
1112
+ renderedStale = false;
1113
+ if (stepper) {
1114
+ stepper.reset();
1115
+ } else {
1116
+ form.style.display = "";
1117
+ successEl.style.display = "none";
1118
+ }
1119
+ }
1120
+ };
1121
+ }
1122
+
1123
+ // src/widget/index.ts
1124
+ var OnboardingWidget = {
1125
+ /**
1126
+ * Mount the onboarding widget into a host element.
1127
+ *
1128
+ * @param el - The DOM element to render into.
1129
+ * @param options - Widget configuration. `form: "contact"` renders the
1130
+ * fixed contact form (Phase 1); `form: "signup"` fetches the D540 spec
1131
+ * and renders the dynamic stepper (Phase 2 — siteKey comes from the
1132
+ * spec's botChallenge unless overridden).
1133
+ * @returns A mounted widget control (unmount / reset).
1134
+ *
1135
+ * @example
1136
+ * ```ts
1137
+ * import { OnboardingWidget } from "@nodii/onboarding/widget";
1138
+ *
1139
+ * OnboardingWidget.mount(document.getElementById("signup-form")!, {
1140
+ * onboarder: "acme-corp",
1141
+ * form: "signup",
1142
+ * baseUrl: "https://api.acme.nucleus-cloud.in",
1143
+ * onSuccess: (r) => console.log("landed at", r.terminalStage),
1144
+ * });
1145
+ * ```
1146
+ */
1147
+ mount(el3, options) {
1148
+ if (options.form === "contact") {
1149
+ return mountContactForm(el3, options);
1150
+ }
1151
+ if (options.form === "signup") {
1152
+ return mountSignupForm(el3, options);
1153
+ }
1154
+ throw new Error(
1155
+ `OnboardingWidget.mount: unknown form "${options.form}". Supported: "contact", "signup".`
1156
+ );
1157
+ }
1158
+ };
1159
+
1160
+ export { DEFAULT_WIDGET_CSS, OnboardingWidget, cssEscape, idSelector, injectWidgetStyles, mountContactForm, mountSignupForm };
1161
+ //# sourceMappingURL=chunk-SWMIIUXR.js.map
1162
+ //# sourceMappingURL=chunk-SWMIIUXR.js.map