@flamingo-stack/openframe-frontend-core 0.0.671 → 0.0.672-snapshot.20260928034232

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 (179) hide show
  1. package/dist/chat-protocol/card-marker.d.ts +12 -0
  2. package/dist/chat-protocol/card-marker.d.ts.map +1 -1
  3. package/dist/chat-protocol/index.cjs +17 -0
  4. package/dist/chat-protocol/index.cjs.map +1 -1
  5. package/dist/chat-protocol/index.d.ts +1 -0
  6. package/dist/chat-protocol/index.d.ts.map +1 -1
  7. package/dist/chat-protocol/index.js +15 -1
  8. package/dist/chat-protocol/index.js.map +1 -1
  9. package/dist/{chunk-DAGKBFZN.cjs → chunk-26NSIJMT.cjs} +33 -33
  10. package/dist/{chunk-DAGKBFZN.cjs.map → chunk-26NSIJMT.cjs.map} +1 -1
  11. package/dist/{chunk-FUXUJSPP.cjs → chunk-3UDBK2YG.cjs} +1 -1
  12. package/dist/chunk-3UDBK2YG.cjs.map +1 -0
  13. package/dist/{chunk-XGXHHSDU.cjs → chunk-6QUKYN4K.cjs} +22 -22
  14. package/dist/{chunk-XGXHHSDU.cjs.map → chunk-6QUKYN4K.cjs.map} +1 -1
  15. package/dist/{chunk-SNNE34OP.js → chunk-BF7GZ6BS.js} +5 -5
  16. package/dist/{chunk-SJZPKIW6.cjs → chunk-DRURJCEY.cjs} +12 -12
  17. package/dist/{chunk-SJZPKIW6.cjs.map → chunk-DRURJCEY.cjs.map} +1 -1
  18. package/dist/{chunk-YIDNSRPL.cjs → chunk-G262S3Q5.cjs} +31 -31
  19. package/dist/{chunk-YIDNSRPL.cjs.map → chunk-G262S3Q5.cjs.map} +1 -1
  20. package/dist/{chunk-OG2BNJ2Q.cjs → chunk-HJ532KQR.cjs} +390 -375
  21. package/dist/chunk-HJ532KQR.cjs.map +1 -0
  22. package/dist/{chunk-K3O4D7LW.js → chunk-HRM6242Y.js} +4 -4
  23. package/dist/{chunk-BP542DGQ.cjs → chunk-IFQMIYFO.cjs} +55 -28
  24. package/dist/chunk-IFQMIYFO.cjs.map +1 -0
  25. package/dist/{chunk-IJIIL6WU.js → chunk-IG7M3XKX.js} +6 -6
  26. package/dist/{chunk-PQ7YC5Y6.js → chunk-II6ABVLK.js} +3 -3
  27. package/dist/{chunk-TLLTL6LB.js → chunk-JD4U7QMG.js} +1 -1
  28. package/dist/chunk-JD4U7QMG.js.map +1 -0
  29. package/dist/{chunk-ZRIVCLPR.js → chunk-JD5TKRCU.js} +1 -1
  30. package/dist/chunk-JD5TKRCU.js.map +1 -0
  31. package/dist/{chunk-XH4MRTZM.cjs → chunk-K365MY6G.cjs} +5 -5
  32. package/dist/{chunk-XH4MRTZM.cjs.map → chunk-K365MY6G.cjs.map} +1 -1
  33. package/dist/{chunk-EIAVE6SE.cjs → chunk-KV7W5T6U.cjs} +10 -10
  34. package/dist/{chunk-EIAVE6SE.cjs.map → chunk-KV7W5T6U.cjs.map} +1 -1
  35. package/dist/{chunk-II66XWU5.cjs → chunk-KYF5EONP.cjs} +3 -3
  36. package/dist/{chunk-II66XWU5.cjs.map → chunk-KYF5EONP.cjs.map} +1 -1
  37. package/dist/{chunk-D55IENZJ.cjs → chunk-LCMT2GZK.cjs} +1 -1
  38. package/dist/chunk-LCMT2GZK.cjs.map +1 -0
  39. package/dist/{chunk-STLBB6PF.js → chunk-LMUZZUYC.js} +35 -8
  40. package/dist/chunk-LMUZZUYC.js.map +1 -0
  41. package/dist/{chunk-FAJUQKWQ.cjs → chunk-N6TC6IPV.cjs} +76 -75
  42. package/dist/chunk-N6TC6IPV.cjs.map +1 -0
  43. package/dist/{chunk-RKUCK6MN.js → chunk-OGOMAK7B.js} +2 -2
  44. package/dist/{chunk-G64BNLDO.js → chunk-P4GUIOLP.js} +2 -2
  45. package/dist/{chunk-DUZEFACP.js → chunk-RRU46J3N.js} +3 -3
  46. package/dist/{chunk-DAWYS25M.cjs → chunk-THSIFQGS.cjs} +17 -17
  47. package/dist/{chunk-DAWYS25M.cjs.map → chunk-THSIFQGS.cjs.map} +1 -1
  48. package/dist/{chunk-6C6UUOH4.js → chunk-TTHBFT7Q.js} +8 -7
  49. package/dist/{chunk-6C6UUOH4.js.map → chunk-TTHBFT7Q.js.map} +1 -1
  50. package/dist/{chunk-YNGAJGYB.cjs → chunk-TZP2OXHG.cjs} +346 -6
  51. package/dist/chunk-TZP2OXHG.cjs.map +1 -0
  52. package/dist/{chunk-T3ACDOVD.cjs → chunk-U2H3C4NT.cjs} +7 -7
  53. package/dist/{chunk-T3ACDOVD.cjs.map → chunk-U2H3C4NT.cjs.map} +1 -1
  54. package/dist/{chunk-OYMOG544.js → chunk-UFMXQKVT.js} +5 -5
  55. package/dist/{chunk-NIYIK32F.js → chunk-UMTXGEPU.js} +19 -4
  56. package/dist/{chunk-NIYIK32F.js.map → chunk-UMTXGEPU.js.map} +1 -1
  57. package/dist/{chunk-3PVOF3IB.js → chunk-UTHZ7DLN.js} +343 -3
  58. package/dist/{chunk-3PVOF3IB.js.map → chunk-UTHZ7DLN.js.map} +1 -1
  59. package/dist/{chunk-YJWR7D6O.cjs → chunk-UXT5EK27.cjs} +3 -3
  60. package/dist/{chunk-YJWR7D6O.cjs.map → chunk-UXT5EK27.cjs.map} +1 -1
  61. package/dist/{chunk-UH3BL2OT.js → chunk-VK5737GE.js} +2 -2
  62. package/dist/{chunk-R22IA7MP.js → chunk-VKLM5UKG.js} +4 -4
  63. package/dist/{chunk-FHTNCWIP.js → chunk-X2DR3XNN.js} +5 -5
  64. package/dist/{chunk-GF5TSQ6C.cjs → chunk-YCYOZAB5.cjs} +10 -10
  65. package/dist/{chunk-GF5TSQ6C.cjs.map → chunk-YCYOZAB5.cjs.map} +1 -1
  66. package/dist/{chunk-4PVY4JJN.js → chunk-YWQVL5VJ.js} +3 -3
  67. package/dist/{chunk-PLG6LP7E.cjs → chunk-Z5K4IQZU.cjs} +49 -49
  68. package/dist/{chunk-PLG6LP7E.cjs.map → chunk-Z5K4IQZU.cjs.map} +1 -1
  69. package/dist/components/case-studies/index.cjs +18 -17
  70. package/dist/components/case-studies/index.cjs.map +1 -1
  71. package/dist/components/case-studies/index.js +7 -6
  72. package/dist/components/case-studies/index.js.map +1 -1
  73. package/dist/components/case-studies/share-experience-section.d.ts.map +1 -1
  74. package/dist/components/chat/index.cjs +2 -2
  75. package/dist/components/chat/index.js +1 -1
  76. package/dist/components/contact/contact-form.d.ts +6 -1
  77. package/dist/components/contact/contact-form.d.ts.map +1 -1
  78. package/dist/components/contact/index.cjs +6 -6
  79. package/dist/components/contact/index.js +5 -5
  80. package/dist/components/docs/index.cjs +9 -9
  81. package/dist/components/docs/index.js +8 -8
  82. package/dist/components/embeds/index.cjs +6 -6
  83. package/dist/components/embeds/index.js +5 -5
  84. package/dist/components/faq/index.cjs +6 -6
  85. package/dist/components/faq/index.js +5 -5
  86. package/dist/components/features/index.cjs +5 -5
  87. package/dist/components/features/index.js +4 -4
  88. package/dist/components/features/waitlist-form.d.ts +7 -2
  89. package/dist/components/features/waitlist-form.d.ts.map +1 -1
  90. package/dist/components/help-center-pages/index.cjs +77 -76
  91. package/dist/components/help-center-pages/index.cjs.map +1 -1
  92. package/dist/components/help-center-pages/index.js +17 -16
  93. package/dist/components/help-center-pages/index.js.map +1 -1
  94. package/dist/components/help-center-pages/trust-center-sections.d.ts.map +1 -1
  95. package/dist/components/index.cjs +43 -43
  96. package/dist/components/index.js +15 -15
  97. package/dist/components/meeting-scheduler/booking-form.d.ts +4 -1
  98. package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
  99. package/dist/components/meeting-scheduler/index.cjs +75 -40
  100. package/dist/components/meeting-scheduler/index.cjs.map +1 -1
  101. package/dist/components/meeting-scheduler/index.d.ts +5 -1
  102. package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
  103. package/dist/components/meeting-scheduler/index.js +46 -11
  104. package/dist/components/meeting-scheduler/index.js.map +1 -1
  105. package/dist/components/navigation/index.cjs +8 -8
  106. package/dist/components/navigation/index.js +7 -7
  107. package/dist/components/onboarding-guides/index.cjs +8 -8
  108. package/dist/components/onboarding-guides/index.js +7 -7
  109. package/dist/components/tickets/help-center-create-form.d.ts.map +1 -1
  110. package/dist/components/tickets/index.cjs +9 -9
  111. package/dist/components/tickets/index.js +8 -8
  112. package/dist/components/ui/index.cjs +5 -5
  113. package/dist/components/ui/index.js +4 -4
  114. package/dist/contexts/endpoints-runtime-context.d.ts +5 -0
  115. package/dist/contexts/endpoints-runtime-context.d.ts.map +1 -1
  116. package/dist/contexts/index.cjs +2 -2
  117. package/dist/contexts/index.js +1 -1
  118. package/dist/hooks/index.cjs +5 -3
  119. package/dist/hooks/index.cjs.map +1 -1
  120. package/dist/hooks/index.d.ts +1 -0
  121. package/dist/hooks/index.d.ts.map +1 -1
  122. package/dist/hooks/index.js +4 -2
  123. package/dist/hooks/use-form-rescue.d.ts +37 -0
  124. package/dist/hooks/use-form-rescue.d.ts.map +1 -0
  125. package/dist/index.cjs +53 -9
  126. package/dist/index.cjs.map +1 -1
  127. package/dist/index.js +52 -8
  128. package/dist/index.js.map +1 -1
  129. package/dist/utils/form-rescue.cjs +123 -0
  130. package/dist/utils/form-rescue.cjs.map +1 -0
  131. package/dist/utils/form-rescue.d.ts +105 -0
  132. package/dist/utils/form-rescue.d.ts.map +1 -0
  133. package/dist/utils/form-rescue.js +101 -0
  134. package/dist/utils/form-rescue.js.map +1 -0
  135. package/dist/utils/index.cjs +119 -0
  136. package/dist/utils/index.cjs.map +1 -1
  137. package/dist/utils/index.d.ts +1 -0
  138. package/dist/utils/index.d.ts.map +1 -1
  139. package/dist/utils/index.js +99 -1
  140. package/dist/utils/index.js.map +1 -1
  141. package/package.json +7 -1
  142. package/src/chat-protocol/__tests__/card-marker.test.ts +27 -1
  143. package/src/chat-protocol/card-marker.ts +24 -0
  144. package/src/chat-protocol/index.ts +3 -0
  145. package/src/components/case-studies/share-experience-section.tsx +1 -0
  146. package/src/components/contact/contact-form.tsx +38 -2
  147. package/src/components/features/waitlist-form.tsx +27 -2
  148. package/src/components/help-center-pages/trust-center-sections.tsx +1 -0
  149. package/src/components/meeting-scheduler/booking-form.tsx +14 -0
  150. package/src/components/meeting-scheduler/index.tsx +38 -3
  151. package/src/components/tickets/help-center-create-form.tsx +2 -0
  152. package/src/contexts/endpoints-runtime-context.tsx +5 -0
  153. package/src/hooks/__tests__/use-form-rescue.test.tsx +189 -0
  154. package/src/hooks/index.ts +3 -0
  155. package/src/hooks/use-form-rescue.ts +315 -0
  156. package/src/utils/__tests__/form-rescue.test.ts +106 -0
  157. package/src/utils/form-rescue.ts +198 -0
  158. package/src/utils/index.ts +5 -0
  159. package/dist/chunk-BP542DGQ.cjs.map +0 -1
  160. package/dist/chunk-D55IENZJ.cjs.map +0 -1
  161. package/dist/chunk-FAJUQKWQ.cjs.map +0 -1
  162. package/dist/chunk-FUXUJSPP.cjs.map +0 -1
  163. package/dist/chunk-OG2BNJ2Q.cjs.map +0 -1
  164. package/dist/chunk-STLBB6PF.js.map +0 -1
  165. package/dist/chunk-TLLTL6LB.js.map +0 -1
  166. package/dist/chunk-YNGAJGYB.cjs.map +0 -1
  167. package/dist/chunk-ZRIVCLPR.js.map +0 -1
  168. /package/dist/{chunk-SNNE34OP.js.map → chunk-BF7GZ6BS.js.map} +0 -0
  169. /package/dist/{chunk-K3O4D7LW.js.map → chunk-HRM6242Y.js.map} +0 -0
  170. /package/dist/{chunk-IJIIL6WU.js.map → chunk-IG7M3XKX.js.map} +0 -0
  171. /package/dist/{chunk-PQ7YC5Y6.js.map → chunk-II6ABVLK.js.map} +0 -0
  172. /package/dist/{chunk-RKUCK6MN.js.map → chunk-OGOMAK7B.js.map} +0 -0
  173. /package/dist/{chunk-G64BNLDO.js.map → chunk-P4GUIOLP.js.map} +0 -0
  174. /package/dist/{chunk-DUZEFACP.js.map → chunk-RRU46J3N.js.map} +0 -0
  175. /package/dist/{chunk-OYMOG544.js.map → chunk-UFMXQKVT.js.map} +0 -0
  176. /package/dist/{chunk-UH3BL2OT.js.map → chunk-VK5737GE.js.map} +0 -0
  177. /package/dist/{chunk-R22IA7MP.js.map → chunk-VKLM5UKG.js.map} +0 -0
  178. /package/dist/{chunk-FHTNCWIP.js.map → chunk-X2DR3XNN.js.map} +0 -0
  179. /package/dist/{chunk-4PVY4JJN.js.map → chunk-YWQVL5VJ.js.map} +0 -0
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ // src/utils/form-rescue.ts
4
+ var FORM_RESCUE_FORM_IDS = [
5
+ "contact",
6
+ "case_study_pitch",
7
+ "data_room_request",
8
+ "trust_center_request",
9
+ "tmcg_join",
10
+ "meeting_booking",
11
+ "waitlist"
12
+ ];
13
+ function isFormRescueFormId(value) {
14
+ return typeof value === "string" && FORM_RESCUE_FORM_IDS.includes(value);
15
+ }
16
+ var FORM_RESCUE_VALUE_FIELDS = [
17
+ "email",
18
+ "name",
19
+ "firstName",
20
+ "lastName",
21
+ "company",
22
+ "jobtitle",
23
+ "companySize",
24
+ "linkedin_url"
25
+ ];
26
+ var EXCLUDED_FIELD_PATTERN = /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;
27
+ function isExcludedRescueField(name) {
28
+ return EXCLUDED_FIELD_PATTERN.test(name);
29
+ }
30
+ var FORM_RESCUE_MAX_VALUE_CHARS = 200;
31
+ var FORM_RESCUE_DEBOUNCE_MS = 1500;
32
+ var FORM_RESCUE_MAX_FIELDS = 40;
33
+ var FORM_RESCUE_ATTEMPT_FIELD = "form_attempt_id";
34
+ var FORM_RESCUE_RESUME_FIELD = "form_resume_token";
35
+ var FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD];
36
+ var FORM_RESCUE_RESUME_PARAM = "resume";
37
+ var FORM_DRAFT_STATUSES = ["started", "rescuable", "rescued", "submitted"];
38
+ var FORM_RESCUE_SKIP_REASONS = {
39
+ finishedElsewhere: "finished_elsewhere",
40
+ alreadyRescued: "already_rescued",
41
+ internal: "internal_email"
42
+ };
43
+ var EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
44
+ function isRescueEmail(value) {
45
+ return typeof value === "string" && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());
46
+ }
47
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
48
+ function isFormAttemptId(value) {
49
+ return typeof value === "string" && UUID_PATTERN.test(value);
50
+ }
51
+ function isFilled(value) {
52
+ if (value == null) return false;
53
+ if (typeof value === "string") return value.trim().length > 0;
54
+ if (typeof value === "boolean") return value;
55
+ if (Array.isArray(value)) return value.length > 0;
56
+ if (typeof value === "object") return Object.values(value).some(isFilled);
57
+ return true;
58
+ }
59
+ function sanitizeRescueValues(values) {
60
+ const out = {};
61
+ if (!values || typeof values !== "object") return out;
62
+ for (const key of FORM_RESCUE_VALUE_FIELDS) {
63
+ if (isExcludedRescueField(key)) continue;
64
+ const raw = values[key];
65
+ if (typeof raw !== "string") continue;
66
+ const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);
67
+ if (trimmed) out[key] = trimmed;
68
+ }
69
+ return out;
70
+ }
71
+ function filledRescueFields(values, fieldNames) {
72
+ return fieldNames.filter((name) => !isExcludedRescueField(name) && isFilled(values[name])).slice(0, FORM_RESCUE_MAX_FIELDS);
73
+ }
74
+ function rescueCompletionPct(filledCount, totalCount) {
75
+ if (totalCount <= 0) return 0;
76
+ return Math.max(0, Math.min(100, Math.round(filledCount / totalCount * 100)));
77
+ }
78
+ function sanitizeRescueFieldName(value) {
79
+ if (typeof value !== "string") return null;
80
+ const trimmed = value.trim();
81
+ return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;
82
+ }
83
+ var FORM_RESCUE_EVENTS = {
84
+ started: "form_started",
85
+ progress: "form_progress",
86
+ leadCaptured: "form_lead_captured",
87
+ abandoned: "form_abandoned",
88
+ submitted: "form_submitted",
89
+ rescued: "form_rescued",
90
+ resumed: "form_resumed"
91
+ };
92
+ function captureFormRescueEvent(event, properties) {
93
+ try {
94
+ if (typeof window === "undefined") return;
95
+ const ph = window.posthog;
96
+ if (typeof ph?.capture === "function") ph.capture(event, properties);
97
+ } catch {
98
+ }
99
+ }
100
+
101
+ exports.FORM_DRAFT_STATUSES = FORM_DRAFT_STATUSES;
102
+ exports.FORM_RESCUE_ATTEMPT_FIELD = FORM_RESCUE_ATTEMPT_FIELD;
103
+ exports.FORM_RESCUE_DEBOUNCE_MS = FORM_RESCUE_DEBOUNCE_MS;
104
+ exports.FORM_RESCUE_EVENTS = FORM_RESCUE_EVENTS;
105
+ exports.FORM_RESCUE_FORM_IDS = FORM_RESCUE_FORM_IDS;
106
+ exports.FORM_RESCUE_MAX_FIELDS = FORM_RESCUE_MAX_FIELDS;
107
+ exports.FORM_RESCUE_MAX_VALUE_CHARS = FORM_RESCUE_MAX_VALUE_CHARS;
108
+ exports.FORM_RESCUE_RESUME_FIELD = FORM_RESCUE_RESUME_FIELD;
109
+ exports.FORM_RESCUE_RESUME_PARAM = FORM_RESCUE_RESUME_PARAM;
110
+ exports.FORM_RESCUE_SKIP_REASONS = FORM_RESCUE_SKIP_REASONS;
111
+ exports.FORM_RESCUE_SUBMIT_KEYS = FORM_RESCUE_SUBMIT_KEYS;
112
+ exports.FORM_RESCUE_VALUE_FIELDS = FORM_RESCUE_VALUE_FIELDS;
113
+ exports.captureFormRescueEvent = captureFormRescueEvent;
114
+ exports.filledRescueFields = filledRescueFields;
115
+ exports.isExcludedRescueField = isExcludedRescueField;
116
+ exports.isFormAttemptId = isFormAttemptId;
117
+ exports.isFormRescueFormId = isFormRescueFormId;
118
+ exports.isRescueEmail = isRescueEmail;
119
+ exports.rescueCompletionPct = rescueCompletionPct;
120
+ exports.sanitizeRescueFieldName = sanitizeRescueFieldName;
121
+ exports.sanitizeRescueValues = sanitizeRescueValues;
122
+ //# sourceMappingURL=form-rescue.cjs.map
123
+ //# sourceMappingURL=form-rescue.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/form-rescue.ts"],"names":[],"mappings":"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","file":"form-rescue.cjs","sourcesContent":["/**\n * Form rescue — the shared rules for saving a half-filled public form so the\n * team can follow up with a visitor who left before submitting.\n *\n * Pure and server-safe: the browser hook (`hooks/use-form-rescue`) and the\n * host's draft endpoint import the SAME allowlist, exclusions, caps and\n * completion math, so the server re-applies exactly the filter the client ran\n * and a rename here reaches both sides at once. Also exported through the\n * granular subpath `./utils/form-rescue` for server-only consumers.\n *\n * What a draft may carry:\n * - values of ALLOWLISTED contact fields only (`FORM_RESCUE_VALUE_FIELDS`),\n * trimmed and capped at `FORM_RESCUE_MAX_VALUE_CHARS`;\n * - for every other field, only whether it is filled (never its value);\n * - nothing whose name looks like a credential or payment field\n * (`isExcludedRescueField`), whatever the allowlist says.\n *\n * Field values go to the host only, never to analytics: the PostHog events\n * (`captureFormRescueEvent`) carry ids, field names and percentages.\n */\n\n/** Every public form that can be rescued. The host validates `form_id` against this list. */\nexport const FORM_RESCUE_FORM_IDS = [\n 'contact',\n 'case_study_pitch',\n 'data_room_request',\n 'trust_center_request',\n 'tmcg_join',\n 'meeting_booking',\n 'waitlist',\n] as const;\n\nexport type FormRescueFormId = (typeof FORM_RESCUE_FORM_IDS)[number];\n\nexport function isFormRescueFormId(value: unknown): value is FormRescueFormId {\n return typeof value === 'string' && (FORM_RESCUE_FORM_IDS as readonly string[]).includes(value);\n}\n\n/**\n * Fields whose VALUES a draft keeps: who the visitor is and where they work.\n * Covers both naming schemes in use (the contact form's `name` / `companySize`,\n * the meeting booking's HubSpot `firstName` / `company` / `jobtitle`).\n * Free text (`message`) and phone numbers are reported as filled only.\n */\nexport const FORM_RESCUE_VALUE_FIELDS = [\n 'email',\n 'name',\n 'firstName',\n 'lastName',\n 'company',\n 'jobtitle',\n 'companySize',\n 'linkedin_url',\n] as const;\n\n/** Never send or store these, whatever the allowlist says. */\nconst EXCLUDED_FIELD_PATTERN =\n /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;\n\nexport function isExcludedRescueField(name: string): boolean {\n return EXCLUDED_FIELD_PATTERN.test(name);\n}\n\n/** Longest value a draft keeps per field. */\nexport const FORM_RESCUE_MAX_VALUE_CHARS = 200;\n/** Save this long after the visitor stops typing. */\nexport const FORM_RESCUE_DEBOUNCE_MS = 1500;\n/** Most fields a draft reports (filled names), a bound on the payload. */\nexport const FORM_RESCUE_MAX_FIELDS = 40;\n\n/** Body keys a form's SUBMIT carries so the host can close the matching draft. */\nexport const FORM_RESCUE_ATTEMPT_FIELD = 'form_attempt_id';\nexport const FORM_RESCUE_RESUME_FIELD = 'form_resume_token';\n/** Every rescue key that rides in a submit body. Hosts forwarding a payload upstream strip by THIS array. */\nexport const FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD] as const;\n\n/** Query parameter a resume link carries. */\nexport const FORM_RESCUE_RESUME_PARAM = 'resume';\n\n/** Draft lifecycle. `submitted` is final; `rescued` can still become `submitted`. */\nexport const FORM_DRAFT_STATUSES = ['started', 'rescuable', 'rescued', 'submitted'] as const;\nexport type FormDraftStatus = (typeof FORM_DRAFT_STATUSES)[number];\n\n/** Why a due draft was settled WITHOUT alerting the team (stored on the draft). */\nexport const FORM_RESCUE_SKIP_REASONS = {\n finishedElsewhere: 'finished_elsewhere',\n alreadyRescued: 'already_rescued',\n internal: 'internal_email',\n} as const;\nexport type FormRescueSkipReason = (typeof FORM_RESCUE_SKIP_REASONS)[keyof typeof FORM_RESCUE_SKIP_REASONS];\n\n/** Same shape the host's `contact_submissions.email` check accepts. */\nconst EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/;\n\nexport function isRescueEmail(value: unknown): value is string {\n return typeof value === 'string' && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());\n}\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport function isFormAttemptId(value: unknown): value is string {\n return typeof value === 'string' && UUID_PATTERN.test(value);\n}\n\nfunction isFilled(value: unknown): boolean {\n if (value == null) return false;\n if (typeof value === 'string') return value.trim().length > 0;\n if (typeof value === 'boolean') return value;\n if (Array.isArray(value)) return value.length > 0;\n if (typeof value === 'object') return Object.values(value as Record<string, unknown>).some(isFilled);\n return true;\n}\n\n/**\n * Keep only allowlisted, non-excluded string values, trimmed and capped.\n * The ONE filter, run by the browser before sending and by the host before storing.\n */\nexport function sanitizeRescueValues(values: Record<string, unknown> | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!values || typeof values !== 'object') return out;\n for (const key of FORM_RESCUE_VALUE_FIELDS) {\n if (isExcludedRescueField(key)) continue;\n const raw = values[key];\n if (typeof raw !== 'string') continue;\n const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);\n if (trimmed) out[key] = trimmed;\n }\n return out;\n}\n\n/** Names of the given fields that hold something, credentials never included. */\nexport function filledRescueFields(values: Record<string, unknown>, fieldNames: readonly string[]): string[] {\n return fieldNames\n .filter(name => !isExcludedRescueField(name) && isFilled(values[name]))\n .slice(0, FORM_RESCUE_MAX_FIELDS);\n}\n\nexport function rescueCompletionPct(filledCount: number, totalCount: number): number {\n if (totalCount <= 0) return 0;\n return Math.max(0, Math.min(100, Math.round((filledCount / totalCount) * 100)));\n}\n\n/** A field NAME the host stores (`last_field`, `fields_filled`); bounded, printable. */\nexport function sanitizeRescueFieldName(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;\n}\n\n/** A form's progress at one moment: allowlisted values, filled field names, position. */\nexport interface FormDraftProgress {\n values: Record<string, string>;\n fields_filled: string[];\n last_field: string | null;\n completion_pct: number;\n}\n\n/** What the browser sends on every save (`POST <formDraftsUrl>`), beside the humanity signals. */\nexport interface FormDraftSaveRequest extends FormDraftProgress {\n /** The attempt's idempotency key: every save of one attempt updates one draft. */\n attempt_id: string;\n form_id: FormRescueFormId;\n source_path: string;\n utm?: Partial<Record<'source' | 'medium' | 'campaign' | 'content' | 'term', string>>;\n}\n\n/** What the host answers a resume link with: the allowlisted values, nothing internal. */\nexport interface FormDraftResumeResponse {\n form_id: FormRescueFormId;\n values: Record<string, string>;\n}\n\n/** The PostHog events this feature emits. Properties are ids, names and numbers, never values. */\nexport const FORM_RESCUE_EVENTS = {\n started: 'form_started',\n progress: 'form_progress',\n leadCaptured: 'form_lead_captured',\n abandoned: 'form_abandoned',\n submitted: 'form_submitted',\n rescued: 'form_rescued',\n resumed: 'form_resumed',\n} as const;\n\ntype AnalyticsValue = string | number | boolean | string[] | null;\n\n/**\n * Send one form event to PostHog when the page loaded it (GTM exposes\n * `window.posthog`); a no-op otherwise. Never throws.\n */\nexport function captureFormRescueEvent(event: string, properties: Record<string, AnalyticsValue>): void {\n try {\n if (typeof window === 'undefined') return;\n const ph = (window as unknown as { posthog?: { capture?: (e: string, p: object) => void } }).posthog;\n if (typeof ph?.capture === 'function') ph.capture(event, properties);\n } catch {\n // Analytics never breaks a form.\n }\n}\n"]}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Form rescue — the shared rules for saving a half-filled public form so the
3
+ * team can follow up with a visitor who left before submitting.
4
+ *
5
+ * Pure and server-safe: the browser hook (`hooks/use-form-rescue`) and the
6
+ * host's draft endpoint import the SAME allowlist, exclusions, caps and
7
+ * completion math, so the server re-applies exactly the filter the client ran
8
+ * and a rename here reaches both sides at once. Also exported through the
9
+ * granular subpath `./utils/form-rescue` for server-only consumers.
10
+ *
11
+ * What a draft may carry:
12
+ * - values of ALLOWLISTED contact fields only (`FORM_RESCUE_VALUE_FIELDS`),
13
+ * trimmed and capped at `FORM_RESCUE_MAX_VALUE_CHARS`;
14
+ * - for every other field, only whether it is filled (never its value);
15
+ * - nothing whose name looks like a credential or payment field
16
+ * (`isExcludedRescueField`), whatever the allowlist says.
17
+ *
18
+ * Field values go to the host only, never to analytics: the PostHog events
19
+ * (`captureFormRescueEvent`) carry ids, field names and percentages.
20
+ */
21
+ /** Every public form that can be rescued. The host validates `form_id` against this list. */
22
+ export declare const FORM_RESCUE_FORM_IDS: readonly ["contact", "case_study_pitch", "data_room_request", "trust_center_request", "tmcg_join", "meeting_booking", "waitlist"];
23
+ export type FormRescueFormId = (typeof FORM_RESCUE_FORM_IDS)[number];
24
+ export declare function isFormRescueFormId(value: unknown): value is FormRescueFormId;
25
+ /**
26
+ * Fields whose VALUES a draft keeps: who the visitor is and where they work.
27
+ * Covers both naming schemes in use (the contact form's `name` / `companySize`,
28
+ * the meeting booking's HubSpot `firstName` / `company` / `jobtitle`).
29
+ * Free text (`message`) and phone numbers are reported as filled only.
30
+ */
31
+ export declare const FORM_RESCUE_VALUE_FIELDS: readonly ["email", "name", "firstName", "lastName", "company", "jobtitle", "companySize", "linkedin_url"];
32
+ export declare function isExcludedRescueField(name: string): boolean;
33
+ /** Longest value a draft keeps per field. */
34
+ export declare const FORM_RESCUE_MAX_VALUE_CHARS = 200;
35
+ /** Save this long after the visitor stops typing. */
36
+ export declare const FORM_RESCUE_DEBOUNCE_MS = 1500;
37
+ /** Most fields a draft reports (filled names), a bound on the payload. */
38
+ export declare const FORM_RESCUE_MAX_FIELDS = 40;
39
+ /** Body keys a form's SUBMIT carries so the host can close the matching draft. */
40
+ export declare const FORM_RESCUE_ATTEMPT_FIELD = "form_attempt_id";
41
+ export declare const FORM_RESCUE_RESUME_FIELD = "form_resume_token";
42
+ /** Every rescue key that rides in a submit body. Hosts forwarding a payload upstream strip by THIS array. */
43
+ export declare const FORM_RESCUE_SUBMIT_KEYS: readonly ["form_attempt_id", "form_resume_token"];
44
+ /** Query parameter a resume link carries. */
45
+ export declare const FORM_RESCUE_RESUME_PARAM = "resume";
46
+ /** Draft lifecycle. `submitted` is final; `rescued` can still become `submitted`. */
47
+ export declare const FORM_DRAFT_STATUSES: readonly ["started", "rescuable", "rescued", "submitted"];
48
+ export type FormDraftStatus = (typeof FORM_DRAFT_STATUSES)[number];
49
+ /** Why a due draft was settled WITHOUT alerting the team (stored on the draft). */
50
+ export declare const FORM_RESCUE_SKIP_REASONS: {
51
+ readonly finishedElsewhere: "finished_elsewhere";
52
+ readonly alreadyRescued: "already_rescued";
53
+ readonly internal: "internal_email";
54
+ };
55
+ export type FormRescueSkipReason = (typeof FORM_RESCUE_SKIP_REASONS)[keyof typeof FORM_RESCUE_SKIP_REASONS];
56
+ export declare function isRescueEmail(value: unknown): value is string;
57
+ export declare function isFormAttemptId(value: unknown): value is string;
58
+ /**
59
+ * Keep only allowlisted, non-excluded string values, trimmed and capped.
60
+ * The ONE filter, run by the browser before sending and by the host before storing.
61
+ */
62
+ export declare function sanitizeRescueValues(values: Record<string, unknown> | null | undefined): Record<string, string>;
63
+ /** Names of the given fields that hold something, credentials never included. */
64
+ export declare function filledRescueFields(values: Record<string, unknown>, fieldNames: readonly string[]): string[];
65
+ export declare function rescueCompletionPct(filledCount: number, totalCount: number): number;
66
+ /** A field NAME the host stores (`last_field`, `fields_filled`); bounded, printable. */
67
+ export declare function sanitizeRescueFieldName(value: unknown): string | null;
68
+ /** A form's progress at one moment: allowlisted values, filled field names, position. */
69
+ export interface FormDraftProgress {
70
+ values: Record<string, string>;
71
+ fields_filled: string[];
72
+ last_field: string | null;
73
+ completion_pct: number;
74
+ }
75
+ /** What the browser sends on every save (`POST <formDraftsUrl>`), beside the humanity signals. */
76
+ export interface FormDraftSaveRequest extends FormDraftProgress {
77
+ /** The attempt's idempotency key: every save of one attempt updates one draft. */
78
+ attempt_id: string;
79
+ form_id: FormRescueFormId;
80
+ source_path: string;
81
+ utm?: Partial<Record<'source' | 'medium' | 'campaign' | 'content' | 'term', string>>;
82
+ }
83
+ /** What the host answers a resume link with: the allowlisted values, nothing internal. */
84
+ export interface FormDraftResumeResponse {
85
+ form_id: FormRescueFormId;
86
+ values: Record<string, string>;
87
+ }
88
+ /** The PostHog events this feature emits. Properties are ids, names and numbers, never values. */
89
+ export declare const FORM_RESCUE_EVENTS: {
90
+ readonly started: "form_started";
91
+ readonly progress: "form_progress";
92
+ readonly leadCaptured: "form_lead_captured";
93
+ readonly abandoned: "form_abandoned";
94
+ readonly submitted: "form_submitted";
95
+ readonly rescued: "form_rescued";
96
+ readonly resumed: "form_resumed";
97
+ };
98
+ type AnalyticsValue = string | number | boolean | string[] | null;
99
+ /**
100
+ * Send one form event to PostHog when the page loaded it (GTM exposes
101
+ * `window.posthog`); a no-op otherwise. Never throws.
102
+ */
103
+ export declare function captureFormRescueEvent(event: string, properties: Record<string, AnalyticsValue>): void;
104
+ export {};
105
+ //# sourceMappingURL=form-rescue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-rescue.d.ts","sourceRoot":"","sources":["../../src/utils/form-rescue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ // src/utils/form-rescue.ts
2
+ var FORM_RESCUE_FORM_IDS = [
3
+ "contact",
4
+ "case_study_pitch",
5
+ "data_room_request",
6
+ "trust_center_request",
7
+ "tmcg_join",
8
+ "meeting_booking",
9
+ "waitlist"
10
+ ];
11
+ function isFormRescueFormId(value) {
12
+ return typeof value === "string" && FORM_RESCUE_FORM_IDS.includes(value);
13
+ }
14
+ var FORM_RESCUE_VALUE_FIELDS = [
15
+ "email",
16
+ "name",
17
+ "firstName",
18
+ "lastName",
19
+ "company",
20
+ "jobtitle",
21
+ "companySize",
22
+ "linkedin_url"
23
+ ];
24
+ var EXCLUDED_FIELD_PATTERN = /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;
25
+ function isExcludedRescueField(name) {
26
+ return EXCLUDED_FIELD_PATTERN.test(name);
27
+ }
28
+ var FORM_RESCUE_MAX_VALUE_CHARS = 200;
29
+ var FORM_RESCUE_DEBOUNCE_MS = 1500;
30
+ var FORM_RESCUE_MAX_FIELDS = 40;
31
+ var FORM_RESCUE_ATTEMPT_FIELD = "form_attempt_id";
32
+ var FORM_RESCUE_RESUME_FIELD = "form_resume_token";
33
+ var FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD];
34
+ var FORM_RESCUE_RESUME_PARAM = "resume";
35
+ var FORM_DRAFT_STATUSES = ["started", "rescuable", "rescued", "submitted"];
36
+ var FORM_RESCUE_SKIP_REASONS = {
37
+ finishedElsewhere: "finished_elsewhere",
38
+ alreadyRescued: "already_rescued",
39
+ internal: "internal_email"
40
+ };
41
+ var EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
42
+ function isRescueEmail(value) {
43
+ return typeof value === "string" && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());
44
+ }
45
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
46
+ function isFormAttemptId(value) {
47
+ return typeof value === "string" && UUID_PATTERN.test(value);
48
+ }
49
+ function isFilled(value) {
50
+ if (value == null) return false;
51
+ if (typeof value === "string") return value.trim().length > 0;
52
+ if (typeof value === "boolean") return value;
53
+ if (Array.isArray(value)) return value.length > 0;
54
+ if (typeof value === "object") return Object.values(value).some(isFilled);
55
+ return true;
56
+ }
57
+ function sanitizeRescueValues(values) {
58
+ const out = {};
59
+ if (!values || typeof values !== "object") return out;
60
+ for (const key of FORM_RESCUE_VALUE_FIELDS) {
61
+ if (isExcludedRescueField(key)) continue;
62
+ const raw = values[key];
63
+ if (typeof raw !== "string") continue;
64
+ const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);
65
+ if (trimmed) out[key] = trimmed;
66
+ }
67
+ return out;
68
+ }
69
+ function filledRescueFields(values, fieldNames) {
70
+ return fieldNames.filter((name) => !isExcludedRescueField(name) && isFilled(values[name])).slice(0, FORM_RESCUE_MAX_FIELDS);
71
+ }
72
+ function rescueCompletionPct(filledCount, totalCount) {
73
+ if (totalCount <= 0) return 0;
74
+ return Math.max(0, Math.min(100, Math.round(filledCount / totalCount * 100)));
75
+ }
76
+ function sanitizeRescueFieldName(value) {
77
+ if (typeof value !== "string") return null;
78
+ const trimmed = value.trim();
79
+ return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;
80
+ }
81
+ var FORM_RESCUE_EVENTS = {
82
+ started: "form_started",
83
+ progress: "form_progress",
84
+ leadCaptured: "form_lead_captured",
85
+ abandoned: "form_abandoned",
86
+ submitted: "form_submitted",
87
+ rescued: "form_rescued",
88
+ resumed: "form_resumed"
89
+ };
90
+ function captureFormRescueEvent(event, properties) {
91
+ try {
92
+ if (typeof window === "undefined") return;
93
+ const ph = window.posthog;
94
+ if (typeof ph?.capture === "function") ph.capture(event, properties);
95
+ } catch {
96
+ }
97
+ }
98
+
99
+ export { FORM_DRAFT_STATUSES, FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_DEBOUNCE_MS, FORM_RESCUE_EVENTS, FORM_RESCUE_FORM_IDS, FORM_RESCUE_MAX_FIELDS, FORM_RESCUE_MAX_VALUE_CHARS, FORM_RESCUE_RESUME_FIELD, FORM_RESCUE_RESUME_PARAM, FORM_RESCUE_SKIP_REASONS, FORM_RESCUE_SUBMIT_KEYS, FORM_RESCUE_VALUE_FIELDS, captureFormRescueEvent, filledRescueFields, isExcludedRescueField, isFormAttemptId, isFormRescueFormId, isRescueEmail, rescueCompletionPct, sanitizeRescueFieldName, sanitizeRescueValues };
100
+ //# sourceMappingURL=form-rescue.js.map
101
+ //# sourceMappingURL=form-rescue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/form-rescue.ts"],"names":[],"mappings":";AAsBO,IAAM,oBAAA,GAAuB;AAAA,EAClC,SAAA;AAAA,EACA,kBAAA;AAAA,EACA,mBAAA;AAAA,EACA,sBAAA;AAAA,EACA,WAAA;AAAA,EACA,iBAAA;AAAA,EACA;AACF;AAIO,SAAS,mBAAmB,KAAA,EAA2C;AAC5E,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAa,oBAAA,CAA2C,SAAS,KAAK,CAAA;AAChG;AAQO,IAAM,wBAAA,GAA2B;AAAA,EACtC,OAAA;AAAA,EACA,MAAA;AAAA,EACA,WAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF;AAGA,IAAM,sBAAA,GACJ,6FAAA;AAEK,SAAS,sBAAsB,IAAA,EAAuB;AAC3D,EAAA,OAAO,sBAAA,CAAuB,KAAK,IAAI,CAAA;AACzC;AAGO,IAAM,2BAAA,GAA8B;AAEpC,IAAM,uBAAA,GAA0B;AAEhC,IAAM,sBAAA,GAAyB;AAG/B,IAAM,yBAAA,GAA4B;AAClC,IAAM,wBAAA,GAA2B;AAEjC,IAAM,uBAAA,GAA0B,CAAC,yBAAA,EAA2B,wBAAwB;AAGpF,IAAM,wBAAA,GAA2B;AAGjC,IAAM,mBAAA,GAAsB,CAAC,SAAA,EAAW,WAAA,EAAa,WAAW,WAAW;AAI3E,IAAM,wBAAA,GAA2B;AAAA,EACtC,iBAAA,EAAmB,oBAAA;AAAA,EACnB,cAAA,EAAgB,iBAAA;AAAA,EAChB,QAAA,EAAU;AACZ;AAIA,IAAM,aAAA,GAAgB,kDAAA;AAEf,SAAS,cAAc,KAAA,EAAiC;AAC7D,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,CAAM,MAAA,IAAU,+BAA+B,aAAA,CAAc,IAAA,CAAK,KAAA,CAAM,IAAA,EAAM,CAAA;AACpH;AAEA,IAAM,YAAA,GAAe,iEAAA;AAEd,SAAS,gBAAgB,KAAA,EAAiC;AAC/D,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,YAAA,CAAa,KAAK,KAAK,CAAA;AAC7D;AAEA,SAAS,SAAS,KAAA,EAAyB;AACzC,EAAA,IAAI,KAAA,IAAS,MAAM,OAAO,KAAA;AAC1B,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,SAAiB,KAAA,CAAM,IAAA,GAAO,MAAA,GAAS,CAAA;AAC5D,EAAA,IAAI,OAAO,KAAA,KAAU,SAAA,EAAW,OAAO,KAAA;AACvC,EAAA,IAAI,MAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,OAAO,MAAM,MAAA,GAAS,CAAA;AAChD,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU,OAAO,OAAO,MAAA,CAAO,KAAgC,CAAA,CAAE,IAAA,CAAK,QAAQ,CAAA;AACnG,EAAA,OAAO,IAAA;AACT;AAMO,SAAS,qBAAqB,MAAA,EAA4E;AAC/G,EAAA,MAAM,MAA8B,EAAC;AACrC,EAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,UAAU,OAAO,GAAA;AAClD,EAAA,KAAA,MAAW,OAAO,wBAAA,EAA0B;AAC1C,IAAA,IAAI,qBAAA,CAAsB,GAAG,CAAA,EAAG;AAChC,IAAA,MAAM,GAAA,GAAM,OAAO,GAAG,CAAA;AACtB,IAAA,IAAI,OAAO,QAAQ,QAAA,EAAU;AAC7B,IAAA,MAAM,UAAU,GAAA,CAAI,IAAA,EAAK,CAAE,KAAA,CAAM,GAAG,2BAA2B,CAAA;AAC/D,IAAA,IAAI,OAAA,EAAS,GAAA,CAAI,GAAG,CAAA,GAAI,OAAA;AAAA,EAC1B;AACA,EAAA,OAAO,GAAA;AACT;AAGO,SAAS,kBAAA,CAAmB,QAAiC,UAAA,EAAyC;AAC3G,EAAA,OAAO,UAAA,CACJ,MAAA,CAAO,CAAA,IAAA,KAAQ,CAAC,sBAAsB,IAAI,CAAA,IAAK,QAAA,CAAS,MAAA,CAAO,IAAI,CAAC,CAAC,CAAA,CACrE,KAAA,CAAM,GAAG,sBAAsB,CAAA;AACpC;AAEO,SAAS,mBAAA,CAAoB,aAAqB,UAAA,EAA4B;AACnF,EAAA,IAAI,UAAA,IAAc,GAAG,OAAO,CAAA;AAC5B,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,IAAA,CAAK,KAAA,CAAO,WAAA,GAAc,UAAA,GAAc,GAAG,CAAC,CAAC,CAAA;AAChF;AAGO,SAAS,wBAAwB,KAAA,EAA+B;AACrE,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,IAAA;AACtC,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,OAAO,wBAAA,CAAyB,KAAK,OAAO,CAAA,IAAK,CAAC,qBAAA,CAAsB,OAAO,IAAI,OAAA,GAAU,IAAA;AAC/F;AA0BO,IAAM,kBAAA,GAAqB;AAAA,EAChC,OAAA,EAAS,cAAA;AAAA,EACT,QAAA,EAAU,eAAA;AAAA,EACV,YAAA,EAAc,oBAAA;AAAA,EACd,SAAA,EAAW,gBAAA;AAAA,EACX,SAAA,EAAW,gBAAA;AAAA,EACX,OAAA,EAAS,cAAA;AAAA,EACT,OAAA,EAAS;AACX;AAQO,SAAS,sBAAA,CAAuB,OAAe,UAAA,EAAkD;AACtG,EAAA,IAAI;AACF,IAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,IAAA,MAAM,KAAM,MAAA,CAAiF,OAAA;AAC7F,IAAA,IAAI,OAAO,EAAA,EAAI,OAAA,KAAY,YAAY,EAAA,CAAG,OAAA,CAAQ,OAAO,UAAU,CAAA;AAAA,EACrE,CAAA,CAAA,MAAQ;AAAA,EAER;AACF","file":"form-rescue.js","sourcesContent":["/**\n * Form rescue — the shared rules for saving a half-filled public form so the\n * team can follow up with a visitor who left before submitting.\n *\n * Pure and server-safe: the browser hook (`hooks/use-form-rescue`) and the\n * host's draft endpoint import the SAME allowlist, exclusions, caps and\n * completion math, so the server re-applies exactly the filter the client ran\n * and a rename here reaches both sides at once. Also exported through the\n * granular subpath `./utils/form-rescue` for server-only consumers.\n *\n * What a draft may carry:\n * - values of ALLOWLISTED contact fields only (`FORM_RESCUE_VALUE_FIELDS`),\n * trimmed and capped at `FORM_RESCUE_MAX_VALUE_CHARS`;\n * - for every other field, only whether it is filled (never its value);\n * - nothing whose name looks like a credential or payment field\n * (`isExcludedRescueField`), whatever the allowlist says.\n *\n * Field values go to the host only, never to analytics: the PostHog events\n * (`captureFormRescueEvent`) carry ids, field names and percentages.\n */\n\n/** Every public form that can be rescued. The host validates `form_id` against this list. */\nexport const FORM_RESCUE_FORM_IDS = [\n 'contact',\n 'case_study_pitch',\n 'data_room_request',\n 'trust_center_request',\n 'tmcg_join',\n 'meeting_booking',\n 'waitlist',\n] as const;\n\nexport type FormRescueFormId = (typeof FORM_RESCUE_FORM_IDS)[number];\n\nexport function isFormRescueFormId(value: unknown): value is FormRescueFormId {\n return typeof value === 'string' && (FORM_RESCUE_FORM_IDS as readonly string[]).includes(value);\n}\n\n/**\n * Fields whose VALUES a draft keeps: who the visitor is and where they work.\n * Covers both naming schemes in use (the contact form's `name` / `companySize`,\n * the meeting booking's HubSpot `firstName` / `company` / `jobtitle`).\n * Free text (`message`) and phone numbers are reported as filled only.\n */\nexport const FORM_RESCUE_VALUE_FIELDS = [\n 'email',\n 'name',\n 'firstName',\n 'lastName',\n 'company',\n 'jobtitle',\n 'companySize',\n 'linkedin_url',\n] as const;\n\n/** Never send or store these, whatever the allowlist says. */\nconst EXCLUDED_FIELD_PATTERN =\n /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;\n\nexport function isExcludedRescueField(name: string): boolean {\n return EXCLUDED_FIELD_PATTERN.test(name);\n}\n\n/** Longest value a draft keeps per field. */\nexport const FORM_RESCUE_MAX_VALUE_CHARS = 200;\n/** Save this long after the visitor stops typing. */\nexport const FORM_RESCUE_DEBOUNCE_MS = 1500;\n/** Most fields a draft reports (filled names), a bound on the payload. */\nexport const FORM_RESCUE_MAX_FIELDS = 40;\n\n/** Body keys a form's SUBMIT carries so the host can close the matching draft. */\nexport const FORM_RESCUE_ATTEMPT_FIELD = 'form_attempt_id';\nexport const FORM_RESCUE_RESUME_FIELD = 'form_resume_token';\n/** Every rescue key that rides in a submit body. Hosts forwarding a payload upstream strip by THIS array. */\nexport const FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD] as const;\n\n/** Query parameter a resume link carries. */\nexport const FORM_RESCUE_RESUME_PARAM = 'resume';\n\n/** Draft lifecycle. `submitted` is final; `rescued` can still become `submitted`. */\nexport const FORM_DRAFT_STATUSES = ['started', 'rescuable', 'rescued', 'submitted'] as const;\nexport type FormDraftStatus = (typeof FORM_DRAFT_STATUSES)[number];\n\n/** Why a due draft was settled WITHOUT alerting the team (stored on the draft). */\nexport const FORM_RESCUE_SKIP_REASONS = {\n finishedElsewhere: 'finished_elsewhere',\n alreadyRescued: 'already_rescued',\n internal: 'internal_email',\n} as const;\nexport type FormRescueSkipReason = (typeof FORM_RESCUE_SKIP_REASONS)[keyof typeof FORM_RESCUE_SKIP_REASONS];\n\n/** Same shape the host's `contact_submissions.email` check accepts. */\nconst EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/;\n\nexport function isRescueEmail(value: unknown): value is string {\n return typeof value === 'string' && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());\n}\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport function isFormAttemptId(value: unknown): value is string {\n return typeof value === 'string' && UUID_PATTERN.test(value);\n}\n\nfunction isFilled(value: unknown): boolean {\n if (value == null) return false;\n if (typeof value === 'string') return value.trim().length > 0;\n if (typeof value === 'boolean') return value;\n if (Array.isArray(value)) return value.length > 0;\n if (typeof value === 'object') return Object.values(value as Record<string, unknown>).some(isFilled);\n return true;\n}\n\n/**\n * Keep only allowlisted, non-excluded string values, trimmed and capped.\n * The ONE filter, run by the browser before sending and by the host before storing.\n */\nexport function sanitizeRescueValues(values: Record<string, unknown> | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!values || typeof values !== 'object') return out;\n for (const key of FORM_RESCUE_VALUE_FIELDS) {\n if (isExcludedRescueField(key)) continue;\n const raw = values[key];\n if (typeof raw !== 'string') continue;\n const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);\n if (trimmed) out[key] = trimmed;\n }\n return out;\n}\n\n/** Names of the given fields that hold something, credentials never included. */\nexport function filledRescueFields(values: Record<string, unknown>, fieldNames: readonly string[]): string[] {\n return fieldNames\n .filter(name => !isExcludedRescueField(name) && isFilled(values[name]))\n .slice(0, FORM_RESCUE_MAX_FIELDS);\n}\n\nexport function rescueCompletionPct(filledCount: number, totalCount: number): number {\n if (totalCount <= 0) return 0;\n return Math.max(0, Math.min(100, Math.round((filledCount / totalCount) * 100)));\n}\n\n/** A field NAME the host stores (`last_field`, `fields_filled`); bounded, printable. */\nexport function sanitizeRescueFieldName(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;\n}\n\n/** A form's progress at one moment: allowlisted values, filled field names, position. */\nexport interface FormDraftProgress {\n values: Record<string, string>;\n fields_filled: string[];\n last_field: string | null;\n completion_pct: number;\n}\n\n/** What the browser sends on every save (`POST <formDraftsUrl>`), beside the humanity signals. */\nexport interface FormDraftSaveRequest extends FormDraftProgress {\n /** The attempt's idempotency key: every save of one attempt updates one draft. */\n attempt_id: string;\n form_id: FormRescueFormId;\n source_path: string;\n utm?: Partial<Record<'source' | 'medium' | 'campaign' | 'content' | 'term', string>>;\n}\n\n/** What the host answers a resume link with: the allowlisted values, nothing internal. */\nexport interface FormDraftResumeResponse {\n form_id: FormRescueFormId;\n values: Record<string, string>;\n}\n\n/** The PostHog events this feature emits. Properties are ids, names and numbers, never values. */\nexport const FORM_RESCUE_EVENTS = {\n started: 'form_started',\n progress: 'form_progress',\n leadCaptured: 'form_lead_captured',\n abandoned: 'form_abandoned',\n submitted: 'form_submitted',\n rescued: 'form_rescued',\n resumed: 'form_resumed',\n} as const;\n\ntype AnalyticsValue = string | number | boolean | string[] | null;\n\n/**\n * Send one form event to PostHog when the page loaded it (GTM exposes\n * `window.posthog`); a no-op otherwise. Never throws.\n */\nexport function captureFormRescueEvent(event: string, properties: Record<string, AnalyticsValue>): void {\n try {\n if (typeof window === 'undefined') return;\n const ph = (window as unknown as { posthog?: { capture?: (e: string, p: object) => void } }).posthog;\n if (typeof ph?.capture === 'function') ph.capture(event, properties);\n } catch {\n // Analytics never breaks a form.\n }\n}\n"]}
@@ -5300,6 +5300,104 @@ function evaluateHumanitySignals(body, opts) {
5300
5300
  }
5301
5301
  var splitCsvEnv = (s) => s?.split(",").map((t) => t.trim()).filter(Boolean) ?? [];
5302
5302
 
5303
+ // src/utils/form-rescue.ts
5304
+ var FORM_RESCUE_FORM_IDS = [
5305
+ "contact",
5306
+ "case_study_pitch",
5307
+ "data_room_request",
5308
+ "trust_center_request",
5309
+ "tmcg_join",
5310
+ "meeting_booking",
5311
+ "waitlist"
5312
+ ];
5313
+ function isFormRescueFormId(value) {
5314
+ return typeof value === "string" && FORM_RESCUE_FORM_IDS.includes(value);
5315
+ }
5316
+ var FORM_RESCUE_VALUE_FIELDS = [
5317
+ "email",
5318
+ "name",
5319
+ "firstName",
5320
+ "lastName",
5321
+ "company",
5322
+ "jobtitle",
5323
+ "companySize",
5324
+ "linkedin_url"
5325
+ ];
5326
+ var EXCLUDED_FIELD_PATTERN = /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;
5327
+ function isExcludedRescueField(name) {
5328
+ return EXCLUDED_FIELD_PATTERN.test(name);
5329
+ }
5330
+ var FORM_RESCUE_MAX_VALUE_CHARS = 200;
5331
+ var FORM_RESCUE_DEBOUNCE_MS = 1500;
5332
+ var FORM_RESCUE_MAX_FIELDS = 40;
5333
+ var FORM_RESCUE_ATTEMPT_FIELD = "form_attempt_id";
5334
+ var FORM_RESCUE_RESUME_FIELD = "form_resume_token";
5335
+ var FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD];
5336
+ var FORM_RESCUE_RESUME_PARAM = "resume";
5337
+ var FORM_DRAFT_STATUSES = ["started", "rescuable", "rescued", "submitted"];
5338
+ var FORM_RESCUE_SKIP_REASONS = {
5339
+ finishedElsewhere: "finished_elsewhere",
5340
+ alreadyRescued: "already_rescued",
5341
+ internal: "internal_email"
5342
+ };
5343
+ var EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
5344
+ function isRescueEmail(value) {
5345
+ return typeof value === "string" && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());
5346
+ }
5347
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5348
+ function isFormAttemptId(value) {
5349
+ return typeof value === "string" && UUID_PATTERN.test(value);
5350
+ }
5351
+ function isFilled(value) {
5352
+ if (value == null) return false;
5353
+ if (typeof value === "string") return value.trim().length > 0;
5354
+ if (typeof value === "boolean") return value;
5355
+ if (Array.isArray(value)) return value.length > 0;
5356
+ if (typeof value === "object") return Object.values(value).some(isFilled);
5357
+ return true;
5358
+ }
5359
+ function sanitizeRescueValues(values) {
5360
+ const out = {};
5361
+ if (!values || typeof values !== "object") return out;
5362
+ for (const key of FORM_RESCUE_VALUE_FIELDS) {
5363
+ if (isExcludedRescueField(key)) continue;
5364
+ const raw = values[key];
5365
+ if (typeof raw !== "string") continue;
5366
+ const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);
5367
+ if (trimmed) out[key] = trimmed;
5368
+ }
5369
+ return out;
5370
+ }
5371
+ function filledRescueFields(values, fieldNames) {
5372
+ return fieldNames.filter((name) => !isExcludedRescueField(name) && isFilled(values[name])).slice(0, FORM_RESCUE_MAX_FIELDS);
5373
+ }
5374
+ function rescueCompletionPct(filledCount, totalCount) {
5375
+ if (totalCount <= 0) return 0;
5376
+ return Math.max(0, Math.min(100, Math.round(filledCount / totalCount * 100)));
5377
+ }
5378
+ function sanitizeRescueFieldName(value) {
5379
+ if (typeof value !== "string") return null;
5380
+ const trimmed = value.trim();
5381
+ return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;
5382
+ }
5383
+ var FORM_RESCUE_EVENTS = {
5384
+ started: "form_started",
5385
+ progress: "form_progress",
5386
+ leadCaptured: "form_lead_captured",
5387
+ abandoned: "form_abandoned",
5388
+ submitted: "form_submitted",
5389
+ rescued: "form_rescued",
5390
+ resumed: "form_resumed"
5391
+ };
5392
+ function captureFormRescueEvent(event, properties) {
5393
+ try {
5394
+ if (typeof window === "undefined") return;
5395
+ const ph = window.posthog;
5396
+ if (typeof ph?.capture === "function") ph.capture(event, properties);
5397
+ } catch {
5398
+ }
5399
+ }
5400
+
5303
5401
  // src/utils/doc-tree-nav.ts
5304
5402
  var DEFAULT_FOLDER_INDEX_FILE = "README.md";
5305
5403
  var DEFAULT_FOLDER_INDEX = DEFAULT_FOLDER_INDEX_FILE;
@@ -6032,6 +6130,18 @@ exports.DEV_SECTION_PARAM_KEYS = DEV_SECTION_PARAM_KEYS;
6032
6130
  exports.DOC_TABLE_TYPES = DOC_TABLE_TYPES;
6033
6131
  exports.ELAPSED_MS_FIELD = ELAPSED_MS_FIELD;
6034
6132
  exports.FIRST_TOUCH_ATTRIBUTION_KEY = FIRST_TOUCH_ATTRIBUTION_KEY;
6133
+ exports.FORM_DRAFT_STATUSES = FORM_DRAFT_STATUSES;
6134
+ exports.FORM_RESCUE_ATTEMPT_FIELD = FORM_RESCUE_ATTEMPT_FIELD;
6135
+ exports.FORM_RESCUE_DEBOUNCE_MS = FORM_RESCUE_DEBOUNCE_MS;
6136
+ exports.FORM_RESCUE_EVENTS = FORM_RESCUE_EVENTS;
6137
+ exports.FORM_RESCUE_FORM_IDS = FORM_RESCUE_FORM_IDS;
6138
+ exports.FORM_RESCUE_MAX_FIELDS = FORM_RESCUE_MAX_FIELDS;
6139
+ exports.FORM_RESCUE_MAX_VALUE_CHARS = FORM_RESCUE_MAX_VALUE_CHARS;
6140
+ exports.FORM_RESCUE_RESUME_FIELD = FORM_RESCUE_RESUME_FIELD;
6141
+ exports.FORM_RESCUE_RESUME_PARAM = FORM_RESCUE_RESUME_PARAM;
6142
+ exports.FORM_RESCUE_SKIP_REASONS = FORM_RESCUE_SKIP_REASONS;
6143
+ exports.FORM_RESCUE_SUBMIT_KEYS = FORM_RESCUE_SUBMIT_KEYS;
6144
+ exports.FORM_RESCUE_VALUE_FIELDS = FORM_RESCUE_VALUE_FIELDS;
6035
6145
  exports.FullscreenSwitchController = FullscreenSwitchController;
6036
6146
  exports.GENERIC_EMAIL_DOMAINS = GENERIC_EMAIL_DOMAINS;
6037
6147
  exports.HEADING_EMOJI_RE = HEADING_EMOJI_RE;
@@ -6091,6 +6201,7 @@ exports.buildSuggestionUrl = buildSuggestionUrl;
6091
6201
  exports.buildTicketOpenHref = buildTicketOpenHref;
6092
6202
  exports.canonicalContentRefType = canonicalContentRefType;
6093
6203
  exports.captureFirstTouchAttribution = captureFirstTouchAttribution;
6204
+ exports.captureFormRescueEvent = captureFormRescueEvent;
6094
6205
  exports.clamp = clamp;
6095
6206
  exports.clampPageToTotal = clampPageToTotal;
6096
6207
  exports.classifySocialHost = classifySocialHost;
@@ -6138,6 +6249,7 @@ exports.extractSections = extractSections;
6138
6249
  exports.faqItemAnchor = faqItemAnchor;
6139
6250
  exports.faqSectionSlug = faqSectionSlug;
6140
6251
  exports.fetchPriorityProp = fetchPriorityProp;
6252
+ exports.filledRescueFields = filledRescueFields;
6141
6253
  exports.findAnchorElementByNormalizedId = findAnchorElementByNormalizedId;
6142
6254
  exports.findDocNodeByPath = findDocNodeByPath;
6143
6255
  exports.findHoneypotCopySource = findHoneypotCopySource;
@@ -6268,7 +6380,10 @@ exports.isBadgePaletteKey = isBadgePaletteKey;
6268
6380
  exports.isBlankLine = isBlankLine;
6269
6381
  exports.isCrossOriginUrl = isCrossOriginUrl;
6270
6382
  exports.isDismissedCookieValue = isDismissedCookieValue;
6383
+ exports.isExcludedRescueField = isExcludedRescueField;
6271
6384
  exports.isFigmaSlidesUrl = isFigmaSlidesUrl;
6385
+ exports.isFormAttemptId = isFormAttemptId;
6386
+ exports.isFormRescueFormId = isFormRescueFormId;
6272
6387
  exports.isGenericDomain = isGenericDomain;
6273
6388
  exports.isGenericWebsiteDomain = isGenericWebsiteDomain;
6274
6389
  exports.isHumanitySignalKey = isHumanitySignalKey;
@@ -6278,6 +6393,7 @@ exports.isLocalUrl = isLocalUrl;
6278
6393
  exports.isOSPlatform = isOSPlatform;
6279
6394
  exports.isOrderedRange = isOrderedRange;
6280
6395
  exports.isPublishableMediaType = isPublishableMediaType;
6396
+ exports.isRescueEmail = isRescueEmail;
6281
6397
  exports.isToday = isToday;
6282
6398
  exports.isValidEmailDomain = isValidEmailDomain;
6283
6399
  exports.isValidSchedulingSlug = isValidSchedulingSlug;
@@ -6337,11 +6453,14 @@ exports.readDismissCookie = readDismissCookie;
6337
6453
  exports.readLeadingDecisionFrame = readLeadingDecisionFrame;
6338
6454
  exports.recordCountLabel = recordCountLabel;
6339
6455
  exports.releaseTypeToBadgeColor = releaseTypeToBadgeColor;
6456
+ exports.rescueCompletionPct = rescueCompletionPct;
6340
6457
  exports.resolveContentFetchPath = resolveContentFetchPath;
6341
6458
  exports.resolveRedirectTarget = resolveRedirectTarget;
6342
6459
  exports.resolveReleaseCover = resolveReleaseCover;
6343
6460
  exports.rgbToHex = rgbToHex;
6344
6461
  exports.sanitizeLandingUrl = sanitizeLandingUrl;
6462
+ exports.sanitizeRescueFieldName = sanitizeRescueFieldName;
6463
+ exports.sanitizeRescueValues = sanitizeRescueValues;
6345
6464
  exports.sanitizeTitleForChat = sanitizeTitleForChat;
6346
6465
  exports.scanHeadings = scanHeadings;
6347
6466
  exports.schedulingAudienceKey = schedulingAudienceKey;