@parag.more.withsurface.com/forms-sdk 0.1.4

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 (177) hide show
  1. package/README.md +63 -0
  2. package/dist/index.cjs +2737 -0
  3. package/dist/index.cjs.map +7 -0
  4. package/dist/index.mjs +2714 -0
  5. package/dist/index.mjs.map +7 -0
  6. package/dist/react/index.cjs +44 -0
  7. package/dist/react/index.cjs.map +7 -0
  8. package/dist/react/index.mjs +21 -0
  9. package/dist/react/index.mjs.map +7 -0
  10. package/dist/surface-forms-sdk.min.js +2 -0
  11. package/dist/surface-forms-sdk.min.js.map +7 -0
  12. package/dist/types/bind/bindForm.d.ts +43 -0
  13. package/dist/types/bind/bindForm.d.ts.map +1 -0
  14. package/dist/types/bind/bindForm.test.d.ts +2 -0
  15. package/dist/types/bind/bindForm.test.d.ts.map +1 -0
  16. package/dist/types/bind/bookingWidget.d.ts +35 -0
  17. package/dist/types/bind/bookingWidget.d.ts.map +1 -0
  18. package/dist/types/bind/bookingWidget.test.d.ts +2 -0
  19. package/dist/types/bind/bookingWidget.test.d.ts.map +1 -0
  20. package/dist/types/core/client/emailValidation.d.ts +23 -0
  21. package/dist/types/core/client/emailValidation.d.ts.map +1 -0
  22. package/dist/types/core/client/emailValidation.test.d.ts +2 -0
  23. package/dist/types/core/client/emailValidation.test.d.ts.map +1 -0
  24. package/dist/types/core/client/responses.d.ts +35 -0
  25. package/dist/types/core/client/responses.d.ts.map +1 -0
  26. package/dist/types/core/client/responses.test.d.ts +2 -0
  27. package/dist/types/core/client/responses.test.d.ts.map +1 -0
  28. package/dist/types/core/client/runtimeConfig.contract.test.d.ts +2 -0
  29. package/dist/types/core/client/runtimeConfig.contract.test.d.ts.map +1 -0
  30. package/dist/types/core/client/runtimeConfig.d.ts +72 -0
  31. package/dist/types/core/client/runtimeConfig.d.ts.map +1 -0
  32. package/dist/types/core/client/runtimeConfig.test.d.ts +2 -0
  33. package/dist/types/core/client/runtimeConfig.test.d.ts.map +1 -0
  34. package/dist/types/core/client/scheduler.contract.test.d.ts +2 -0
  35. package/dist/types/core/client/scheduler.contract.test.d.ts.map +1 -0
  36. package/dist/types/core/client/scheduler.d.ts +121 -0
  37. package/dist/types/core/client/scheduler.d.ts.map +1 -0
  38. package/dist/types/core/client/scheduler.test.d.ts +2 -0
  39. package/dist/types/core/client/scheduler.test.d.ts.map +1 -0
  40. package/dist/types/core/client/transport.d.ts +56 -0
  41. package/dist/types/core/client/transport.d.ts.map +1 -0
  42. package/dist/types/core/client/transport.test.d.ts +2 -0
  43. package/dist/types/core/client/transport.test.d.ts.map +1 -0
  44. package/dist/types/core/engine/answerAudit.d.ts +21 -0
  45. package/dist/types/core/engine/answerAudit.d.ts.map +1 -0
  46. package/dist/types/core/engine/answerAudit.test.d.ts +2 -0
  47. package/dist/types/core/engine/answerAudit.test.d.ts.map +1 -0
  48. package/dist/types/core/engine/answerStates.contract.test.d.ts +2 -0
  49. package/dist/types/core/engine/answerStates.contract.test.d.ts.map +1 -0
  50. package/dist/types/core/engine/answerStates.d.ts +92 -0
  51. package/dist/types/core/engine/answerStates.d.ts.map +1 -0
  52. package/dist/types/core/engine/formEngine.d.ts +145 -0
  53. package/dist/types/core/engine/formEngine.d.ts.map +1 -0
  54. package/dist/types/core/engine/formEngine.test.d.ts +2 -0
  55. package/dist/types/core/engine/formEngine.test.d.ts.map +1 -0
  56. package/dist/types/core/engine/redirect.d.ts +36 -0
  57. package/dist/types/core/engine/redirect.d.ts.map +1 -0
  58. package/dist/types/core/engine/redirect.test.d.ts +2 -0
  59. package/dist/types/core/engine/redirect.test.d.ts.map +1 -0
  60. package/dist/types/core/engine/responsePayload.contract.test.d.ts +2 -0
  61. package/dist/types/core/engine/responsePayload.contract.test.d.ts.map +1 -0
  62. package/dist/types/core/engine/responsePayload.d.ts +74 -0
  63. package/dist/types/core/engine/responsePayload.d.ts.map +1 -0
  64. package/dist/types/core/engine/responsePayload.test.d.ts +2 -0
  65. package/dist/types/core/engine/responsePayload.test.d.ts.map +1 -0
  66. package/dist/types/core/engine/urlData.contract.test.d.ts +2 -0
  67. package/dist/types/core/engine/urlData.contract.test.d.ts.map +1 -0
  68. package/dist/types/core/engine/urlData.d.ts +30 -0
  69. package/dist/types/core/engine/urlData.d.ts.map +1 -0
  70. package/dist/types/core/engine/urlData.test.d.ts +2 -0
  71. package/dist/types/core/engine/urlData.test.d.ts.map +1 -0
  72. package/dist/types/core/engine/urlPrefill.d.ts +21 -0
  73. package/dist/types/core/engine/urlPrefill.d.ts.map +1 -0
  74. package/dist/types/core/engine/urlPrefill.test.d.ts +2 -0
  75. package/dist/types/core/engine/urlPrefill.test.d.ts.map +1 -0
  76. package/dist/types/core/events/dropoffDetection.d.ts +21 -0
  77. package/dist/types/core/events/dropoffDetection.d.ts.map +1 -0
  78. package/dist/types/core/events/dropoffDetection.test.d.ts +2 -0
  79. package/dist/types/core/events/dropoffDetection.test.d.ts.map +1 -0
  80. package/dist/types/core/events/emitter.d.ts +23 -0
  81. package/dist/types/core/events/emitter.d.ts.map +1 -0
  82. package/dist/types/core/events/emitter.test.d.ts +2 -0
  83. package/dist/types/core/events/emitter.test.d.ts.map +1 -0
  84. package/dist/types/core/events/formJourneyTracker.contract.test.d.ts +2 -0
  85. package/dist/types/core/events/formJourneyTracker.contract.test.d.ts.map +1 -0
  86. package/dist/types/core/events/formJourneyTracker.d.ts +69 -0
  87. package/dist/types/core/events/formJourneyTracker.d.ts.map +1 -0
  88. package/dist/types/core/events/formJourneyTracker.test.d.ts +2 -0
  89. package/dist/types/core/events/formJourneyTracker.test.d.ts.map +1 -0
  90. package/dist/types/core/events/pushEvents.contract.test.d.ts +2 -0
  91. package/dist/types/core/events/pushEvents.contract.test.d.ts.map +1 -0
  92. package/dist/types/core/events/pushEvents.d.ts +62 -0
  93. package/dist/types/core/events/pushEvents.d.ts.map +1 -0
  94. package/dist/types/core/events/pushEvents.test.d.ts +2 -0
  95. package/dist/types/core/events/pushEvents.test.d.ts.map +1 -0
  96. package/dist/types/core/events/startedDetection.d.ts +11 -0
  97. package/dist/types/core/events/startedDetection.d.ts.map +1 -0
  98. package/dist/types/core/events/startedDetection.test.d.ts +2 -0
  99. package/dist/types/core/events/startedDetection.test.d.ts.map +1 -0
  100. package/dist/types/core/events/viewDetection.d.ts +15 -0
  101. package/dist/types/core/events/viewDetection.d.ts.map +1 -0
  102. package/dist/types/core/events/viewDetection.test.d.ts +2 -0
  103. package/dist/types/core/events/viewDetection.test.d.ts.map +1 -0
  104. package/dist/types/core/identity/browserFingerprint.d.ts +40 -0
  105. package/dist/types/core/identity/browserFingerprint.d.ts.map +1 -0
  106. package/dist/types/core/identity/browserFingerprint.test.d.ts +2 -0
  107. package/dist/types/core/identity/browserFingerprint.test.d.ts.map +1 -0
  108. package/dist/types/core/identity/ensureIdentified.d.ts +27 -0
  109. package/dist/types/core/identity/ensureIdentified.d.ts.map +1 -0
  110. package/dist/types/core/identity/ensureIdentified.test.d.ts +2 -0
  111. package/dist/types/core/identity/ensureIdentified.test.d.ts.map +1 -0
  112. package/dist/types/core/identity/identify.contract.test.d.ts +2 -0
  113. package/dist/types/core/identity/identify.contract.test.d.ts.map +1 -0
  114. package/dist/types/core/identity/identify.d.ts +48 -0
  115. package/dist/types/core/identity/identify.d.ts.map +1 -0
  116. package/dist/types/core/identity/identify.test.d.ts +2 -0
  117. package/dist/types/core/identity/identify.test.d.ts.map +1 -0
  118. package/dist/types/core/identity/journey.contract.test.d.ts +2 -0
  119. package/dist/types/core/identity/journey.contract.test.d.ts.map +1 -0
  120. package/dist/types/core/identity/journey.d.ts +28 -0
  121. package/dist/types/core/identity/journey.d.ts.map +1 -0
  122. package/dist/types/core/identity/journey.test.d.ts +2 -0
  123. package/dist/types/core/identity/journey.test.d.ts.map +1 -0
  124. package/dist/types/core/identity/journey.testStubs.d.ts +5 -0
  125. package/dist/types/core/identity/journey.testStubs.d.ts.map +1 -0
  126. package/dist/types/core/identity/leadDataCache.d.ts +27 -0
  127. package/dist/types/core/identity/leadDataCache.d.ts.map +1 -0
  128. package/dist/types/core/identity/leadDataCache.test.d.ts +2 -0
  129. package/dist/types/core/identity/leadDataCache.test.d.ts.map +1 -0
  130. package/dist/types/enrichment/attachEnrichment.d.ts +35 -0
  131. package/dist/types/enrichment/attachEnrichment.d.ts.map +1 -0
  132. package/dist/types/enrichment/attachEnrichment.test.d.ts +2 -0
  133. package/dist/types/enrichment/attachEnrichment.test.d.ts.map +1 -0
  134. package/dist/types/index.d.ts +28 -0
  135. package/dist/types/index.d.ts.map +1 -0
  136. package/dist/types/react/index.d.ts +2 -0
  137. package/dist/types/react/index.d.ts.map +1 -0
  138. package/dist/types/react/ssr.test.d.ts +2 -0
  139. package/dist/types/react/ssr.test.d.ts.map +1 -0
  140. package/dist/types/react/useSurfaceForm.d.ts +25 -0
  141. package/dist/types/react/useSurfaceForm.d.ts.map +1 -0
  142. package/dist/types/react/useSurfaceForm.test.d.ts +2 -0
  143. package/dist/types/react/useSurfaceForm.test.d.ts.map +1 -0
  144. package/dist/types/surfaceForms.d.ts +120 -0
  145. package/dist/types/surfaceForms.d.ts.map +1 -0
  146. package/dist/types/surfaceForms.test.d.ts +2 -0
  147. package/dist/types/surfaceForms.test.d.ts.map +1 -0
  148. package/dist/types/tracking/attachTracking.d.ts +35 -0
  149. package/dist/types/tracking/attachTracking.d.ts.map +1 -0
  150. package/dist/types/tracking/attachTracking.test.d.ts +2 -0
  151. package/dist/types/tracking/attachTracking.test.d.ts.map +1 -0
  152. package/dist/types/tracking/conversionRules.contract.test.d.ts +2 -0
  153. package/dist/types/tracking/conversionRules.contract.test.d.ts.map +1 -0
  154. package/dist/types/tracking/conversionRules.d.ts +87 -0
  155. package/dist/types/tracking/conversionRules.d.ts.map +1 -0
  156. package/dist/types/tracking/conversionRules.test.d.ts +2 -0
  157. package/dist/types/tracking/conversionRules.test.d.ts.map +1 -0
  158. package/dist/types/tracking/vendors.d.ts +46 -0
  159. package/dist/types/tracking/vendors.d.ts.map +1 -0
  160. package/dist/types/tracking/vendors.test.d.ts +2 -0
  161. package/dist/types/tracking/vendors.test.d.ts.map +1 -0
  162. package/dist/types/validation/attachEmailValidation.d.ts +8 -0
  163. package/dist/types/validation/attachEmailValidation.d.ts.map +1 -0
  164. package/dist/types/validation/attachEmailValidation.test.d.ts +2 -0
  165. package/dist/types/validation/attachEmailValidation.test.d.ts.map +1 -0
  166. package/docs/api.md +310 -0
  167. package/docs/reference/binding.md +116 -0
  168. package/docs/reference/clients.md +212 -0
  169. package/docs/reference/engine.md +193 -0
  170. package/docs/reference/events.md +93 -0
  171. package/docs/reference/identity.md +116 -0
  172. package/llms.txt +317 -0
  173. package/package.json +25 -0
  174. package/skills/surface-forms-sdk/SKILL.md +153 -0
  175. package/skills/surface-forms-sdk/debugging.md +64 -0
  176. package/skills/surface-forms-sdk/editing.md +67 -0
  177. package/skills/surface-forms-sdk/patterns.md +222 -0
@@ -0,0 +1,64 @@
1
+ # Debugging an SDK form
2
+
3
+ Symptom → cause → check, distilled from real field runs. Surface never sees the customer's HTML,
4
+ so debugging always triangulates three views: the **browser console** (the SDK warns at runtime),
5
+ **`validate_form_html`** (the static gate), and **response read-back**
6
+ (`list_responses`/`get_response` — what actually landed). When in doubt, read a response back;
7
+ that is the only end-to-end truth.
8
+
9
+ ## First moves, always
10
+
11
+ 1. `form.state()` in the browser console — `currentStepId`, `answers`, `responseId`, `finished`
12
+ tell you most of the story. `form.capabilities` tells you what's actually wired
13
+ (enrichment/emailValidation/tracking) in this environment.
14
+ 2. Attach a firehose while reproducing:
15
+ ```js
16
+ [
17
+ "viewed",
18
+ "started",
19
+ "stepChanged",
20
+ "stepCompleted",
21
+ "completed",
22
+ "disqualified",
23
+ "saved",
24
+ "error",
25
+ ].forEach((e) => form.on(e, (p) => console.log("[surface]", e, p)));
26
+ ```
27
+ 3. Re-run `validate_form_html` against the deployed HTML — drift since the last validation is the
28
+ most common root cause after any record edit.
29
+
30
+ ## Symptom table
31
+
32
+ | Symptom | Likely cause | Check |
33
+ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
34
+ | `init` throws "could not fetch runtime config" | Wrong/missing `apiBaseUrl` — the error names the exact URL and host it tried | Use the `apiBaseUrl` from `get_sdk_binding_map`; curl the URL from the error |
35
+ | No responses at all | `preview: true` left in the deployed page (blocks ALL writes) | Grep the page for `preview`; `saved` event never fires in preview |
36
+ | No responses at all | Form never published — runtime-config serves the "not published yet" placeholder, so step/question ids don't match | Console shows unbound-questionId warnings; `publish_form` |
37
+ | Some answers missing | Binding drift: questionId unbound or misspelled | Console warning "record declares questionIds with no bound element"; `validate_form_html` |
38
+ | Some answers missing | Partial saves off and visitor abandoned mid-form | `trackPartialResponses` in settings; expected behavior |
39
+ | Finished response stored as a partial | SDK < 0.1.3 sent the terminal write fire-and-forget; current SDK awaits it — remaining cause is tearing the page down mid-flight | Upgrade; `await form.submit()` then `await form.flush()` before any programmatic close |
40
+ | 400 "Fields are missing or incorrectly formatted" | Hand-built `setAnswer` with the wrong shape (choice questions want structured lists, not label strings) | Compare the payload with the binding map's `answerShape`; prefer `data-*` binding, which builds choice answers itself |
41
+ | Choice answers wrong/empty | Radio/checkbox `value` attrs drifted from the record's option keys | `validate_form_html` warns option-key drift; diff against `choiceKeys` |
42
+ | Same email accepted one day, rejected the next | Fail-open verdict (`isValid: true, failed: true`) later resolving to a definitive one — expected waterfall behavior, not a bug | Check `failed` on the verdict; don't write tests expecting stable verdicts for fresh domains |
43
+ | Thank-you never shows | Record has no `endStepKind: "thank_you"` step, or the HTML has no container for it | Binding map: does a step carry `endStepKind`? Submit reveals it automatically when it exists |
44
+ | Disqualified visitors see the thank-you | No `endStepKind: "disqualified"` step; or page routes them manually to the wrong container | Same check; `disqualify()` only reveals a `disqualified` ending |
45
+ | Steps don't switch | `data-step-id` doesn't match record stepIds, or goto target invalid | Console warns on invalid `goToStep`; `validate_form_html` blocks unknown step ids |
46
+ | Duplicate responses per visitor | `init` called more than once without `destroy()` (SPA remounts) | One engine per page-life; call `form.destroy()` on teardown |
47
+ | GTM/GA4/Meta events not firing | Preview mode (tracking is fully blocked in preview), or settings not set | Deployed page without preview; `window.dataLayer` after a step submit; settings via `update_form_settings` |
48
+ | Conversions (ads rules) not firing | Rules gate on the trigger + URL conditions; terminal rules only fire on `completed`, never on disqualify; everything queues until the first `responseId` | Check rule trigger/conditions in the dashboard; watch for the `saved` event before expecting queued fires |
49
+ | Enrichment fields empty | The environment has no ENABLED enrichment provider — a record-declared `enrichmentSourceField` is then silently inert | `form.capabilities.enrichment` (false = inert); `debug: true` warns at boot; enable a provider in the dashboard |
50
+ | Lead attribution missing | `identify` blocked (preview) or ad-blocked; re-identify after SPA route change | `form.identify()`; network tab `identify` call |
51
+
52
+ ## Reading a response back
53
+
54
+ `list_responses` (newest first) → `get_response` with the id. Confirm: every expected questionId
55
+ present, choice answers carry the structured list, `surfaceTagStatus: "sdk"` in meta, and — if a
56
+ booking step exists — the scheduler answer has `eventScheduled: true` plus the meeting fields.
57
+ A response that looks right here ends the client-side investigation.
58
+
59
+ ## When the record and HTML disagree about reality
60
+
61
+ `get_sdk_binding_map` (draft AND `source: "live"`) is the arbiter: the live map is what the
62
+ deployed SDK actually fetched; the draft map is what the next publish will make true. An HTML
63
+ page validated against the draft but running against an older live record explains most
64
+ "validator says fine, runtime disagrees" reports — publish, or validate against `source: "live"`.
@@ -0,0 +1,67 @@
1
+ # Editing an existing SDK form
2
+
3
+ The record and the HTML live in different places (Surface vs the user's project) and only stay
4
+ consistent if every record edit is followed through to the HTML. This is the highest-drift-risk
5
+ journey in the SDK: a record edit that ships without its HTML counterpart silently degrades data
6
+ capture, and `validate_form_html` is the only gate that sees both sides.
7
+
8
+ ## The loop
9
+
10
+ 1. **Read the current record** — `get_form_structure` for the machine-address view (stepIds,
11
+ row/column addresses, questionIds), `get_sdk_binding_map` for what the HTML must bind. Note the
12
+ `revision` — pass it as `ifRevision` on every write so a concurrent edit fails loudly instead of
13
+ interleaving.
14
+ 2. **Edit the record** — `update_component` / `add_component` (with `placement`) /
15
+ `remove_component` / `update_step` / `batch_edit` (all-or-nothing, one `ifRevision` for the
16
+ whole batch — prefer it for multi-part restructures). `move_component` refuses on SDK forms;
17
+ position is presentation and lives in the HTML.
18
+ 3. **Re-fetch `get_sdk_binding_map` and diff against the HTML.** Every difference is work:
19
+ - **New question** → add its binding (`data-question-id`, `data-question-type`, field names per
20
+ `answerShape`). Until bound, it's a `validate_form_html` blocker (scheduler/file questions
21
+ excepted — their answers are written in code).
22
+ - **Removed question** → delete its binding; a stale `data-question-id` is a blocker (answers
23
+ would be dropped server-side).
24
+ - **Changed choice options** (`choiceKeys`) → update every radio/checkbox `value` to the new
25
+ keys. The SDK builds answers from control values, so stale values submit options that no
26
+ longer exist — `validate_form_html` warns on exactly this drift.
27
+ - **Changed IdentityInfo fields** (`fieldNames`) → update the `data-field-name` attributes.
28
+ - **New/removed steps** → add/remove the `data-step-id` container and adjust navigation markers
29
+ (`data-surface-goto-step` targets pointing at a removed step are blockers).
30
+ 4. **`validate_form_html`** with the updated HTML against `source: "draft"` (the default). Fix
31
+ every blocker; re-run until clean.
32
+ 5. **`validate_form`** if the edit touched more than presentation-adjacent content.
33
+ (`simulate_response` does not apply to SDK forms — it evaluates record routing.)
34
+ 6. **`publish_form` with `confirm: true`.** Draft edits are invisible to the SDK until this —
35
+ and once a form has been published, `preview: true` serves the LIVE config, not the draft
36
+ (the draft path needs a never-published form plus the `previewToken` from
37
+ `get_sdk_binding_map`). So for edits to a published form, `validate_form_html` is the
38
+ pre-publish gate; there is no draft preview.
39
+ 7. **Deploy the HTML change and publish the record together.** If the HTML deploy and
40
+ `publish_form` can't be simultaneous, sequence by change type — every ordering has a
41
+ divergence window, so pick the one whose window degrades least:
42
+ - **Additions → publish the record first.** During the window the old HTML doesn't show the
43
+ new question, so responses arrive without it — nothing errors, but a newly-required
44
+ question goes unanswered until the deploy lands (SDK ingest does not enforce `required`
45
+ server-side). If that gap is unacceptable, hold the publish until the deploy is ready to
46
+ go out with it. The reverse order is strictly worse: deployed bindings for a questionId
47
+ that isn't on the live record yet let visitors answer a visible question whose answer is
48
+ dropped server-side.
49
+ - **Removals or choice-key changes → deploy the HTML first.** The stale record briefly
50
+ declares a question nobody is asked (harmless); the reverse leaves visitors answering
51
+ removed questions or submitting stale option keys.
52
+ Never leave the two sides diverged longer than a deploy cycle.
53
+ 8. **Read back one response** (`list_responses` / `get_response`) after the first post-edit
54
+ submission and confirm the changed answers landed with the expected shape.
55
+
56
+ ## Record-side effects worth knowing
57
+
58
+ - Changing a question's text also updates its label (`questionName`) if the label was never
59
+ customized — the responses table follows automatically.
60
+ - Replacing an IdentityInfo's `fields[]` re-validates `enrichmentSourceField` against the new
61
+ keys; pointing it at a removed field is rejected in the same call (update or clear it together
62
+ with `fields`).
63
+ - Content edits rebuild content-derived state (choice lists) but preserve `questionId`s — existing
64
+ HTML bindings and analytics survive a re-word.
65
+ - `update_form_settings` changes (tracking ids, language, partial-response gates) propagate to the
66
+ edge on confirm and do NOT need a `publish_form` — but allow a few minutes for the CDN response
67
+ cache to roll over before verifying.
@@ -0,0 +1,222 @@
1
+ # SDK form HTML patterns
2
+
3
+ Concrete shapes for the authoring step. Substitute the real `environmentId`/`formId`/
4
+ `questionId`/`stepId`/`apiBaseUrl` values from `get_sdk_binding_map` — never invent ids.
5
+ Class names and layout are examples; keep the user's own markup style.
6
+
7
+ ## Boot — script tag (plain HTML page)
8
+
9
+ ```html
10
+ <form id="contact-form">
11
+ <!-- bindings here -->
12
+ </form>
13
+ <script src="/vendor/surface-forms-sdk.min.js"></script>
14
+ <script>
15
+ SurfaceFormsSDK.SurfaceForms.init({
16
+ environmentId: "env_xxx",
17
+ formId: "form_xxx",
18
+ apiBaseUrl: "https://…", // from get_sdk_binding_map — required off the default host
19
+ container: document.querySelector("#contact-form"),
20
+ preview: true, // remove before deploying
21
+ previewToken: "…", // with preview: true, serves a never-published form's DRAFT config
22
+ });
23
+ </script>
24
+ ```
25
+
26
+ ESM projects: `import { SurfaceForms } from "@parag.more.withsurface.com/forms-sdk"` (substitute the
27
+ installed package name) and pass the same options.
28
+
29
+ ## Single-step form
30
+
31
+ No `data-step-id` needed. The `<form>` container's native submit finishes, so a plain submit button works; `surface-submit-button` is for non-form containers or extra buttons.
32
+
33
+ ```html
34
+ <form id="contact-form">
35
+ <label
36
+ >Work email
37
+ <input type="email" data-question-id="q_email" data-question-type="ShortInput" data-field-name="input" />
38
+ </label>
39
+ <label
40
+ >Company size
41
+ <select data-question-id="q_size" data-question-type="Dropdown">
42
+ <option>1-10</option>
43
+ <option>11-50</option>
44
+ </select>
45
+ </label>
46
+ <button type="submit">Send</button>
47
+ </form>
48
+ ```
49
+
50
+ Note the email input: when the record's component is `ShortInput`, the explicit attributes
51
+ override the SDK's `type=email → EmailForm` inference. The Dropdown's `<select>` infers its
52
+ state key (`selected`); its option values must equal the record's option keys.
53
+
54
+ ## Choice question (radios/checkboxes)
55
+
56
+ The SDK builds the structured answer from the whole group — `data-question-type` on the container is REQUIRED, each control's `value` must equal the record option key exactly, and no `data-field-name` is set:
57
+
58
+ ```html
59
+ <fieldset data-question-id="q_role" data-question-type="MultipleOptionsQuestion">
60
+ <label><input type="radio" name="role" value="Revenue / Sales Ops" /> Revenue / Sales Ops</label>
61
+ <label><input type="radio" name="role" value="Sales leadership" /> Sales leadership</label>
62
+ <label><input type="radio" name="role" value="Other" /> Other</label>
63
+ </fieldset>
64
+ ```
65
+
66
+ Multi-select records use checkboxes with the same markup. `validate_form_html` cross-checks the control values against the record's options.
67
+
68
+ ## Multi-step with a branch, disqualify, and a thank-you ending
69
+
70
+ One `data-step-id` container per record step — the SDK shows one at a time (mark the
71
+ containers after the first `hidden` to avoid a boot flash). The branch is the page's own
72
+ buttons; the record has no routing. The submit button lives on the LAST QUESTION step: after
73
+ the terminal write the SDK reveals the record's `endStepKind: "thank_you"` step by itself
74
+ (and the `"disqualified"` one on disqualify).
75
+
76
+ ```html
77
+ <form id="qualify-form">
78
+ <section data-step-id="step_role">
79
+ <label
80
+ >Your role
81
+ <input data-question-id="q_role" />
82
+ </label>
83
+ <button type="button" data-surface-goto-step="step_company">I'm a buyer</button>
84
+ <button type="button" class="surface-disqualify-button">Just browsing</button>
85
+ </section>
86
+
87
+ <section data-step-id="step_company" hidden>
88
+ <label
89
+ >Company
90
+ <input data-question-id="q_company" />
91
+ </label>
92
+ <button type="submit" class="surface-submit-button">Finish</button>
93
+ </section>
94
+
95
+ <section data-step-id="step_done" hidden>
96
+ <p>Thanks — we'll be in touch.</p>
97
+ </section>
98
+ </form>
99
+ ```
100
+
101
+ Dynamic branching instead of static buttons: keep a handle from `init` and call
102
+ `form.goToStep(stepId)` / `form.disqualify()` from your own logic, and declare
103
+ `data-surface-nav="js"` on any element so `validate_form_html` knows navigation/submit is
104
+ code-driven. Full event list for your own listeners: `viewed`, `started`, `stepChanged`,
105
+ `stepCompleted`, `completed`, `disqualified`, `saved`, `error`.
106
+
107
+ ## Multi-field component
108
+
109
+ `data-field-name` resolves per control — set it on each input, not (only) the wrapper. `IdentityInfo` is the standard contact-details component; bind only the fields the record declares (its `fieldNames` in the binding map):
110
+
111
+ ```html
112
+ <div data-question-id="q_contact" data-question-type="IdentityInfo">
113
+ <input data-field-name="firstName" placeholder="First name" />
114
+ <input data-field-name="lastName" placeholder="Last name" />
115
+ <input type="email" data-field-name="workEmailAddress" placeholder="Work email" />
116
+ </div>
117
+ ```
118
+
119
+ ## Scheduler step (Surface scheduler)
120
+
121
+ A `CalendlyScreen` with `calendarType: "Surface"` renders through the SDK's own unstyled widget — never iframe the hosted booking page, and never bind the question in HTML (the `onComplete` writes the answer):
122
+
123
+ ```html
124
+ <section data-step-id="step_book" hidden>
125
+ <div id="booking"></div>
126
+ </section>
127
+ <script>
128
+ const { createSchedulerClient, createTransport, mountBookingWidget, toSchedulerAnswerState } =
129
+ SurfaceFormsSDK;
130
+ const scheduler = createSchedulerClient({ transport: createTransport({ apiBaseUrl }), preview: PREVIEW });
131
+ mountBookingWidget({
132
+ container: document.querySelector("#booking"),
133
+ scheduler,
134
+ eventTypeId: "evt_xxx", // the record's CalendlyScreen content.eventTypeId
135
+ identity: { responseId: form.state().responseId ?? undefined },
136
+ onComplete(booking) {
137
+ form.setAnswer("q_booking", { ...toSchedulerAnswerState(booking), type: "CalendlyScreen" });
138
+ form.submit();
139
+ },
140
+ });
141
+ </script>
142
+ ```
143
+
144
+ The widget emits semantic markup under stable `surface-booking-*` classes (full DOM tree in
145
+ `llms.txt`) and injects no CSS — style from the page. Labels are locale-formatted by default
146
+ with the raw value on each element's `data-value`; pass `formatters: { month?, date?, time? }`
147
+ to override the copy. In preview mode the client returns a synthetic booking, so the flow is
148
+ testable offline.
149
+
150
+ ## React adapter (SPA)
151
+
152
+ Headless mode: `init` without `container` (no `data-*` binding), one engine per mounted form, `destroy()` on unmount — remounting without destroy creates duplicate responses. Answers are set imperatively with the exact `answerShape` from the binding map (choice questions included — there is no group auto-build without the bind layer).
153
+
154
+ ```tsx
155
+ import { SurfaceForms, type SurfaceForm } from "@parag.more.withsurface.com/forms-sdk";
156
+ import { useSurfaceForm } from "@parag.more.withsurface.com/forms-sdk/react";
157
+ import { useEffect, useState } from "react";
158
+
159
+ export function QualifyForm() {
160
+ const [form, setForm] = useState<SurfaceForm | null>(null);
161
+
162
+ useEffect(() => {
163
+ let handle: SurfaceForm | null = null;
164
+ let disposed = false;
165
+ void SurfaceForms.init({
166
+ environmentId: "env_xxx",
167
+ formId: "form_xxx",
168
+ apiBaseUrl: "https://…", // from get_sdk_binding_map
169
+ }).then((created) => {
170
+ if (disposed) created.destroy();
171
+ else {
172
+ handle = created;
173
+ setForm(created);
174
+ }
175
+ });
176
+ return () => {
177
+ disposed = true;
178
+ handle?.destroy();
179
+ };
180
+ }, []);
181
+
182
+ if (!form) return null;
183
+ return <QualifySteps form={form} />;
184
+ }
185
+
186
+ function QualifySteps({ form }: { form: SurfaceForm }) {
187
+ const { state, setAnswer, goToStep, disqualify, submit } = useSurfaceForm(form);
188
+ const role = (state.answers[state.currentStepId]?.q_role as { input?: string })?.input ?? "";
189
+
190
+ if (state.currentStepId === "step_role") {
191
+ return (
192
+ <section>
193
+ <input
194
+ value={role}
195
+ onChange={(e) => setAnswer("q_role", { type: "ShortInput", input: e.target.value })}
196
+ />
197
+ <button onClick={() => goToStep("step_company")}>I'm a buyer</button>
198
+ <button onClick={() => disqualify()}>Just browsing</button>
199
+ </section>
200
+ );
201
+ }
202
+ if (state.currentStepId === "step_done") return <p>Thanks — we'll be in touch.</p>;
203
+ return (
204
+ <section>
205
+ {/* … company inputs … */}
206
+ <button onClick={() => submit()}>Finish</button>
207
+ </section>
208
+ );
209
+ }
210
+ ```
211
+
212
+ `state` is `useSyncExternalStore`-backed: referentially stable between engine mutations, safe to render from directly. `submit()`/`disqualify()` move `state.currentStepId` to the record's matching ending step, so the same step-switch renders the confirmation. On SPA route changes call `form.identify()` to re-attribute the visitor; on programmatic teardown after a submit flow, `await form.flush()` first.
213
+
214
+ ## Tracking setup
215
+
216
+ Tracking is settings-driven and needs no page code — the SDK bootstraps the vendors and fires the same events as native forms (`SurfaceFormStarted`, `SurfaceFormStepSubmitted`, `SurfaceFormSubmitEvent` on GTM's dataLayer; Meta `trackCustom`; GA4 events).
217
+
218
+ - **Set via MCP**: `update_form_settings` accepts `gtmId`, `gaMeasurementId`, `metaPixelId`, `trackPartialResponses`, `trackCookie` (two-phase: preview → `confirm: true`). Ad-conversion rules (`adsConversions`) and HubSpot are **not settable via MCP** — the user configures those in the dashboard (Form settings → tracking).
219
+ - **Or pass at init**: `SurfaceForms.init({ settings: { gtmId, gaMeasurementId, metaPixelId, adsConversions } })` — caller values win over the record's, which lets a customer wire conversions entirely client-side.
220
+ - **Preview blocks tracking entirely** (no vendor scripts load, nothing fires) — always verify on a deployed non-preview page.
221
+ - **Verify**: complete a step, then inspect `window.dataLayer` for `SurfaceFormStepSubmitted` (its `stepId` is the real step id and the submit event carries `formId`). Conversion rules fire once per rule per response, queue until the first save produces a `responseId`, and terminal rules fire on completion only — never on disqualify.
222
+ - `form.capabilities.tracking` tells you whether any tracking config is active for this init.