@redacto.io/dpdpa-report-sdk-js 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1662 @@
1
+ // src/constants.ts
2
+ var DEFAULT_BASE_URL = "https://salazar.redacto.tech";
3
+ var PATHS = {
4
+ submit: "/webhook/dpdpa/generate",
5
+ status: "/webhook/dpdpa/status"
6
+ };
7
+ var DEFAULT_POLL_INTERVAL_MS = 3e3;
8
+ var DEFAULT_POLL_TIMEOUT_MS = 3e5;
9
+ var MAX_CONSECUTIVE_ERRORS = 3;
10
+ var REQUEST_TIMEOUT_MS = 15e3;
11
+ var STORAGE_KEY = "redacto_dpdpa_lead";
12
+ var DEFAULT_LOGO_URL = "https://cdn.redacto.io/assets/redacto-logo.svg";
13
+ var DEFAULT_BRAND = {
14
+ name: "Redacto",
15
+ primaryColor: "#4262ff",
16
+ accentColor: "#a259ff",
17
+ textColor: "#050038",
18
+ borderColor: "#e5e5e5",
19
+ backgroundColor: "#f7f7f7",
20
+ borderRadius: "8px",
21
+ fontFamily: "inherit"
22
+ };
23
+ var SUCCESS_COLOR = "#a6c03d";
24
+ var ERROR_COLOR = "#f24e1e";
25
+ var NAME_MIN_LENGTH = 2;
26
+ var NAME_MAX_LENGTH = 120;
27
+ var DOMAIN_REGEX = /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.[a-z0-9-]{1,63})+$/i;
28
+ var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
29
+ var FREEMAIL_DOMAINS = /* @__PURE__ */ new Set([
30
+ "gmail.com",
31
+ "googlemail.com",
32
+ "yahoo.com",
33
+ "yahoo.co.in",
34
+ "yahoo.co.uk",
35
+ "hotmail.com",
36
+ "outlook.com",
37
+ "live.com",
38
+ "icloud.com",
39
+ "me.com",
40
+ "aol.com",
41
+ "proton.me",
42
+ "protonmail.com",
43
+ "mail.com",
44
+ "gmx.com",
45
+ "zoho.com",
46
+ "rediffmail.com"
47
+ ]);
48
+
49
+ // src/core/dom.ts
50
+ function el(tag, attrs, ...children) {
51
+ const element = document.createElement(tag);
52
+ if (attrs) applyAttrs(element, attrs);
53
+ appendChildren(element, children);
54
+ return element;
55
+ }
56
+ function svg(tag, attrs, ...children) {
57
+ const element = document.createElementNS(
58
+ "http://www.w3.org/2000/svg",
59
+ tag
60
+ );
61
+ if (attrs) {
62
+ for (const [key, value] of Object.entries(attrs)) {
63
+ if (value == null || value === false) continue;
64
+ if (key === "style" && typeof value === "object") {
65
+ applyStyles(element, value);
66
+ } else {
67
+ element.setAttribute(key, String(value));
68
+ }
69
+ }
70
+ }
71
+ appendChildren(element, children);
72
+ return element;
73
+ }
74
+ function applyAttrs(element, attrs) {
75
+ for (const [key, value] of Object.entries(attrs)) {
76
+ if (value == null || value === false) continue;
77
+ if (key === "style" && typeof value === "object") {
78
+ applyStyles(element, value);
79
+ } else if (key === "className") {
80
+ element.className = value;
81
+ } else if (key === "dataset" && typeof value === "object") {
82
+ Object.assign(element.dataset, value);
83
+ } else if (key.startsWith("on") && typeof value === "function") {
84
+ const event = key.slice(2).toLowerCase();
85
+ element.addEventListener(event, value);
86
+ } else if (key === "checked" || key === "disabled" || key === "readOnly") {
87
+ element[key] = value;
88
+ } else {
89
+ element.setAttribute(key, String(value));
90
+ }
91
+ }
92
+ }
93
+ var UNITLESS_PROPS = /* @__PURE__ */ new Set([
94
+ "animationIterationCount",
95
+ "aspectRatio",
96
+ "borderImageOutset",
97
+ "borderImageSlice",
98
+ "borderImageWidth",
99
+ "boxFlex",
100
+ "boxFlexGroup",
101
+ "boxOrdinalGroup",
102
+ "columnCount",
103
+ "columns",
104
+ "flex",
105
+ "flexGrow",
106
+ "flexPositive",
107
+ "flexShrink",
108
+ "flexNegative",
109
+ "flexOrder",
110
+ "fontWeight",
111
+ "gridArea",
112
+ "gridRow",
113
+ "gridRowEnd",
114
+ "gridRowSpan",
115
+ "gridRowStart",
116
+ "gridColumn",
117
+ "gridColumnEnd",
118
+ "gridColumnSpan",
119
+ "gridColumnStart",
120
+ "lineClamp",
121
+ "lineHeight",
122
+ "opacity",
123
+ "order",
124
+ "orphans",
125
+ "scale",
126
+ "tabSize",
127
+ "widows",
128
+ "zIndex",
129
+ "zoom"
130
+ ]);
131
+ function applyStyles(element, styles) {
132
+ for (const [key, value] of Object.entries(styles)) {
133
+ if (value == null) continue;
134
+ const stringValue = typeof value === "number" && !UNITLESS_PROPS.has(key) ? `${value}px` : String(value);
135
+ element.style[key] = stringValue;
136
+ }
137
+ }
138
+ function appendChildren(parent, children) {
139
+ for (const child of children) {
140
+ if (child == null || child === false) continue;
141
+ if (typeof child === "string") {
142
+ parent.appendChild(document.createTextNode(child));
143
+ } else {
144
+ parent.appendChild(child);
145
+ }
146
+ }
147
+ }
148
+ function clearElement(element) {
149
+ while (element.firstChild) {
150
+ element.removeChild(element.firstChild);
151
+ }
152
+ }
153
+
154
+ // src/core/state.ts
155
+ var Store = class {
156
+ constructor(initialState) {
157
+ this.listeners = /* @__PURE__ */ new Set();
158
+ this.state = { ...initialState };
159
+ }
160
+ getState() {
161
+ return { ...this.state };
162
+ }
163
+ setState(partial) {
164
+ const prevState = this.state;
165
+ this.state = { ...this.state, ...partial };
166
+ this.listeners.forEach((listener) => listener(this.state, prevState));
167
+ }
168
+ subscribe(listener) {
169
+ this.listeners.add(listener);
170
+ return () => {
171
+ this.listeners.delete(listener);
172
+ };
173
+ }
174
+ destroy() {
175
+ this.listeners.clear();
176
+ }
177
+ };
178
+
179
+ // src/api/types.ts
180
+ var ApiError = class extends Error {
181
+ constructor(message, status) {
182
+ super(message);
183
+ this.name = "ApiError";
184
+ this.status = status;
185
+ }
186
+ };
187
+
188
+ // src/api/index.ts
189
+ function timedFetch(url, init2 = {}) {
190
+ const controller = new AbortController();
191
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
192
+ return fetch(url, { ...init2, signal: controller.signal }).finally(
193
+ () => clearTimeout(timer)
194
+ );
195
+ }
196
+ function toNetworkError(err) {
197
+ if (err instanceof DOMException && err.name === "AbortError") {
198
+ return new ApiError("Request timed out", void 0);
199
+ }
200
+ return new ApiError(
201
+ err instanceof Error ? err.message : "Network error",
202
+ void 0
203
+ );
204
+ }
205
+ async function submitLead({
206
+ baseUrl,
207
+ body
208
+ }) {
209
+ let res;
210
+ try {
211
+ res = await timedFetch(`${baseUrl}${PATHS.submit}`, {
212
+ method: "POST",
213
+ headers: { "Content-Type": "application/json" },
214
+ body: JSON.stringify(body)
215
+ });
216
+ } catch (err) {
217
+ throw toNetworkError(err);
218
+ }
219
+ let json;
220
+ try {
221
+ json = await res.json();
222
+ } catch {
223
+ throw new ApiError(`Unexpected response (HTTP ${res.status})`, res.status);
224
+ }
225
+ if (!res.ok) {
226
+ const message = json && "message" in json && json.message ? json.message : `Request failed (HTTP ${res.status})`;
227
+ throw new ApiError(message, res.status);
228
+ }
229
+ return json;
230
+ }
231
+ async function fetchJobStatus({
232
+ baseUrl,
233
+ jobId
234
+ }) {
235
+ const url = `${baseUrl}${PATHS.status}?job_id=${encodeURIComponent(jobId)}`;
236
+ let res;
237
+ try {
238
+ res = await timedFetch(url, { method: "GET" });
239
+ } catch (err) {
240
+ throw toNetworkError(err);
241
+ }
242
+ let json;
243
+ try {
244
+ json = await res.json();
245
+ } catch {
246
+ throw new ApiError(`Unexpected response (HTTP ${res.status})`, res.status);
247
+ }
248
+ if (!res.ok) {
249
+ const message = json && "message" in json && json.message ? json.message : `Status check failed (HTTP ${res.status})`;
250
+ throw new ApiError(message, res.status);
251
+ }
252
+ return json;
253
+ }
254
+
255
+ // src/services/storage.ts
256
+ var isBrowser = () => typeof window !== "undefined" && typeof window.localStorage !== "undefined";
257
+ var saveLead = (lead) => {
258
+ if (!isBrowser()) return;
259
+ try {
260
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(lead));
261
+ } catch (error) {
262
+ console.error("Failed to save lead:", error);
263
+ }
264
+ };
265
+ var isCapturedLead = (v) => {
266
+ if (!v || typeof v !== "object") return false;
267
+ const o = v;
268
+ return typeof o.email === "string" && typeof o.company_name === "string" && typeof o.company_domain === "string" && (o.job_id === null || typeof o.job_id === "string") && typeof o.timestamp === "string";
269
+ };
270
+ var loadLead = () => {
271
+ if (!isBrowser()) return null;
272
+ try {
273
+ const raw = window.localStorage.getItem(STORAGE_KEY);
274
+ if (!raw) return null;
275
+ const parsed = JSON.parse(raw);
276
+ return isCapturedLead(parsed) ? parsed : null;
277
+ } catch (error) {
278
+ console.error("Failed to load lead:", error);
279
+ return null;
280
+ }
281
+ };
282
+
283
+ // src/styles/inject.ts
284
+ var STYLE_ID = "redacto-dpdpa-form-styles";
285
+ function buildCss(brand) {
286
+ return `
287
+ :host {
288
+ display: block;
289
+ width: 100%;
290
+ container-type: inline-size;
291
+ container-name: dpdpa-card;
292
+ }
293
+
294
+ :host, :host > *, :host *, :host *::before, :host *::after {
295
+ box-sizing: border-box;
296
+ }
297
+
298
+ .dpdpa-card {
299
+ box-sizing: border-box;
300
+ width: 100%;
301
+ min-height: 520px;
302
+ display: flex;
303
+ flex-direction: column;
304
+ padding: 28px 24px;
305
+ background: ${brand.background};
306
+ color: ${brand.text};
307
+ border: 1px solid ${brand.border};
308
+ border-radius: ${brand.borderRadius};
309
+ font-family: ${brand.fontFamily};
310
+ font-size: 15px;
311
+ line-height: 1.5;
312
+ text-align: left;
313
+ }
314
+
315
+ @container dpdpa-card (max-width: 420px) {
316
+ .dpdpa-card {
317
+ padding: 20px 16px;
318
+ min-height: 480px;
319
+ }
320
+ .dpdpa-heading {
321
+ font-size: 18px;
322
+ }
323
+ .dpdpa-success-title,
324
+ .dpdpa-error-title {
325
+ font-size: 16px;
326
+ }
327
+ .dpdpa-header {
328
+ margin-bottom: 16px;
329
+ }
330
+ }
331
+
332
+ @supports not (container-type: inline-size) {
333
+ @media (max-width: 480px) {
334
+ .dpdpa-card {
335
+ padding: 20px 16px;
336
+ }
337
+ .dpdpa-heading {
338
+ font-size: 18px;
339
+ }
340
+ }
341
+ }
342
+
343
+ .dpdpa-header {
344
+ display: flex;
345
+ align-items: center;
346
+ flex-wrap: wrap;
347
+ gap: 10px;
348
+ margin-bottom: 20px;
349
+ }
350
+
351
+ .dpdpa-logo {
352
+ max-height: 28px;
353
+ max-width: 140px;
354
+ display: inline-block;
355
+ object-fit: contain;
356
+ }
357
+
358
+ .dpdpa-wordmark {
359
+ font-weight: 700;
360
+ font-size: 17px;
361
+ color: ${brand.text};
362
+ letter-spacing: 0.3px;
363
+ }
364
+
365
+ .dpdpa-footer {
366
+ margin-top: 20px;
367
+ padding-top: 16px;
368
+ border-top: 1px solid ${brand.border};
369
+ font-size: 12px;
370
+ color: ${brand.text};
371
+ opacity: 0.7;
372
+ text-align: center;
373
+ }
374
+
375
+ .dpdpa-form {
376
+ flex: 1;
377
+ display: flex;
378
+ flex-direction: column;
379
+ justify-content: flex-start;
380
+ gap: 14px;
381
+ margin: 0;
382
+ }
383
+
384
+ .dpdpa-heading {
385
+ font-size: 20px;
386
+ font-weight: 700;
387
+ margin: 0 0 4px;
388
+ color: ${brand.text};
389
+ line-height: 1.3;
390
+ }
391
+
392
+ .dpdpa-subheading {
393
+ margin: 0 0 8px;
394
+ font-size: 14px;
395
+ color: ${brand.text};
396
+ opacity: 0.75;
397
+ }
398
+
399
+ .dpdpa-field {
400
+ display: flex;
401
+ flex-direction: column;
402
+ gap: 4px;
403
+ }
404
+
405
+ .dpdpa-label {
406
+ font-size: 13px;
407
+ font-weight: 600;
408
+ color: ${brand.text};
409
+ }
410
+
411
+ .dpdpa-input {
412
+ width: 100%;
413
+ min-width: 0;
414
+ padding: 11px 13px;
415
+ font-family: inherit;
416
+ font-size: 14px;
417
+ color: ${brand.text};
418
+ background: #ffffff;
419
+ border: 1px solid ${brand.border};
420
+ border-radius: ${brand.borderRadius};
421
+ outline: none;
422
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
423
+ }
424
+
425
+ .dpdpa-input:focus {
426
+ border-color: ${brand.primary};
427
+ box-shadow: 0 0 0 3px ${brand.primaryFocus};
428
+ }
429
+
430
+ .dpdpa-input:disabled {
431
+ background: ${brand.border};
432
+ opacity: 0.7;
433
+ cursor: not-allowed;
434
+ }
435
+
436
+ .dpdpa-field-error,
437
+ .dpdpa-general-error {
438
+ font-size: 12px;
439
+ color: ${brand.errorColor};
440
+ line-height: 1.4;
441
+ }
442
+
443
+ .dpdpa-general-error {
444
+ padding: 10px 12px;
445
+ background: ${brand.errorColor}14;
446
+ border: 1px solid ${brand.errorColor}33;
447
+ border-radius: ${brand.borderRadius};
448
+ }
449
+
450
+ .dpdpa-btn-primary {
451
+ display: inline-flex;
452
+ align-items: center;
453
+ justify-content: center;
454
+ gap: 8px;
455
+ width: 100%;
456
+ padding: 12px 18px;
457
+ font-family: inherit;
458
+ font-size: 15px;
459
+ font-weight: 600;
460
+ color: ${brand.primaryText};
461
+ background: ${brand.primary};
462
+ border: none;
463
+ border-radius: ${brand.borderRadius};
464
+ cursor: pointer;
465
+ text-decoration: none;
466
+ transition: background-color 0.15s ease, transform 0.05s ease;
467
+ margin-top: 6px;
468
+ }
469
+
470
+ .dpdpa-btn-primary:hover:not(:disabled) {
471
+ background: ${brand.primaryHover};
472
+ }
473
+
474
+ .dpdpa-btn-primary:active:not(:disabled) {
475
+ transform: translateY(1px);
476
+ }
477
+
478
+ .dpdpa-btn-primary:disabled {
479
+ opacity: 0.7;
480
+ cursor: not-allowed;
481
+ }
482
+
483
+ .dpdpa-btn-link {
484
+ text-decoration: none;
485
+ color: ${brand.primaryText};
486
+ }
487
+
488
+ .dpdpa-spinner {
489
+ display: inline-block;
490
+ border-radius: 50%;
491
+ border-style: solid;
492
+ border-color: ${brand.primaryText}4D;
493
+ animation: dpdpa-spin 0.7s linear infinite;
494
+ }
495
+
496
+ .dpdpa-spinner-inline {
497
+ width: 14px;
498
+ height: 14px;
499
+ border-width: 2px;
500
+ border-top-color: ${brand.primaryText};
501
+ }
502
+
503
+ .dpdpa-polling {
504
+ flex: 1;
505
+ display: flex;
506
+ flex-direction: column;
507
+ justify-content: center;
508
+ padding: 4px 2px 8px;
509
+ gap: 16px;
510
+ }
511
+
512
+ .dpdpa-polling-sub {
513
+ margin: 0;
514
+ font-size: 13px;
515
+ color: ${brand.text};
516
+ opacity: 0.6;
517
+ text-align: center;
518
+ }
519
+
520
+ .dpdpa-steps {
521
+ display: flex;
522
+ flex-direction: column;
523
+ gap: 10px;
524
+ }
525
+
526
+ .dpdpa-step {
527
+ display: flex;
528
+ align-items: center;
529
+ gap: 10px;
530
+ font-size: 15px;
531
+ animation: dpdpa-step-in 0.35s ease-out both;
532
+ }
533
+
534
+ .dpdpa-step-done .dpdpa-step-label {
535
+ color: ${brand.text};
536
+ opacity: 0.6;
537
+ }
538
+
539
+ .dpdpa-step-icon {
540
+ flex: none;
541
+ width: 18px;
542
+ height: 18px;
543
+ border-radius: 50%;
544
+ display: inline-flex;
545
+ align-items: center;
546
+ justify-content: center;
547
+ }
548
+
549
+ .dpdpa-step-icon-active {
550
+ position: relative;
551
+ width: 10px;
552
+ height: 10px;
553
+ margin: 0 4px;
554
+ box-shadow: 0 0 0 0 ${brand.primaryFocus};
555
+ animation: dpdpa-pulse 1.4s ease-out infinite;
556
+ }
557
+
558
+ .dpdpa-step-label {
559
+ color: ${brand.text};
560
+ font-weight: 500;
561
+ word-break: break-word;
562
+ }
563
+
564
+ .dpdpa-shimmer {
565
+ background: linear-gradient(
566
+ 90deg,
567
+ ${brand.text} 0%,
568
+ ${brand.primary} 40%,
569
+ ${brand.accent} 50%,
570
+ ${brand.primary} 60%,
571
+ ${brand.text} 100%
572
+ );
573
+ background-size: 200% 100%;
574
+ -webkit-background-clip: text;
575
+ background-clip: text;
576
+ -webkit-text-fill-color: transparent;
577
+ color: transparent;
578
+ animation: dpdpa-shimmer 2.2s linear infinite;
579
+ }
580
+
581
+ .dpdpa-dots {
582
+ display: inline-flex;
583
+ align-items: center;
584
+ gap: 3px;
585
+ margin-left: 2px;
586
+ }
587
+
588
+ .dpdpa-dots > span {
589
+ width: 4px;
590
+ height: 4px;
591
+ border-radius: 50%;
592
+ background: ${brand.primary};
593
+ opacity: 0.4;
594
+ animation: dpdpa-dot-bounce 1.2s ease-in-out infinite;
595
+ }
596
+
597
+ .dpdpa-dots > span:nth-child(2) { animation-delay: 0.15s; }
598
+ .dpdpa-dots > span:nth-child(3) { animation-delay: 0.3s; }
599
+
600
+ @keyframes dpdpa-shimmer {
601
+ from { background-position: 200% 0; }
602
+ to { background-position: -200% 0; }
603
+ }
604
+
605
+ @keyframes dpdpa-dot-bounce {
606
+ 0%, 80%, 100% { opacity: 0.3; transform: translateY(0); }
607
+ 40% { opacity: 1; transform: translateY(-3px); }
608
+ }
609
+
610
+ @keyframes dpdpa-pulse {
611
+ 0% { box-shadow: 0 0 0 0 ${brand.primaryFocus}; }
612
+ 70% { box-shadow: 0 0 0 8px rgba(0, 0, 0, 0); }
613
+ 100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
614
+ }
615
+
616
+ @keyframes dpdpa-step-in {
617
+ from { opacity: 0; transform: translateY(4px); }
618
+ to { opacity: 1; transform: translateY(0); }
619
+ }
620
+
621
+ .dpdpa-progress {
622
+ width: 100%;
623
+ height: 6px;
624
+ background: ${brand.border};
625
+ border-radius: 999px;
626
+ overflow: hidden;
627
+ margin-top: 4px;
628
+ }
629
+
630
+ .dpdpa-progress-fill {
631
+ height: 100%;
632
+ background: linear-gradient(to right, ${brand.primary}, ${brand.accent});
633
+ transition: width 0.5s ease;
634
+ }
635
+
636
+ .dpdpa-success {
637
+ flex: 1;
638
+ display: flex;
639
+ flex-direction: column;
640
+ justify-content: center;
641
+ align-items: center;
642
+ text-align: center;
643
+ gap: 12px;
644
+ padding: 8px 4px;
645
+ }
646
+
647
+ .dpdpa-check-circle {
648
+ width: 56px;
649
+ height: 56px;
650
+ border-radius: 50%;
651
+ display: flex;
652
+ align-items: center;
653
+ justify-content: center;
654
+ }
655
+
656
+ .dpdpa-success-title {
657
+ margin: 0;
658
+ font-size: 18px;
659
+ font-weight: 700;
660
+ color: ${brand.text};
661
+ line-height: 1.35;
662
+ }
663
+
664
+ .dpdpa-success-sub {
665
+ margin: 0;
666
+ font-size: 14px;
667
+ color: ${brand.text};
668
+ opacity: 0.75;
669
+ }
670
+
671
+ .dpdpa-reset-link {
672
+ font-size: 13px;
673
+ color: ${brand.primary};
674
+ text-decoration: none;
675
+ margin-top: 4px;
676
+ cursor: pointer;
677
+ }
678
+
679
+ .dpdpa-reset-link:hover {
680
+ text-decoration: underline;
681
+ }
682
+
683
+ .dpdpa-error-view {
684
+ flex: 1;
685
+ display: flex;
686
+ flex-direction: column;
687
+ justify-content: center;
688
+ gap: 12px;
689
+ text-align: center;
690
+ padding: 8px 4px;
691
+ }
692
+
693
+ .dpdpa-error-title {
694
+ margin: 0;
695
+ font-size: 18px;
696
+ font-weight: 700;
697
+ color: ${brand.errorColor};
698
+ }
699
+
700
+ .dpdpa-error-message {
701
+ margin: 0;
702
+ font-size: 14px;
703
+ color: ${brand.text};
704
+ }
705
+
706
+ @keyframes dpdpa-spin {
707
+ from { transform: rotate(0deg); }
708
+ to { transform: rotate(360deg); }
709
+ }
710
+ `;
711
+ }
712
+ function injectFormStyles(root, brand) {
713
+ if (typeof document === "undefined") return;
714
+ if (root.querySelector(`#${STYLE_ID}`)) return;
715
+ const style = document.createElement("style");
716
+ style.id = STYLE_ID;
717
+ style.textContent = buildCss(brand);
718
+ root.appendChild(style);
719
+ }
720
+
721
+ // src/form/colors.ts
722
+ var HEX_RE = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
723
+ function normalizeHex(hex, fallback) {
724
+ if (!hex || !HEX_RE.test(hex.trim())) return fallback;
725
+ return hex.trim();
726
+ }
727
+ function hexToRgb(hex) {
728
+ const h = hex.replace("#", "").trim();
729
+ const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
730
+ return {
731
+ r: parseInt(full.slice(0, 2), 16),
732
+ g: parseInt(full.slice(2, 4), 16),
733
+ b: parseInt(full.slice(4, 6), 16)
734
+ };
735
+ }
736
+ function darken(hex, amount) {
737
+ const { r, g, b } = hexToRgb(hex);
738
+ const d = (c) => Math.max(0, Math.round(c * (1 - amount)));
739
+ return `rgb(${d(r)}, ${d(g)}, ${d(b)})`;
740
+ }
741
+ function rgba(hex, alpha) {
742
+ const { r, g, b } = hexToRgb(hex);
743
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
744
+ }
745
+ function relativeLuminance(hex) {
746
+ const { r, g, b } = hexToRgb(hex);
747
+ const norm = (c) => {
748
+ const s = c / 255;
749
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
750
+ };
751
+ return 0.2126 * norm(r) + 0.7152 * norm(g) + 0.0722 * norm(b);
752
+ }
753
+ function contrastingTextOn(bg) {
754
+ return relativeLuminance(bg) > 0.55 ? "#050038" : "#ffffff";
755
+ }
756
+ function resolveBranding(b) {
757
+ var _a;
758
+ const primary = normalizeHex(b == null ? void 0 : b.primaryColor, DEFAULT_BRAND.primaryColor);
759
+ const accent = normalizeHex(
760
+ (_a = b == null ? void 0 : b.accentColor) != null ? _a : b == null ? void 0 : b.primaryColor,
761
+ DEFAULT_BRAND.accentColor
762
+ );
763
+ return {
764
+ logo: (b == null ? void 0 : b.logo) || DEFAULT_LOGO_URL,
765
+ name: (b == null ? void 0 : b.name) || DEFAULT_BRAND.name,
766
+ primary,
767
+ accent,
768
+ primaryHover: darken(primary, 0.08),
769
+ primaryFocus: rgba(primary, 0.3),
770
+ primaryText: contrastingTextOn(primary),
771
+ text: (b == null ? void 0 : b.textColor) || DEFAULT_BRAND.textColor,
772
+ border: (b == null ? void 0 : b.borderColor) || DEFAULT_BRAND.borderColor,
773
+ background: (b == null ? void 0 : b.backgroundColor) || DEFAULT_BRAND.backgroundColor,
774
+ borderRadius: (b == null ? void 0 : b.borderRadius) || DEFAULT_BRAND.borderRadius,
775
+ fontFamily: (b == null ? void 0 : b.fontFamily) || DEFAULT_BRAND.fontFamily,
776
+ successColor: SUCCESS_COLOR,
777
+ errorColor: ERROR_COLOR
778
+ };
779
+ }
780
+
781
+ // src/form/validation.ts
782
+ function normalizeDomain(raw) {
783
+ return raw.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "");
784
+ }
785
+ function validateField(field, rawValue) {
786
+ var _a;
787
+ const value = rawValue.trim();
788
+ switch (field) {
789
+ case "company_name": {
790
+ if (value.length < NAME_MIN_LENGTH) return "Please enter a company name";
791
+ if (value.length > NAME_MAX_LENGTH) return "Company name is too long";
792
+ return null;
793
+ }
794
+ case "company_domain": {
795
+ if (!value) return "Enter your company domain";
796
+ const normalized = normalizeDomain(value);
797
+ if (!DOMAIN_REGEX.test(normalized))
798
+ return "Enter a valid domain like example.com";
799
+ return null;
800
+ }
801
+ case "email": {
802
+ if (!value) return "Enter your work email";
803
+ if (!EMAIL_REGEX.test(value)) return "Enter a valid email";
804
+ const domain = (_a = value.split("@")[1]) == null ? void 0 : _a.toLowerCase();
805
+ if (domain && FREEMAIL_DOMAINS.has(domain))
806
+ return "Please use your work email";
807
+ return null;
808
+ }
809
+ }
810
+ }
811
+ function validateAll(fields) {
812
+ const errors = {};
813
+ Object.keys(fields).forEach((key) => {
814
+ const err = validateField(key, fields[key]);
815
+ if (err) errors[key] = err;
816
+ });
817
+ return errors;
818
+ }
819
+
820
+ // src/form/view.ts
821
+ function header(brand) {
822
+ const logoImg = el("img", {
823
+ src: brand.logo,
824
+ alt: `${brand.name} logo`,
825
+ className: "dpdpa-logo",
826
+ onerror: (ev) => {
827
+ const img = ev.currentTarget;
828
+ img.style.display = "none";
829
+ const wordmark2 = img.nextElementSibling;
830
+ if (wordmark2) wordmark2.style.display = "inline-block";
831
+ }
832
+ });
833
+ const wordmark = el(
834
+ "span",
835
+ { className: "dpdpa-wordmark", style: { display: "none" } },
836
+ brand.name
837
+ );
838
+ return el("div", { className: "dpdpa-header" }, logoImg, wordmark);
839
+ }
840
+ function inputRow(brand, opts) {
841
+ const inputEl = el("input", {
842
+ type: opts.type,
843
+ className: "dpdpa-input",
844
+ placeholder: opts.placeholder,
845
+ value: opts.value,
846
+ autocomplete: opts.autocomplete,
847
+ "aria-label": opts.label,
848
+ onInput: (ev) => opts.onInput(ev.currentTarget.value),
849
+ onBlur: () => opts.onBlur()
850
+ });
851
+ const errorSlot = el(
852
+ "div",
853
+ {
854
+ className: "dpdpa-field-error",
855
+ role: "alert",
856
+ style: { display: opts.error ? "block" : "none" }
857
+ },
858
+ opts.error || ""
859
+ );
860
+ const labelEl = el("label", { className: "dpdpa-label" }, opts.label);
861
+ const row = el(
862
+ "div",
863
+ { className: "dpdpa-field" },
864
+ labelEl,
865
+ inputEl,
866
+ errorSlot
867
+ );
868
+ return { row, input: inputEl, errorSlot };
869
+ }
870
+ function footer(brand) {
871
+ return el(
872
+ "div",
873
+ { className: "dpdpa-footer" },
874
+ "Powered by ",
875
+ el("strong", null, brand.name)
876
+ );
877
+ }
878
+ function renderIdle(brand, fields, errors, generalErrorText, handlers) {
879
+ const nameRow = inputRow(brand, {
880
+ field: "company_name",
881
+ label: "Company name",
882
+ type: "text",
883
+ placeholder: "Acme Inc",
884
+ value: fields.company_name,
885
+ error: errors.company_name,
886
+ autocomplete: "organization",
887
+ onInput: (v) => handlers.onInput("company_name", v),
888
+ onBlur: () => handlers.onBlur("company_name")
889
+ });
890
+ const domainRow = inputRow(brand, {
891
+ field: "company_domain",
892
+ label: "Company domain",
893
+ type: "text",
894
+ placeholder: "acme.com",
895
+ value: fields.company_domain,
896
+ error: errors.company_domain,
897
+ autocomplete: "url",
898
+ onInput: (v) => handlers.onInput("company_domain", v),
899
+ onBlur: () => handlers.onBlur("company_domain")
900
+ });
901
+ const emailRow = inputRow(brand, {
902
+ field: "email",
903
+ label: "Work email",
904
+ type: "email",
905
+ placeholder: "you@acme.com",
906
+ value: fields.email,
907
+ error: errors.email,
908
+ autocomplete: "email",
909
+ onInput: (v) => handlers.onInput("email", v),
910
+ onBlur: () => handlers.onBlur("email")
911
+ });
912
+ const generalError = el(
913
+ "div",
914
+ {
915
+ className: "dpdpa-general-error",
916
+ role: "alert",
917
+ style: { display: generalErrorText ? "block" : "none" }
918
+ },
919
+ generalErrorText || ""
920
+ );
921
+ const submitBtn = el(
922
+ "button",
923
+ {
924
+ type: "submit",
925
+ className: "dpdpa-btn-primary"
926
+ },
927
+ "Get my free report"
928
+ );
929
+ const form = el(
930
+ "form",
931
+ {
932
+ className: "dpdpa-form",
933
+ novalidate: "true",
934
+ onSubmit: (ev) => {
935
+ ev.preventDefault();
936
+ handlers.onSubmit();
937
+ }
938
+ },
939
+ el(
940
+ "h2",
941
+ { className: "dpdpa-heading" },
942
+ "Get your free DPDPA compliance report"
943
+ ),
944
+ el(
945
+ "p",
946
+ { className: "dpdpa-subheading" },
947
+ "We'll analyse your site and deliver a compliance assessment PDF."
948
+ ),
949
+ generalError,
950
+ nameRow.row,
951
+ domainRow.row,
952
+ emailRow.row,
953
+ submitBtn
954
+ );
955
+ const card = el(
956
+ "div",
957
+ { className: "dpdpa-card" },
958
+ header(brand),
959
+ form,
960
+ footer(brand)
961
+ );
962
+ return {
963
+ root: card,
964
+ fieldInputs: {
965
+ company_name: nameRow.input,
966
+ company_domain: domainRow.input,
967
+ email: emailRow.input
968
+ },
969
+ fieldErrors: {
970
+ company_name: nameRow.errorSlot,
971
+ company_domain: domainRow.errorSlot,
972
+ email: emailRow.errorSlot
973
+ },
974
+ submitBtn,
975
+ generalError
976
+ };
977
+ }
978
+ function renderSubmitting(brand, fields) {
979
+ const refs = renderIdle(brand, fields, {}, null, {
980
+ onInput: () => {
981
+ },
982
+ onBlur: () => {
983
+ },
984
+ onSubmit: () => {
985
+ }
986
+ });
987
+ Object.values(refs.fieldInputs).forEach((inp) => {
988
+ inp.disabled = true;
989
+ });
990
+ refs.submitBtn.disabled = true;
991
+ refs.submitBtn.textContent = "";
992
+ refs.submitBtn.appendChild(
993
+ el("span", { className: "dpdpa-spinner dpdpa-spinner-inline" })
994
+ );
995
+ refs.submitBtn.appendChild(document.createTextNode(" Submitting\u2026"));
996
+ return refs.root;
997
+ }
998
+ function animatedDots() {
999
+ return el(
1000
+ "span",
1001
+ { className: "dpdpa-dots", "aria-hidden": "true" },
1002
+ el("span"),
1003
+ el("span"),
1004
+ el("span")
1005
+ );
1006
+ }
1007
+ function stepCheck(brand) {
1008
+ return el(
1009
+ "div",
1010
+ {
1011
+ className: "dpdpa-step-icon dpdpa-step-icon-done",
1012
+ style: { backgroundColor: brand.successColor }
1013
+ },
1014
+ svg(
1015
+ "svg",
1016
+ { width: 12, height: 12, viewBox: "0 0 24 24", fill: "none" },
1017
+ svg("path", {
1018
+ d: "M5 12l5 5L20 7",
1019
+ stroke: "#ffffff",
1020
+ "stroke-width": 3,
1021
+ "stroke-linecap": "round",
1022
+ "stroke-linejoin": "round"
1023
+ })
1024
+ )
1025
+ );
1026
+ }
1027
+ function stepPulse(brand) {
1028
+ return el("div", {
1029
+ className: "dpdpa-step-icon dpdpa-step-icon-active",
1030
+ style: { backgroundColor: brand.primary }
1031
+ });
1032
+ }
1033
+ var STEP_ORDER = {
1034
+ pending: 0,
1035
+ researching: 0,
1036
+ analysis_complete: 2
1037
+ };
1038
+ var STEPS = [
1039
+ { label: (company) => `Researching ${company}` },
1040
+ { label: () => "Analysing DPDPA compliance" },
1041
+ { label: () => "Generating your report" }
1042
+ ];
1043
+ function renderPolling(brand, opts) {
1044
+ const pct = Math.min(100, Math.round(opts.elapsedMs / opts.pollTimeoutMs * 100));
1045
+ const progressFill = el("div", {
1046
+ className: "dpdpa-progress-fill",
1047
+ style: { width: `${pct}%` }
1048
+ });
1049
+ const progressBar = el(
1050
+ "div",
1051
+ {
1052
+ className: "dpdpa-progress",
1053
+ role: "progressbar",
1054
+ "aria-label": "Report generation progress",
1055
+ "aria-valuemin": "0",
1056
+ "aria-valuemax": "100",
1057
+ "aria-valuenow": String(pct)
1058
+ },
1059
+ progressFill
1060
+ );
1061
+ const currentIdx = STEP_ORDER[opts.subStatus];
1062
+ const stepsList = el("div", { className: "dpdpa-steps" });
1063
+ STEPS.forEach((step, idx) => {
1064
+ if (idx > currentIdx) return;
1065
+ const isActive = idx === currentIdx;
1066
+ const row = el(
1067
+ "div",
1068
+ {
1069
+ className: `dpdpa-step ${isActive ? "dpdpa-step-active" : "dpdpa-step-done"}`
1070
+ },
1071
+ isActive ? stepPulse(brand) : stepCheck(brand),
1072
+ el(
1073
+ "span",
1074
+ {
1075
+ className: isActive ? "dpdpa-step-label dpdpa-shimmer" : "dpdpa-step-label"
1076
+ },
1077
+ step.label(opts.companyName)
1078
+ )
1079
+ );
1080
+ if (isActive) row.appendChild(animatedDots());
1081
+ stepsList.appendChild(row);
1082
+ });
1083
+ const body = el(
1084
+ "div",
1085
+ { className: "dpdpa-polling" },
1086
+ stepsList,
1087
+ el(
1088
+ "p",
1089
+ { className: "dpdpa-polling-sub" },
1090
+ "This usually takes 60 to 120 seconds. Keep this tab open."
1091
+ ),
1092
+ progressBar
1093
+ );
1094
+ return el("div", { className: "dpdpa-card" }, header(brand), body, footer(brand));
1095
+ }
1096
+ function checkIcon(brand) {
1097
+ return el(
1098
+ "div",
1099
+ {
1100
+ className: "dpdpa-check-circle",
1101
+ style: { backgroundColor: brand.successColor }
1102
+ },
1103
+ svg(
1104
+ "svg",
1105
+ {
1106
+ width: 28,
1107
+ height: 28,
1108
+ viewBox: "0 0 24 24",
1109
+ fill: "none"
1110
+ },
1111
+ svg("path", {
1112
+ d: "M5 12l5 5L20 7",
1113
+ stroke: "#ffffff",
1114
+ "stroke-width": 3,
1115
+ "stroke-linecap": "round",
1116
+ "stroke-linejoin": "round"
1117
+ })
1118
+ )
1119
+ );
1120
+ }
1121
+ function isSafeUrl(raw) {
1122
+ const s = (raw || "").trim();
1123
+ if (!s) return false;
1124
+ if (/^https?:\/\//i.test(s)) return true;
1125
+ if (/^\/[^/]/.test(s) || s.startsWith("./") || s.startsWith("../"))
1126
+ return true;
1127
+ return false;
1128
+ }
1129
+ function renderComplete(brand, opts) {
1130
+ const safeHref = isSafeUrl(opts.reportUrl) ? opts.reportUrl : "#";
1131
+ const downloadBtn = el(
1132
+ "a",
1133
+ {
1134
+ href: safeHref,
1135
+ target: "_blank",
1136
+ rel: "noopener noreferrer",
1137
+ className: "dpdpa-btn-primary dpdpa-btn-link"
1138
+ },
1139
+ "Download PDF"
1140
+ );
1141
+ const resetLink = el(
1142
+ "a",
1143
+ {
1144
+ href: "#",
1145
+ className: "dpdpa-reset-link",
1146
+ onClick: (ev) => {
1147
+ ev.preventDefault();
1148
+ opts.onReset();
1149
+ }
1150
+ },
1151
+ "Request another report"
1152
+ );
1153
+ const body = el(
1154
+ "div",
1155
+ { className: "dpdpa-success" },
1156
+ checkIcon(brand),
1157
+ el(
1158
+ "h3",
1159
+ { className: "dpdpa-success-title" },
1160
+ `Your DPDPA report for ${opts.companyName} is ready`
1161
+ ),
1162
+ el(
1163
+ "p",
1164
+ { className: "dpdpa-success-sub" },
1165
+ "Click below to download your compliance assessment PDF."
1166
+ ),
1167
+ downloadBtn,
1168
+ resetLink
1169
+ );
1170
+ return el("div", { className: "dpdpa-card" }, header(brand), body, footer(brand));
1171
+ }
1172
+ function renderError(brand, opts) {
1173
+ const retryBtn = el(
1174
+ "button",
1175
+ {
1176
+ type: "button",
1177
+ className: "dpdpa-btn-primary",
1178
+ onClick: () => opts.retryable ? opts.onRetry() : opts.onBack()
1179
+ },
1180
+ opts.retryable ? "Try again" : "Edit details"
1181
+ );
1182
+ const body = el(
1183
+ "div",
1184
+ { className: "dpdpa-error-view" },
1185
+ el("h3", { className: "dpdpa-error-title" }, "Something went wrong"),
1186
+ el("p", { className: "dpdpa-error-message" }, opts.message),
1187
+ retryBtn
1188
+ );
1189
+ return el("div", { className: "dpdpa-card" }, header(brand), body, footer(brand));
1190
+ }
1191
+
1192
+ // src/form/index.ts
1193
+ var EMPTY_FIELDS = {
1194
+ company_name: "",
1195
+ company_domain: "",
1196
+ email: ""
1197
+ };
1198
+ var FormController = class {
1199
+ constructor(options) {
1200
+ this.targetEl = null;
1201
+ this.hostEl = null;
1202
+ this.shadowRoot = null;
1203
+ this.idleRefs = null;
1204
+ this.pollTimer = null;
1205
+ this.pollStartedAt = 0;
1206
+ this.pollHiddenSince = null;
1207
+ this.pollHiddenTotalMs = 0;
1208
+ this.pollResumePending = false;
1209
+ this.lastSubStatus = null;
1210
+ this.consecutiveErrors = 0;
1211
+ this.destroyed = false;
1212
+ this.unsubscribe = null;
1213
+ this.visibilityHandler = null;
1214
+ this.options = options;
1215
+ this.brand = resolveBranding(options.branding);
1216
+ this.fields = { ...EMPTY_FIELDS };
1217
+ this.store = new Store({
1218
+ phase: "idle",
1219
+ subStatus: "pending",
1220
+ elapsedMs: 0,
1221
+ jobId: null,
1222
+ reportUrl: null,
1223
+ errors: {},
1224
+ generalError: null,
1225
+ errorMessage: "",
1226
+ retryable: true
1227
+ });
1228
+ }
1229
+ async init() {
1230
+ if (typeof this.options.target === "string") {
1231
+ const found = document.querySelector(this.options.target);
1232
+ if (!found)
1233
+ throw new Error(
1234
+ `DPDPA report form: target element not found for selector "${this.options.target}"`
1235
+ );
1236
+ this.targetEl = found;
1237
+ } else {
1238
+ this.targetEl = this.options.target;
1239
+ }
1240
+ this.hostEl = document.createElement("div");
1241
+ this.hostEl.setAttribute("data-redacto-dpdpa-report", "");
1242
+ this.hostEl.style.display = "block";
1243
+ this.targetEl.appendChild(this.hostEl);
1244
+ this.shadowRoot = this.hostEl.attachShadow({ mode: "open" });
1245
+ injectFormStyles(this.shadowRoot, this.brand);
1246
+ this.unsubscribe = this.store.subscribe((state, prev) => {
1247
+ if (state.phase !== prev.phase) this.render();
1248
+ else if (state.phase === "polling" && state.subStatus !== prev.subStatus)
1249
+ this.render();
1250
+ else if (state.phase === "polling" && state.elapsedMs !== prev.elapsedMs)
1251
+ this.updateProgress();
1252
+ });
1253
+ this.visibilityHandler = () => this.handleVisibilityChange();
1254
+ document.addEventListener("visibilitychange", this.visibilityHandler);
1255
+ this.render();
1256
+ }
1257
+ destroy() {
1258
+ var _a;
1259
+ this.destroyed = true;
1260
+ this.clearPolling();
1261
+ (_a = this.unsubscribe) == null ? void 0 : _a.call(this);
1262
+ this.unsubscribe = null;
1263
+ if (this.visibilityHandler) {
1264
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
1265
+ this.visibilityHandler = null;
1266
+ }
1267
+ this.store.destroy();
1268
+ if (this.hostEl && this.hostEl.parentNode)
1269
+ this.hostEl.parentNode.removeChild(this.hostEl);
1270
+ this.hostEl = null;
1271
+ this.shadowRoot = null;
1272
+ this.targetEl = null;
1273
+ this.idleRefs = null;
1274
+ }
1275
+ render() {
1276
+ if (!this.shadowRoot) return;
1277
+ clearElement(this.shadowRoot);
1278
+ injectFormStyles(this.shadowRoot, this.brand);
1279
+ this.idleRefs = null;
1280
+ const s = this.store.getState();
1281
+ let node;
1282
+ switch (s.phase) {
1283
+ case "idle":
1284
+ this.idleRefs = renderIdle(
1285
+ this.brand,
1286
+ this.fields,
1287
+ s.errors,
1288
+ s.generalError,
1289
+ {
1290
+ onInput: (field, value) => this.handleInput(field, value),
1291
+ onBlur: (field) => this.handleBlur(field),
1292
+ onSubmit: () => this.handleSubmit()
1293
+ }
1294
+ );
1295
+ node = this.idleRefs.root;
1296
+ break;
1297
+ case "submitting":
1298
+ node = renderSubmitting(this.brand, this.fields);
1299
+ break;
1300
+ case "polling":
1301
+ node = renderPolling(this.brand, {
1302
+ companyName: this.fields.company_name,
1303
+ subStatus: s.subStatus,
1304
+ elapsedMs: s.elapsedMs,
1305
+ pollTimeoutMs: this.getPollTimeout()
1306
+ });
1307
+ break;
1308
+ case "complete":
1309
+ node = renderComplete(this.brand, {
1310
+ companyName: this.fields.company_name,
1311
+ reportUrl: s.reportUrl || "#",
1312
+ onReset: () => this.resetToIdle()
1313
+ });
1314
+ break;
1315
+ case "error":
1316
+ node = renderError(this.brand, {
1317
+ message: s.errorMessage,
1318
+ retryable: s.retryable,
1319
+ onRetry: () => this.submit(),
1320
+ onBack: () => {
1321
+ var _a;
1322
+ return this.backToIdle((_a = s.generalError) != null ? _a : s.errorMessage);
1323
+ }
1324
+ });
1325
+ break;
1326
+ }
1327
+ this.shadowRoot.appendChild(node);
1328
+ if (s.phase === "idle" && this.idleRefs) {
1329
+ const firstErrorField = Object.keys(s.errors).find(
1330
+ (f) => !!s.errors[f]
1331
+ );
1332
+ if (firstErrorField) this.idleRefs.fieldInputs[firstErrorField].focus();
1333
+ }
1334
+ }
1335
+ updateProgress() {
1336
+ if (!this.shadowRoot) return;
1337
+ const bar = this.shadowRoot.querySelector(".dpdpa-progress-fill");
1338
+ const track = this.shadowRoot.querySelector(".dpdpa-progress");
1339
+ if (!bar) return;
1340
+ const pct = Math.min(
1341
+ 100,
1342
+ Math.round(this.store.getState().elapsedMs / this.getPollTimeout() * 100)
1343
+ );
1344
+ bar.style.width = `${pct}%`;
1345
+ if (track) track.setAttribute("aria-valuenow", String(pct));
1346
+ }
1347
+ handleInput(field, value) {
1348
+ this.fields[field] = value;
1349
+ const prev = this.store.getState().errors[field];
1350
+ if (prev) {
1351
+ const next = { ...this.store.getState().errors };
1352
+ delete next[field];
1353
+ this.store.setState({ errors: next });
1354
+ if (this.idleRefs) this.clearFieldErrorSlot(field);
1355
+ }
1356
+ }
1357
+ handleBlur(field) {
1358
+ const err = validateField(field, this.fields[field]);
1359
+ const next = { ...this.store.getState().errors };
1360
+ if (err) next[field] = err;
1361
+ else delete next[field];
1362
+ this.store.setState({ errors: next });
1363
+ this.updateFieldErrorSlot(field, err);
1364
+ }
1365
+ updateFieldErrorSlot(field, err) {
1366
+ if (!this.idleRefs) return;
1367
+ const slot = this.idleRefs.fieldErrors[field];
1368
+ if (err) {
1369
+ slot.textContent = err;
1370
+ slot.style.display = "block";
1371
+ } else {
1372
+ slot.textContent = "";
1373
+ slot.style.display = "none";
1374
+ }
1375
+ }
1376
+ clearFieldErrorSlot(field) {
1377
+ if (!this.idleRefs) return;
1378
+ const slot = this.idleRefs.fieldErrors[field];
1379
+ slot.textContent = "";
1380
+ slot.style.display = "none";
1381
+ }
1382
+ handleSubmit() {
1383
+ const normalizedFields = {
1384
+ company_name: this.fields.company_name.trim(),
1385
+ company_domain: normalizeDomain(this.fields.company_domain),
1386
+ email: this.fields.email.trim()
1387
+ };
1388
+ this.fields = normalizedFields;
1389
+ const errors = validateAll(normalizedFields);
1390
+ if (Object.keys(errors).length > 0) {
1391
+ this.store.setState({ errors, generalError: null });
1392
+ this.render();
1393
+ this.emitError("One or more fields need your attention.", "validation");
1394
+ return;
1395
+ }
1396
+ this.store.setState({ errors: {}, generalError: null });
1397
+ this.submit();
1398
+ }
1399
+ async submit() {
1400
+ var _a, _b, _c, _d;
1401
+ const lead = this.makeLead(null);
1402
+ try {
1403
+ (_b = (_a = this.options).onSubmit) == null ? void 0 : _b.call(_a, {
1404
+ email: lead.email,
1405
+ company_name: lead.company_name,
1406
+ company_domain: lead.company_domain,
1407
+ timestamp: lead.timestamp
1408
+ });
1409
+ } catch (err) {
1410
+ console.error("onSubmit handler threw:", err);
1411
+ }
1412
+ saveLead(lead);
1413
+ this.store.setState({ phase: "submitting" });
1414
+ try {
1415
+ const res = await submitLead({
1416
+ baseUrl: this.getBaseUrl(),
1417
+ body: {
1418
+ company_name: lead.company_name,
1419
+ company_domain: lead.company_domain
1420
+ }
1421
+ });
1422
+ if (this.destroyed) return;
1423
+ if (res.status === "error") {
1424
+ this.showError(res.message || "We couldn't accept that submission.", false);
1425
+ this.emitError(res.message || "Submission rejected", "submit");
1426
+ return;
1427
+ }
1428
+ const acceptedLead = { ...lead, job_id: res.job_id };
1429
+ saveLead(acceptedLead);
1430
+ try {
1431
+ (_d = (_c = this.options).onJobAccepted) == null ? void 0 : _d.call(_c, res.job_id, acceptedLead);
1432
+ } catch (err) {
1433
+ console.error("onJobAccepted handler threw:", err);
1434
+ }
1435
+ this.startPolling(res.job_id);
1436
+ } catch (err) {
1437
+ if (this.destroyed) return;
1438
+ const apiErr = err;
1439
+ const retryable = !apiErr.status || apiErr.status >= 500;
1440
+ this.showError(
1441
+ apiErr.message || "We couldn't reach our server. Please try again.",
1442
+ retryable
1443
+ );
1444
+ this.emitError(apiErr.message || "Submit failed", "submit", apiErr.status);
1445
+ }
1446
+ }
1447
+ startPolling(jobId) {
1448
+ this.clearPolling();
1449
+ this.pollStartedAt = Date.now();
1450
+ this.pollHiddenSince = null;
1451
+ this.pollHiddenTotalMs = 0;
1452
+ this.lastSubStatus = null;
1453
+ this.consecutiveErrors = 0;
1454
+ this.store.setState({
1455
+ phase: "polling",
1456
+ subStatus: "pending",
1457
+ elapsedMs: 0,
1458
+ jobId
1459
+ });
1460
+ this.schedulePoll();
1461
+ }
1462
+ schedulePoll() {
1463
+ var _a;
1464
+ if (this.destroyed) return;
1465
+ if (typeof document !== "undefined" && document.hidden) {
1466
+ if (this.pollHiddenSince === null) this.pollHiddenSince = Date.now();
1467
+ this.pollResumePending = true;
1468
+ return;
1469
+ }
1470
+ const interval = (_a = this.options.pollIntervalMs) != null ? _a : DEFAULT_POLL_INTERVAL_MS;
1471
+ this.pollTimer = setTimeout(() => this.pollTick(), interval);
1472
+ }
1473
+ getActiveElapsed() {
1474
+ return Date.now() - this.pollStartedAt - this.pollHiddenTotalMs;
1475
+ }
1476
+ async pollTick() {
1477
+ var _a, _b, _c, _d;
1478
+ if (this.destroyed) return;
1479
+ const jobId = this.store.getState().jobId;
1480
+ if (!jobId) return;
1481
+ const activeElapsed = this.getActiveElapsed();
1482
+ if (activeElapsed >= this.getPollTimeout()) {
1483
+ this.showError(
1484
+ "Report generation is taking longer than expected. Our team has been notified.",
1485
+ false
1486
+ );
1487
+ this.emitError("Polling timed out", "timeout");
1488
+ return;
1489
+ }
1490
+ try {
1491
+ const res = await fetchJobStatus({ baseUrl: this.getBaseUrl(), jobId });
1492
+ if (this.destroyed) return;
1493
+ this.consecutiveErrors = 0;
1494
+ if ("status" in res && res.status === "error") {
1495
+ this.showError(res.message || "Report generation failed.", true);
1496
+ this.emitError(res.message || "Job errored", "poll");
1497
+ return;
1498
+ }
1499
+ if (res.status === "complete" && "report_url" in res && res.report_url) {
1500
+ const acceptedLead = this.makeLead(jobId);
1501
+ saveLead(acceptedLead);
1502
+ this.store.setState({
1503
+ phase: "complete",
1504
+ reportUrl: res.report_url
1505
+ });
1506
+ try {
1507
+ (_b = (_a = this.options).onComplete) == null ? void 0 : _b.call(_a, res.report_url, acceptedLead);
1508
+ } catch (err) {
1509
+ console.error("onComplete handler threw:", err);
1510
+ }
1511
+ return;
1512
+ }
1513
+ const sub = res.status;
1514
+ if (sub !== this.lastSubStatus) {
1515
+ this.lastSubStatus = sub;
1516
+ try {
1517
+ (_d = (_c = this.options).onStatusChange) == null ? void 0 : _d.call(_c, sub);
1518
+ } catch (err) {
1519
+ console.error("onStatusChange handler threw:", err);
1520
+ }
1521
+ }
1522
+ this.store.setState({
1523
+ phase: "polling",
1524
+ subStatus: sub,
1525
+ elapsedMs: activeElapsed
1526
+ });
1527
+ this.schedulePoll();
1528
+ } catch (err) {
1529
+ if (this.destroyed) return;
1530
+ this.consecutiveErrors++;
1531
+ if (this.consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1532
+ this.showError(
1533
+ "We can't reach our server right now. Please try again.",
1534
+ true
1535
+ );
1536
+ this.emitError(
1537
+ err instanceof Error ? err.message : "Poll failed",
1538
+ "poll"
1539
+ );
1540
+ return;
1541
+ }
1542
+ this.schedulePoll();
1543
+ }
1544
+ }
1545
+ clearPolling() {
1546
+ if (this.pollTimer) {
1547
+ clearTimeout(this.pollTimer);
1548
+ this.pollTimer = null;
1549
+ }
1550
+ this.pollResumePending = false;
1551
+ }
1552
+ handleVisibilityChange() {
1553
+ if (this.destroyed) return;
1554
+ if (document.hidden) {
1555
+ if (this.store.getState().phase === "polling" && this.pollHiddenSince === null) {
1556
+ this.pollHiddenSince = Date.now();
1557
+ }
1558
+ return;
1559
+ }
1560
+ if (this.pollHiddenSince !== null) {
1561
+ this.pollHiddenTotalMs += Date.now() - this.pollHiddenSince;
1562
+ this.pollHiddenSince = null;
1563
+ }
1564
+ if (this.pollResumePending && this.store.getState().phase === "polling") {
1565
+ this.pollResumePending = false;
1566
+ this.pollTick();
1567
+ }
1568
+ }
1569
+ showError(message, retryable) {
1570
+ this.store.setState({
1571
+ phase: "error",
1572
+ errorMessage: message,
1573
+ retryable
1574
+ });
1575
+ }
1576
+ resetToIdle() {
1577
+ this.fields = { ...EMPTY_FIELDS };
1578
+ this.lastSubStatus = null;
1579
+ this.store.setState({
1580
+ phase: "idle",
1581
+ errors: {},
1582
+ generalError: null,
1583
+ errorMessage: "",
1584
+ reportUrl: null,
1585
+ jobId: null,
1586
+ subStatus: "pending",
1587
+ elapsedMs: 0
1588
+ });
1589
+ }
1590
+ backToIdle(generalError) {
1591
+ this.store.setState({
1592
+ phase: "idle",
1593
+ errors: {},
1594
+ generalError,
1595
+ errorMessage: ""
1596
+ });
1597
+ }
1598
+ emitError(message, phase, status) {
1599
+ var _a, _b;
1600
+ try {
1601
+ const err = Object.assign(new Error(message), { phase, status });
1602
+ (_b = (_a = this.options).onError) == null ? void 0 : _b.call(_a, err);
1603
+ } catch (e) {
1604
+ console.error("onError handler threw:", e);
1605
+ }
1606
+ }
1607
+ makeLead(jobId) {
1608
+ return {
1609
+ email: this.fields.email,
1610
+ company_name: this.fields.company_name,
1611
+ company_domain: this.fields.company_domain,
1612
+ job_id: jobId,
1613
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1614
+ };
1615
+ }
1616
+ getBaseUrl() {
1617
+ return (this.options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
1618
+ }
1619
+ getPollTimeout() {
1620
+ var _a;
1621
+ return (_a = this.options.pollTimeoutMs) != null ? _a : DEFAULT_POLL_TIMEOUT_MS;
1622
+ }
1623
+ };
1624
+
1625
+ // src/index.ts
1626
+ var version = "0.1.0";
1627
+ var activeController = null;
1628
+ function init(options) {
1629
+ destroy();
1630
+ activeController = new FormController(options);
1631
+ activeController.init().catch((err) => {
1632
+ var _a;
1633
+ try {
1634
+ (_a = options.onError) == null ? void 0 : _a.call(
1635
+ options,
1636
+ Object.assign(err instanceof Error ? err : new Error(String(err)), {
1637
+ phase: "validation"
1638
+ })
1639
+ );
1640
+ } catch (handlerErr) {
1641
+ console.error("onError handler threw:", handlerErr);
1642
+ }
1643
+ });
1644
+ }
1645
+ function destroy() {
1646
+ if (activeController) {
1647
+ activeController.destroy();
1648
+ activeController = null;
1649
+ }
1650
+ }
1651
+ function getCapturedLead() {
1652
+ return loadLead();
1653
+ }
1654
+ var RedactoDpdpaReport = { init, destroy, getCapturedLead, version };
1655
+ var src_default = RedactoDpdpaReport;
1656
+ export {
1657
+ src_default as default,
1658
+ destroy,
1659
+ getCapturedLead,
1660
+ init,
1661
+ version
1662
+ };