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

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 (178) 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-4PVY4JJN.js → chunk-2TQ64TG3.js} +3 -3
  10. package/dist/{chunk-FAJUQKWQ.cjs → chunk-3EIB5PNM.cjs} +76 -75
  11. package/dist/chunk-3EIB5PNM.cjs.map +1 -0
  12. package/dist/{chunk-FUXUJSPP.cjs → chunk-3UDBK2YG.cjs} +1 -1
  13. package/dist/chunk-3UDBK2YG.cjs.map +1 -0
  14. package/dist/{chunk-G64BNLDO.js → chunk-3X6WFYSV.js} +2 -2
  15. package/dist/{chunk-DUZEFACP.js → chunk-4UAIOXYC.js} +3 -3
  16. package/dist/{chunk-SNNE34OP.js → chunk-5KD5SSP6.js} +5 -5
  17. package/dist/{chunk-RKUCK6MN.js → chunk-6NGIPAQ6.js} +2 -2
  18. package/dist/{chunk-OG2BNJ2Q.cjs → chunk-6PPEG2N4.cjs} +389 -375
  19. package/dist/chunk-6PPEG2N4.cjs.map +1 -0
  20. package/dist/{chunk-NIYIK32F.js → chunk-6ZX6TFSS.js} +18 -4
  21. package/dist/{chunk-NIYIK32F.js.map → chunk-6ZX6TFSS.js.map} +1 -1
  22. package/dist/{chunk-6C6UUOH4.js → chunk-A77HP3N6.js} +8 -7
  23. package/dist/{chunk-6C6UUOH4.js.map → chunk-A77HP3N6.js.map} +1 -1
  24. package/dist/{chunk-YNGAJGYB.cjs → chunk-AXMNWMAU.cjs} +340 -6
  25. package/dist/chunk-AXMNWMAU.cjs.map +1 -0
  26. package/dist/{chunk-PLG6LP7E.cjs → chunk-BAEHCDPI.cjs} +49 -49
  27. package/dist/{chunk-PLG6LP7E.cjs.map → chunk-BAEHCDPI.cjs.map} +1 -1
  28. package/dist/{chunk-R22IA7MP.js → chunk-BBDEIE56.js} +4 -4
  29. package/dist/{chunk-PQ7YC5Y6.js → chunk-D5HO3RNF.js} +3 -3
  30. package/dist/{chunk-II66XWU5.cjs → chunk-D6IOMPAH.cjs} +3 -3
  31. package/dist/{chunk-II66XWU5.cjs.map → chunk-D6IOMPAH.cjs.map} +1 -1
  32. package/dist/{chunk-DAGKBFZN.cjs → chunk-EQIBWG5U.cjs} +33 -33
  33. package/dist/{chunk-DAGKBFZN.cjs.map → chunk-EQIBWG5U.cjs.map} +1 -1
  34. package/dist/{chunk-3PVOF3IB.js → chunk-EVB3EO5S.js} +337 -3
  35. package/dist/{chunk-3PVOF3IB.js.map → chunk-EVB3EO5S.js.map} +1 -1
  36. package/dist/{chunk-SJZPKIW6.cjs → chunk-G64BDYU5.cjs} +12 -12
  37. package/dist/{chunk-SJZPKIW6.cjs.map → chunk-G64BDYU5.cjs.map} +1 -1
  38. package/dist/{chunk-BP542DGQ.cjs → chunk-GLPGYU6H.cjs} +55 -28
  39. package/dist/chunk-GLPGYU6H.cjs.map +1 -0
  40. package/dist/{chunk-TLLTL6LB.js → chunk-JD4U7QMG.js} +1 -1
  41. package/dist/chunk-JD4U7QMG.js.map +1 -0
  42. package/dist/{chunk-ZRIVCLPR.js → chunk-JD5TKRCU.js} +1 -1
  43. package/dist/chunk-JD5TKRCU.js.map +1 -0
  44. package/dist/{chunk-YJWR7D6O.cjs → chunk-JKIM7U7N.cjs} +3 -3
  45. package/dist/{chunk-YJWR7D6O.cjs.map → chunk-JKIM7U7N.cjs.map} +1 -1
  46. package/dist/{chunk-K3O4D7LW.js → chunk-KUASHD7Z.js} +4 -4
  47. package/dist/{chunk-GF5TSQ6C.cjs → chunk-KXL534EV.cjs} +10 -10
  48. package/dist/{chunk-GF5TSQ6C.cjs.map → chunk-KXL534EV.cjs.map} +1 -1
  49. package/dist/{chunk-FHTNCWIP.js → chunk-KYVFU7VQ.js} +5 -5
  50. package/dist/{chunk-OYMOG544.js → chunk-KZAT6VWE.js} +5 -5
  51. package/dist/{chunk-D55IENZJ.cjs → chunk-LCMT2GZK.cjs} +1 -1
  52. package/dist/chunk-LCMT2GZK.cjs.map +1 -0
  53. package/dist/{chunk-IJIIL6WU.js → chunk-MVU7HF5A.js} +6 -6
  54. package/dist/{chunk-XGXHHSDU.cjs → chunk-NYDFVUMH.cjs} +22 -22
  55. package/dist/{chunk-XGXHHSDU.cjs.map → chunk-NYDFVUMH.cjs.map} +1 -1
  56. package/dist/{chunk-STLBB6PF.js → chunk-O75Y5O3U.js} +35 -8
  57. package/dist/chunk-O75Y5O3U.js.map +1 -0
  58. package/dist/{chunk-DAWYS25M.cjs → chunk-PDNGWDJF.cjs} +17 -17
  59. package/dist/{chunk-DAWYS25M.cjs.map → chunk-PDNGWDJF.cjs.map} +1 -1
  60. package/dist/{chunk-T3ACDOVD.cjs → chunk-VRZZCL4R.cjs} +7 -7
  61. package/dist/{chunk-T3ACDOVD.cjs.map → chunk-VRZZCL4R.cjs.map} +1 -1
  62. package/dist/{chunk-XH4MRTZM.cjs → chunk-WQFT7GOC.cjs} +5 -5
  63. package/dist/{chunk-XH4MRTZM.cjs.map → chunk-WQFT7GOC.cjs.map} +1 -1
  64. package/dist/{chunk-YIDNSRPL.cjs → chunk-XGBFQ3YV.cjs} +31 -31
  65. package/dist/{chunk-YIDNSRPL.cjs.map → chunk-XGBFQ3YV.cjs.map} +1 -1
  66. package/dist/{chunk-UH3BL2OT.js → chunk-Y7TZHPS2.js} +2 -2
  67. package/dist/{chunk-EIAVE6SE.cjs → chunk-YKKW67A3.cjs} +10 -10
  68. package/dist/{chunk-EIAVE6SE.cjs.map → chunk-YKKW67A3.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 +3 -1
  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 +73 -39
  100. package/dist/components/meeting-scheduler/index.cjs.map +1 -1
  101. package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
  102. package/dist/components/meeting-scheduler/index.js +44 -10
  103. package/dist/components/meeting-scheduler/index.js.map +1 -1
  104. package/dist/components/navigation/index.cjs +8 -8
  105. package/dist/components/navigation/index.js +7 -7
  106. package/dist/components/onboarding-guides/index.cjs +8 -8
  107. package/dist/components/onboarding-guides/index.js +7 -7
  108. package/dist/components/tickets/help-center-create-form.d.ts.map +1 -1
  109. package/dist/components/tickets/index.cjs +9 -9
  110. package/dist/components/tickets/index.js +8 -8
  111. package/dist/components/ui/index.cjs +5 -5
  112. package/dist/components/ui/index.js +4 -4
  113. package/dist/contexts/endpoints-runtime-context.d.ts +5 -0
  114. package/dist/contexts/endpoints-runtime-context.d.ts.map +1 -1
  115. package/dist/contexts/index.cjs +2 -2
  116. package/dist/contexts/index.js +1 -1
  117. package/dist/hooks/index.cjs +5 -3
  118. package/dist/hooks/index.cjs.map +1 -1
  119. package/dist/hooks/index.d.ts +1 -0
  120. package/dist/hooks/index.d.ts.map +1 -1
  121. package/dist/hooks/index.js +4 -2
  122. package/dist/hooks/use-form-rescue.d.ts +37 -0
  123. package/dist/hooks/use-form-rescue.d.ts.map +1 -0
  124. package/dist/index.cjs +51 -9
  125. package/dist/index.cjs.map +1 -1
  126. package/dist/index.js +50 -8
  127. package/dist/index.js.map +1 -1
  128. package/dist/utils/form-rescue.cjs +117 -0
  129. package/dist/utils/form-rescue.cjs.map +1 -0
  130. package/dist/utils/form-rescue.d.ts +98 -0
  131. package/dist/utils/form-rescue.d.ts.map +1 -0
  132. package/dist/utils/form-rescue.js +96 -0
  133. package/dist/utils/form-rescue.js.map +1 -0
  134. package/dist/utils/index.cjs +113 -0
  135. package/dist/utils/index.cjs.map +1 -1
  136. package/dist/utils/index.d.ts +1 -0
  137. package/dist/utils/index.d.ts.map +1 -1
  138. package/dist/utils/index.js +94 -1
  139. package/dist/utils/index.js.map +1 -1
  140. package/package.json +7 -1
  141. package/src/chat-protocol/__tests__/card-marker.test.ts +27 -1
  142. package/src/chat-protocol/card-marker.ts +24 -0
  143. package/src/chat-protocol/index.ts +3 -0
  144. package/src/components/case-studies/share-experience-section.tsx +1 -0
  145. package/src/components/contact/contact-form.tsx +38 -2
  146. package/src/components/features/waitlist-form.tsx +23 -2
  147. package/src/components/help-center-pages/trust-center-sections.tsx +1 -0
  148. package/src/components/meeting-scheduler/booking-form.tsx +14 -0
  149. package/src/components/meeting-scheduler/index.tsx +33 -3
  150. package/src/components/tickets/help-center-create-form.tsx +2 -0
  151. package/src/contexts/endpoints-runtime-context.tsx +5 -0
  152. package/src/hooks/__tests__/use-form-rescue.test.tsx +182 -0
  153. package/src/hooks/index.ts +3 -0
  154. package/src/hooks/use-form-rescue.ts +315 -0
  155. package/src/utils/__tests__/form-rescue.test.ts +106 -0
  156. package/src/utils/form-rescue.ts +190 -0
  157. package/src/utils/index.ts +5 -0
  158. package/dist/chunk-BP542DGQ.cjs.map +0 -1
  159. package/dist/chunk-D55IENZJ.cjs.map +0 -1
  160. package/dist/chunk-FAJUQKWQ.cjs.map +0 -1
  161. package/dist/chunk-FUXUJSPP.cjs.map +0 -1
  162. package/dist/chunk-OG2BNJ2Q.cjs.map +0 -1
  163. package/dist/chunk-STLBB6PF.js.map +0 -1
  164. package/dist/chunk-TLLTL6LB.js.map +0 -1
  165. package/dist/chunk-YNGAJGYB.cjs.map +0 -1
  166. package/dist/chunk-ZRIVCLPR.js.map +0 -1
  167. /package/dist/{chunk-4PVY4JJN.js.map → chunk-2TQ64TG3.js.map} +0 -0
  168. /package/dist/{chunk-G64BNLDO.js.map → chunk-3X6WFYSV.js.map} +0 -0
  169. /package/dist/{chunk-DUZEFACP.js.map → chunk-4UAIOXYC.js.map} +0 -0
  170. /package/dist/{chunk-SNNE34OP.js.map → chunk-5KD5SSP6.js.map} +0 -0
  171. /package/dist/{chunk-RKUCK6MN.js.map → chunk-6NGIPAQ6.js.map} +0 -0
  172. /package/dist/{chunk-R22IA7MP.js.map → chunk-BBDEIE56.js.map} +0 -0
  173. /package/dist/{chunk-PQ7YC5Y6.js.map → chunk-D5HO3RNF.js.map} +0 -0
  174. /package/dist/{chunk-K3O4D7LW.js.map → chunk-KUASHD7Z.js.map} +0 -0
  175. /package/dist/{chunk-FHTNCWIP.js.map → chunk-KYVFU7VQ.js.map} +0 -0
  176. /package/dist/{chunk-OYMOG544.js.map → chunk-KZAT6VWE.js.map} +0 -0
  177. /package/dist/{chunk-IJIIL6WU.js.map → chunk-MVU7HF5A.js.map} +0 -0
  178. /package/dist/{chunk-UH3BL2OT.js.map → chunk-Y7TZHPS2.js.map} +0 -0
@@ -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/** 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,98 @@
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
+ export declare function isRescueEmail(value: unknown): value is string;
50
+ export declare function isFormAttemptId(value: unknown): value is string;
51
+ /**
52
+ * Keep only allowlisted, non-excluded string values, trimmed and capped.
53
+ * The ONE filter, run by the browser before sending and by the host before storing.
54
+ */
55
+ export declare function sanitizeRescueValues(values: Record<string, unknown> | null | undefined): Record<string, string>;
56
+ /** Names of the given fields that hold something, credentials never included. */
57
+ export declare function filledRescueFields(values: Record<string, unknown>, fieldNames: readonly string[]): string[];
58
+ export declare function rescueCompletionPct(filledCount: number, totalCount: number): number;
59
+ /** A field NAME the host stores (`last_field`, `fields_filled`); bounded, printable. */
60
+ export declare function sanitizeRescueFieldName(value: unknown): string | null;
61
+ /** A form's progress at one moment: allowlisted values, filled field names, position. */
62
+ export interface FormDraftProgress {
63
+ values: Record<string, string>;
64
+ fields_filled: string[];
65
+ last_field: string | null;
66
+ completion_pct: number;
67
+ }
68
+ /** What the browser sends on every save (`POST <formDraftsUrl>`), beside the humanity signals. */
69
+ export interface FormDraftSaveRequest extends FormDraftProgress {
70
+ /** The attempt's idempotency key: every save of one attempt updates one draft. */
71
+ attempt_id: string;
72
+ form_id: FormRescueFormId;
73
+ source_path: string;
74
+ utm?: Partial<Record<'source' | 'medium' | 'campaign' | 'content' | 'term', string>>;
75
+ }
76
+ /** What the host answers a resume link with: the allowlisted values, nothing internal. */
77
+ export interface FormDraftResumeResponse {
78
+ form_id: FormRescueFormId;
79
+ values: Record<string, string>;
80
+ }
81
+ /** The PostHog events this feature emits. Properties are ids, names and numbers, never values. */
82
+ export declare const FORM_RESCUE_EVENTS: {
83
+ readonly started: "form_started";
84
+ readonly progress: "form_progress";
85
+ readonly leadCaptured: "form_lead_captured";
86
+ readonly abandoned: "form_abandoned";
87
+ readonly submitted: "form_submitted";
88
+ readonly rescued: "form_rescued";
89
+ readonly resumed: "form_resumed";
90
+ };
91
+ type AnalyticsValue = string | number | boolean | string[] | null;
92
+ /**
93
+ * Send one form event to PostHog when the page loaded it (GTM exposes
94
+ * `window.posthog`); a no-op otherwise. Never throws.
95
+ */
96
+ export declare function captureFormRescueEvent(event: string, properties: Record<string, AnalyticsValue>): void;
97
+ export {};
98
+ //# 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,96 @@
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 EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
37
+ function isRescueEmail(value) {
38
+ return typeof value === "string" && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());
39
+ }
40
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
41
+ function isFormAttemptId(value) {
42
+ return typeof value === "string" && UUID_PATTERN.test(value);
43
+ }
44
+ function isFilled(value) {
45
+ if (value == null) return false;
46
+ if (typeof value === "string") return value.trim().length > 0;
47
+ if (typeof value === "boolean") return value;
48
+ if (Array.isArray(value)) return value.length > 0;
49
+ if (typeof value === "object") return Object.values(value).some(isFilled);
50
+ return true;
51
+ }
52
+ function sanitizeRescueValues(values) {
53
+ const out = {};
54
+ if (!values || typeof values !== "object") return out;
55
+ for (const key of FORM_RESCUE_VALUE_FIELDS) {
56
+ if (isExcludedRescueField(key)) continue;
57
+ const raw = values[key];
58
+ if (typeof raw !== "string") continue;
59
+ const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);
60
+ if (trimmed) out[key] = trimmed;
61
+ }
62
+ return out;
63
+ }
64
+ function filledRescueFields(values, fieldNames) {
65
+ return fieldNames.filter((name) => !isExcludedRescueField(name) && isFilled(values[name])).slice(0, FORM_RESCUE_MAX_FIELDS);
66
+ }
67
+ function rescueCompletionPct(filledCount, totalCount) {
68
+ if (totalCount <= 0) return 0;
69
+ return Math.max(0, Math.min(100, Math.round(filledCount / totalCount * 100)));
70
+ }
71
+ function sanitizeRescueFieldName(value) {
72
+ if (typeof value !== "string") return null;
73
+ const trimmed = value.trim();
74
+ return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;
75
+ }
76
+ var FORM_RESCUE_EVENTS = {
77
+ started: "form_started",
78
+ progress: "form_progress",
79
+ leadCaptured: "form_lead_captured",
80
+ abandoned: "form_abandoned",
81
+ submitted: "form_submitted",
82
+ rescued: "form_rescued",
83
+ resumed: "form_resumed"
84
+ };
85
+ function captureFormRescueEvent(event, properties) {
86
+ try {
87
+ if (typeof window === "undefined") return;
88
+ const ph = window.posthog;
89
+ if (typeof ph?.capture === "function") ph.capture(event, properties);
90
+ } catch {
91
+ }
92
+ }
93
+
94
+ 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_SUBMIT_KEYS, FORM_RESCUE_VALUE_FIELDS, captureFormRescueEvent, filledRescueFields, isExcludedRescueField, isFormAttemptId, isFormRescueFormId, isRescueEmail, rescueCompletionPct, sanitizeRescueFieldName, sanitizeRescueValues };
95
+ //# sourceMappingURL=form-rescue.js.map
96
+ //# sourceMappingURL=form-rescue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/form-rescue.ts"],"names":[],"mappings":"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","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/** 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,99 @@ 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 EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
5339
+ function isRescueEmail(value) {
5340
+ return typeof value === "string" && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());
5341
+ }
5342
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5343
+ function isFormAttemptId(value) {
5344
+ return typeof value === "string" && UUID_PATTERN.test(value);
5345
+ }
5346
+ function isFilled(value) {
5347
+ if (value == null) return false;
5348
+ if (typeof value === "string") return value.trim().length > 0;
5349
+ if (typeof value === "boolean") return value;
5350
+ if (Array.isArray(value)) return value.length > 0;
5351
+ if (typeof value === "object") return Object.values(value).some(isFilled);
5352
+ return true;
5353
+ }
5354
+ function sanitizeRescueValues(values) {
5355
+ const out = {};
5356
+ if (!values || typeof values !== "object") return out;
5357
+ for (const key of FORM_RESCUE_VALUE_FIELDS) {
5358
+ if (isExcludedRescueField(key)) continue;
5359
+ const raw = values[key];
5360
+ if (typeof raw !== "string") continue;
5361
+ const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);
5362
+ if (trimmed) out[key] = trimmed;
5363
+ }
5364
+ return out;
5365
+ }
5366
+ function filledRescueFields(values, fieldNames) {
5367
+ return fieldNames.filter((name) => !isExcludedRescueField(name) && isFilled(values[name])).slice(0, FORM_RESCUE_MAX_FIELDS);
5368
+ }
5369
+ function rescueCompletionPct(filledCount, totalCount) {
5370
+ if (totalCount <= 0) return 0;
5371
+ return Math.max(0, Math.min(100, Math.round(filledCount / totalCount * 100)));
5372
+ }
5373
+ function sanitizeRescueFieldName(value) {
5374
+ if (typeof value !== "string") return null;
5375
+ const trimmed = value.trim();
5376
+ return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;
5377
+ }
5378
+ var FORM_RESCUE_EVENTS = {
5379
+ started: "form_started",
5380
+ progress: "form_progress",
5381
+ leadCaptured: "form_lead_captured",
5382
+ abandoned: "form_abandoned",
5383
+ submitted: "form_submitted",
5384
+ rescued: "form_rescued",
5385
+ resumed: "form_resumed"
5386
+ };
5387
+ function captureFormRescueEvent(event, properties) {
5388
+ try {
5389
+ if (typeof window === "undefined") return;
5390
+ const ph = window.posthog;
5391
+ if (typeof ph?.capture === "function") ph.capture(event, properties);
5392
+ } catch {
5393
+ }
5394
+ }
5395
+
5303
5396
  // src/utils/doc-tree-nav.ts
5304
5397
  var DEFAULT_FOLDER_INDEX_FILE = "README.md";
5305
5398
  var DEFAULT_FOLDER_INDEX = DEFAULT_FOLDER_INDEX_FILE;
@@ -6032,6 +6125,17 @@ exports.DEV_SECTION_PARAM_KEYS = DEV_SECTION_PARAM_KEYS;
6032
6125
  exports.DOC_TABLE_TYPES = DOC_TABLE_TYPES;
6033
6126
  exports.ELAPSED_MS_FIELD = ELAPSED_MS_FIELD;
6034
6127
  exports.FIRST_TOUCH_ATTRIBUTION_KEY = FIRST_TOUCH_ATTRIBUTION_KEY;
6128
+ exports.FORM_DRAFT_STATUSES = FORM_DRAFT_STATUSES;
6129
+ exports.FORM_RESCUE_ATTEMPT_FIELD = FORM_RESCUE_ATTEMPT_FIELD;
6130
+ exports.FORM_RESCUE_DEBOUNCE_MS = FORM_RESCUE_DEBOUNCE_MS;
6131
+ exports.FORM_RESCUE_EVENTS = FORM_RESCUE_EVENTS;
6132
+ exports.FORM_RESCUE_FORM_IDS = FORM_RESCUE_FORM_IDS;
6133
+ exports.FORM_RESCUE_MAX_FIELDS = FORM_RESCUE_MAX_FIELDS;
6134
+ exports.FORM_RESCUE_MAX_VALUE_CHARS = FORM_RESCUE_MAX_VALUE_CHARS;
6135
+ exports.FORM_RESCUE_RESUME_FIELD = FORM_RESCUE_RESUME_FIELD;
6136
+ exports.FORM_RESCUE_RESUME_PARAM = FORM_RESCUE_RESUME_PARAM;
6137
+ exports.FORM_RESCUE_SUBMIT_KEYS = FORM_RESCUE_SUBMIT_KEYS;
6138
+ exports.FORM_RESCUE_VALUE_FIELDS = FORM_RESCUE_VALUE_FIELDS;
6035
6139
  exports.FullscreenSwitchController = FullscreenSwitchController;
6036
6140
  exports.GENERIC_EMAIL_DOMAINS = GENERIC_EMAIL_DOMAINS;
6037
6141
  exports.HEADING_EMOJI_RE = HEADING_EMOJI_RE;
@@ -6091,6 +6195,7 @@ exports.buildSuggestionUrl = buildSuggestionUrl;
6091
6195
  exports.buildTicketOpenHref = buildTicketOpenHref;
6092
6196
  exports.canonicalContentRefType = canonicalContentRefType;
6093
6197
  exports.captureFirstTouchAttribution = captureFirstTouchAttribution;
6198
+ exports.captureFormRescueEvent = captureFormRescueEvent;
6094
6199
  exports.clamp = clamp;
6095
6200
  exports.clampPageToTotal = clampPageToTotal;
6096
6201
  exports.classifySocialHost = classifySocialHost;
@@ -6138,6 +6243,7 @@ exports.extractSections = extractSections;
6138
6243
  exports.faqItemAnchor = faqItemAnchor;
6139
6244
  exports.faqSectionSlug = faqSectionSlug;
6140
6245
  exports.fetchPriorityProp = fetchPriorityProp;
6246
+ exports.filledRescueFields = filledRescueFields;
6141
6247
  exports.findAnchorElementByNormalizedId = findAnchorElementByNormalizedId;
6142
6248
  exports.findDocNodeByPath = findDocNodeByPath;
6143
6249
  exports.findHoneypotCopySource = findHoneypotCopySource;
@@ -6268,7 +6374,10 @@ exports.isBadgePaletteKey = isBadgePaletteKey;
6268
6374
  exports.isBlankLine = isBlankLine;
6269
6375
  exports.isCrossOriginUrl = isCrossOriginUrl;
6270
6376
  exports.isDismissedCookieValue = isDismissedCookieValue;
6377
+ exports.isExcludedRescueField = isExcludedRescueField;
6271
6378
  exports.isFigmaSlidesUrl = isFigmaSlidesUrl;
6379
+ exports.isFormAttemptId = isFormAttemptId;
6380
+ exports.isFormRescueFormId = isFormRescueFormId;
6272
6381
  exports.isGenericDomain = isGenericDomain;
6273
6382
  exports.isGenericWebsiteDomain = isGenericWebsiteDomain;
6274
6383
  exports.isHumanitySignalKey = isHumanitySignalKey;
@@ -6278,6 +6387,7 @@ exports.isLocalUrl = isLocalUrl;
6278
6387
  exports.isOSPlatform = isOSPlatform;
6279
6388
  exports.isOrderedRange = isOrderedRange;
6280
6389
  exports.isPublishableMediaType = isPublishableMediaType;
6390
+ exports.isRescueEmail = isRescueEmail;
6281
6391
  exports.isToday = isToday;
6282
6392
  exports.isValidEmailDomain = isValidEmailDomain;
6283
6393
  exports.isValidSchedulingSlug = isValidSchedulingSlug;
@@ -6337,11 +6447,14 @@ exports.readDismissCookie = readDismissCookie;
6337
6447
  exports.readLeadingDecisionFrame = readLeadingDecisionFrame;
6338
6448
  exports.recordCountLabel = recordCountLabel;
6339
6449
  exports.releaseTypeToBadgeColor = releaseTypeToBadgeColor;
6450
+ exports.rescueCompletionPct = rescueCompletionPct;
6340
6451
  exports.resolveContentFetchPath = resolveContentFetchPath;
6341
6452
  exports.resolveRedirectTarget = resolveRedirectTarget;
6342
6453
  exports.resolveReleaseCover = resolveReleaseCover;
6343
6454
  exports.rgbToHex = rgbToHex;
6344
6455
  exports.sanitizeLandingUrl = sanitizeLandingUrl;
6456
+ exports.sanitizeRescueFieldName = sanitizeRescueFieldName;
6457
+ exports.sanitizeRescueValues = sanitizeRescueValues;
6345
6458
  exports.sanitizeTitleForChat = sanitizeTitleForChat;
6346
6459
  exports.scanHeadings = scanHeadings;
6347
6460
  exports.schedulingAudienceKey = schedulingAudienceKey;