@parag.more.withsurface.com/forms-sdk 0.1.5 → 0.1.7

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 (107) hide show
  1. package/README.md +12 -6
  2. package/dist/index.cjs +1 -2736
  3. package/dist/index.mjs +1 -2713
  4. package/dist/react/index.cjs +1 -43
  5. package/dist/react/index.mjs +1 -20
  6. package/dist/surface-forms-sdk.min.js +1 -1
  7. package/dist/types/bind/bindForm.d.ts +2 -0
  8. package/dist/types/bind/bookingWidget.d.ts +29 -6
  9. package/dist/types/bind/scheduler/embedUtils.d.ts +51 -0
  10. package/dist/types/bind/scheduler/mountScheduler.d.ts +2 -0
  11. package/dist/types/bind/scheduler/providers/cal.d.ts +3 -0
  12. package/dist/types/bind/scheduler/providers/calendly.d.ts +3 -0
  13. package/dist/types/bind/scheduler/providers/chilical.d.ts +3 -0
  14. package/dist/types/bind/scheduler/providers/clari.d.ts +3 -0
  15. package/dist/types/bind/scheduler/providers/hubspot.d.ts +3 -0
  16. package/dist/types/bind/scheduler/providers/index.d.ts +4 -0
  17. package/dist/types/bind/scheduler/providers/reclaimai.d.ts +3 -0
  18. package/dist/types/bind/scheduler/providers/revenuehero.d.ts +3 -0
  19. package/dist/types/bind/scheduler/providers/savvycal.d.ts +3 -0
  20. package/dist/types/bind/scheduler/providers/surface.d.ts +3 -0
  21. package/dist/types/bind/scheduler/providers/zoom.d.ts +3 -0
  22. package/dist/types/bind/scheduler/styles.d.ts +3 -0
  23. package/dist/types/bind/scheduler/types.d.ts +65 -0
  24. package/dist/types/core/client/emailValidation.d.ts +3 -0
  25. package/dist/types/core/client/responses.d.ts +3 -0
  26. package/dist/types/core/client/runtimeConfig.d.ts +40 -0
  27. package/dist/types/core/client/scheduler.d.ts +4 -0
  28. package/dist/types/core/client/transport.d.ts +4 -0
  29. package/dist/types/core/engine/answerAudit.d.ts +3 -0
  30. package/dist/types/core/engine/answerStates.d.ts +4 -0
  31. package/dist/types/core/engine/formEngine.d.ts +18 -0
  32. package/dist/types/core/engine/redirect.d.ts +1 -0
  33. package/dist/types/core/engine/responsePayload.d.ts +4 -0
  34. package/dist/types/core/engine/schedulerEvents.d.ts +40 -0
  35. package/dist/types/core/engine/urlData.d.ts +2 -0
  36. package/dist/types/core/engine/urlPrefill.d.ts +1 -0
  37. package/dist/types/core/events/dropoffDetection.d.ts +1 -0
  38. package/dist/types/core/events/emitter.d.ts +1 -0
  39. package/dist/types/core/events/formJourneyTracker.d.ts +1 -0
  40. package/dist/types/core/events/pushEvents.d.ts +2 -0
  41. package/dist/types/core/events/startedDetection.d.ts +1 -0
  42. package/dist/types/core/events/viewDetection.d.ts +1 -0
  43. package/dist/types/core/identity/browserFingerprint.d.ts +3 -0
  44. package/dist/types/core/identity/ensureIdentified.d.ts +1 -0
  45. package/dist/types/core/identity/identify.d.ts +1 -0
  46. package/dist/types/core/identity/journey.d.ts +3 -0
  47. package/dist/types/core/identity/leadDataCache.d.ts +1 -0
  48. package/dist/types/enrichment/attachEnrichment.d.ts +1 -0
  49. package/dist/types/index.d.ts +5 -2
  50. package/dist/types/react/useSurfaceForm.d.ts +1 -0
  51. package/dist/types/surfaceForms.d.ts +28 -1
  52. package/dist/types/tracking/attachTracking.d.ts +1 -0
  53. package/dist/types/tracking/conversionRules.d.ts +3 -0
  54. package/dist/types/tracking/vendors.d.ts +3 -0
  55. package/docs/api.md +59 -43
  56. package/docs/reference/binding.md +86 -13
  57. package/docs/reference/clients.md +21 -1
  58. package/docs/reference/engine.md +14 -7
  59. package/docs/reference/events.md +4 -1
  60. package/llms.txt +95 -29
  61. package/package.json +1 -1
  62. package/skills/surface-forms-sdk/SKILL.md +66 -26
  63. package/skills/surface-forms-sdk/debugging.md +21 -19
  64. package/skills/surface-forms-sdk/patterns.md +71 -5
  65. package/dist/types/bind/bindForm.test.d.ts +0 -1
  66. package/dist/types/bind/bookingWidget.test.d.ts +0 -1
  67. package/dist/types/core/client/emailValidation.test.d.ts +0 -1
  68. package/dist/types/core/client/responses.test.d.ts +0 -1
  69. package/dist/types/core/client/runtimeConfig.contract.test.d.ts +0 -1
  70. package/dist/types/core/client/runtimeConfig.test.d.ts +0 -1
  71. package/dist/types/core/client/scheduler.contract.test.d.ts +0 -1
  72. package/dist/types/core/client/scheduler.test.d.ts +0 -1
  73. package/dist/types/core/client/transport.test.d.ts +0 -1
  74. package/dist/types/core/engine/answerAudit.test.d.ts +0 -1
  75. package/dist/types/core/engine/answerStates.contract.test.d.ts +0 -1
  76. package/dist/types/core/engine/formEngine.test.d.ts +0 -1
  77. package/dist/types/core/engine/redirect.test.d.ts +0 -1
  78. package/dist/types/core/engine/responsePayload.contract.test.d.ts +0 -1
  79. package/dist/types/core/engine/responsePayload.test.d.ts +0 -1
  80. package/dist/types/core/engine/urlData.contract.test.d.ts +0 -1
  81. package/dist/types/core/engine/urlData.test.d.ts +0 -1
  82. package/dist/types/core/engine/urlPrefill.test.d.ts +0 -1
  83. package/dist/types/core/events/dropoffDetection.test.d.ts +0 -1
  84. package/dist/types/core/events/emitter.test.d.ts +0 -1
  85. package/dist/types/core/events/formJourneyTracker.contract.test.d.ts +0 -1
  86. package/dist/types/core/events/formJourneyTracker.test.d.ts +0 -1
  87. package/dist/types/core/events/pushEvents.contract.test.d.ts +0 -1
  88. package/dist/types/core/events/pushEvents.test.d.ts +0 -1
  89. package/dist/types/core/events/startedDetection.test.d.ts +0 -1
  90. package/dist/types/core/events/viewDetection.test.d.ts +0 -1
  91. package/dist/types/core/identity/browserFingerprint.test.d.ts +0 -1
  92. package/dist/types/core/identity/ensureIdentified.test.d.ts +0 -1
  93. package/dist/types/core/identity/identify.contract.test.d.ts +0 -1
  94. package/dist/types/core/identity/identify.test.d.ts +0 -1
  95. package/dist/types/core/identity/journey.contract.test.d.ts +0 -1
  96. package/dist/types/core/identity/journey.test.d.ts +0 -1
  97. package/dist/types/core/identity/journey.testStubs.d.ts +0 -3
  98. package/dist/types/core/identity/leadDataCache.test.d.ts +0 -1
  99. package/dist/types/enrichment/attachEnrichment.test.d.ts +0 -1
  100. package/dist/types/react/ssr.test.d.ts +0 -1
  101. package/dist/types/react/useSurfaceForm.test.d.ts +0 -1
  102. package/dist/types/surfaceForms.test.d.ts +0 -1
  103. package/dist/types/tracking/attachTracking.test.d.ts +0 -1
  104. package/dist/types/tracking/conversionRules.contract.test.d.ts +0 -1
  105. package/dist/types/tracking/conversionRules.test.d.ts +0 -1
  106. package/dist/types/tracking/vendors.test.d.ts +0 -1
  107. package/dist/types/validation/attachEmailValidation.test.d.ts +0 -1
package/docs/api.md CHANGED
@@ -35,25 +35,25 @@ exact URL and host it tried.
35
35
 
36
36
  ### `SurfaceFormsInitOptions`
37
37
 
38
- | Option | Type | Meaning |
39
- | --------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40
- | `environmentId` | `string` | Required. From `get_sdk_binding_map`. |
41
- | `formId` | `string` | Required. |
42
- | `apiBaseUrl` | `string?` | The environment's host. **Required unless the environment is on `https://forms.withsurface.com`** — `get_sdk_binding_map` returns it. |
43
- | `container` | `Element?` | Enables the `data-*` binding layer + view detection on this root (§4). Omit for fully headless use. |
44
- | `steps` | `Array<{id, endStepKind?}>?` | Caller-provided step order — **skips the record fetch entirely** (offline mode). Normally omitted. |
45
- | `settings` | object? | Behavioral gates + tracking config (`trackPartialResponses`, `trackCookies`, `gtmId`, `gaMeasurementId`, `metaPixelId`, `hubspotTracking`, `adsConversions`). Caller values win over the record's. |
46
- | `expectedQuestionIds` | `string[]?` | Overrides the record-derived list behind the unbound-question console warning. |
47
- | `resolveNextStep` | `ResolveNextStep?` | Routing hook for `next()`/`submit()` (§7.3); defaults to linear record order. |
48
- | `urlDataStateId` | `string?` | Seeds a `URL_DATA` attribution pseudo-answer under this id (rides every save). |
49
- | `source` | `LeadSource?` | Lead source for identify; defaults `"sdkForm"`. |
50
- | `journey` | `boolean?` | Page-view tracking module (§7.6), default off. |
51
- | `emailValidation` | `boolean?` | Automatic waterfall email validation (§5), default off — verdicts bill the form owner's validator quota. |
52
- | `preview` | `boolean?` | Blocks ALL network writes (identify, responses, events, journey, enrichment, tracking). Scheduler returns synthetic bookings. |
53
- | `previewToken` | `string?` | With `preview: true`, serves a never-published form's DRAFT config. From `create_form` / `get_sdk_binding_map`. |
54
- | `resume` | `{responseId, resumeToken}?` | Continue a stored response (§3). Ignored in preview. |
55
- | `debug` | `boolean?` | Logs lifecycle + audits every answer against the record; warns on states the server would reject and on inert config (declared enrichment source, no provider). Dev only. |
56
- | `meta` | `Record<string, unknown>?` | Seed response meta. `surfaceTagStatus` is always overwritten to `"sdk"`. |
38
+ | Option | Type | Meaning |
39
+ | --------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | `environmentId` | `string` | Required. From `get_sdk_binding_map`. |
41
+ | `formId` | `string` | Required. |
42
+ | `apiBaseUrl` | `string?` | The environment's host. **Required unless the environment is on `https://forms.withsurface.com`** — `get_sdk_binding_map` returns it. |
43
+ | `container` | `Element?` | Enables the `data-*` binding layer + view detection on this root (§4). Omit for fully headless use. |
44
+ | `steps` | `Array<{id, endStepKind?}>?` | Caller-provided step order — **skips the record fetch entirely** (offline mode). Normally omitted. |
45
+ | `settings` | object? | Behavioral gates + tracking config (`trackPartialResponses`, `trackCookies`, `gtmId`, `gaMeasurementId`, `metaPixelId`, `hubspotTracking`, `adsConversions`). Caller values win over the record's. |
46
+ | `expectedQuestionIds` | `string[]?` | Overrides the record-derived list behind the unbound-question console warning. |
47
+ | `resolveNextStep` | `ResolveNextStep?` | Routing hook for `next()`/`submit()` (§7.3); defaults to linear record order. |
48
+ | `urlDataStateId` | `string?` | Seeds a `URL_DATA` attribution pseudo-answer under this id (rides every save). |
49
+ | `source` | `LeadSource?` | Lead source for identify; defaults `"sdkForm"`. |
50
+ | `journey` | `boolean?` | Page-view tracking module (§7.6), default off. |
51
+ | `emailValidation` | `boolean?` | Automatic waterfall email validation (§5), default off — verdicts bill the form owner's validator quota. |
52
+ | `preview` | `boolean?` | Blocks ALL network writes (identify, responses, events, journey, enrichment, tracking). Scheduler returns synthetic bookings. |
53
+ | `previewToken` | `string?` | With `preview: true`, serves a never-published form's DRAFT config. From `create_form` / `get_sdk_binding_map`. |
54
+ | `resume` | `{responseId, resumeToken}?` | Continue a stored response (§3). Ignored in preview. |
55
+ | `debug` | `boolean?` | Logs lifecycle + navigation intent (`next requested from step …`, distinguishing an un-fired click from a blocked advance) + audits every answer against the record; warns on states the server would reject and on inert config (declared enrichment source, no provider). Dev only. |
56
+ | `meta` | `Record<string, unknown>?` | Seed response meta. `surfaceTagStatus` is always overwritten to `"sdk"`. |
57
57
 
58
58
  ## 2. The form handle — `SurfaceForm`
59
59
 
@@ -61,26 +61,28 @@ Everything the engine exposes (§7.3) minus internals, plus init-level helpers.
61
61
  Full method semantics, event payloads, and delivery guarantees:
62
62
  [`reference/engine.md`](reference/engine.md).
63
63
 
64
- | Member | Signature | Semantics |
65
- | ------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
66
- | `setAnswer` | `(questionId, state) => void` | Merges a partial state into the question's answer. Use the typed answer states (§6) — wrong shapes are the classic production 400. |
67
- | `next` / `back` | `() => Promise<void>` / `() => void` | Linear advance (saves progress; the step being LEFT is stamped on the write) / rewind (never writes). |
68
- | `goToStep` | `(stepId) => Promise<void>` | Customer-owned routing: forward jumps save like `next()`; jumps to a visited step rewind like `back()`. Rejects unknown ids. |
69
- | `submit` | `() => Promise<void>` | Finishes the form. **The terminal write is awaited** — when this resolves (and `saved` fires with `finished: true`), the server has the finished response; a delivery failure emits `error` instead. Then reveals the record's `thank_you` step. |
70
- | `disqualify` | `() => Promise<void>` | Finishes as disqualified (`invalidResponse: true`); reveals the `disqualified` ending. |
71
- | `savePartial` | `() => Promise<void>` | Explicit mid-form save (fire-and-forget beacon delivery). |
72
- | `flush` | `() => Promise<void>` | Resolves when every queued write (including post-finish amends) has settled. Await before programmatic teardown. |
73
- | `markViewed` | `() => void` | Emits `form_viewed` + first-step `form_step_viewed`, once. The bind layer calls it via IntersectionObserver; headless callers call it themselves. |
74
- | `state` | `() => FormEngineState` | `{ currentStepId, answers, externalStates, responseId, resumeToken, finished }`. Referentially stable between mutations (safe for `useSyncExternalStore`). |
75
- | `subscribe` | `(cb) => Unsubscribe` | Store subscription (fires on any state change). |
76
- | `on` | `(event, cb) => Unsubscribe` | Lifecycle events: `viewed`, `started`, `stepChanged {fromStepId,toStepId}`, `stepCompleted {stepId}`, `completed {formId}`, `disqualified {stepId}`, `saved {responseId, finished, resumeToken}`, `error {scope, error}`. |
77
- | `setExternalState` | `(id, {type, ...}) => void` | Writes a pseudo-answer that rides every save (enrichment results, custom attribution). Post-finish it amends the stored response. |
78
- | `mergeMeta` | `(partial) => void` | Records caller-computed values (qualification score, experiment bucket) onto response `meta`; rides the next save or amends post-finish. Reserved SDK keys (`surfaceTagStatus`, email-validation verdicts) are dropped with a warning. |
79
- | `identify` | `() => Promise<unknown>` | Re-runs visitor identify (SPA route change); later writes attribute to the fresh session. |
80
- | `validateEmail` | `(email) => Promise<EmailValidationResult>` | On-demand deliverability verdict (§5). |
81
- | `capabilities` | `{enrichment, emailValidation, tracking}` | What actually runs in THIS environment — a record can declare features the environment doesn't back. |
82
- | `runtimeConfig` | `RuntimeConfig?` | The fetched record (undefined when `steps` was caller-provided). |
83
- | `destroy` | `() => void` | Tears down every module binding. One engine per page-life re-`init` without `destroy` creates duplicate responses. |
64
+ | Member | Signature | Semantics |
65
+ | ------------------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
66
+ | `setAnswer` | `(questionId, state) => void` | Merges a partial state into the question's answer. Use the typed answer states (§6) — wrong shapes are the classic production 400. |
67
+ | `next` / `back` | `() => Promise<void>` / `() => void` | Linear advance switches the step immediately and resolves on the step change; the partial save (stamped with the step being LEFT) lands behind it / rewind (never writes). |
68
+ | `goToStep` | `(stepId) => Promise<void>` | Customer-owned routing: forward jumps save like `next()`; jumps to a visited step rewind like `back()`. Rejects unknown ids. |
69
+ | `submit` | `() => Promise<void>` | Finishes the form. **The terminal write is awaited** — when this resolves (and `saved` fires with `finished: true`), the server has the finished response; a delivery failure emits `error` instead. Then reveals the record's `thank_you` step. |
70
+ | `disqualify` | `() => Promise<void>` | Finishes as disqualified (`invalidResponse: true`); reveals the `disqualified` ending. |
71
+ | `savePartial` | `() => Promise<void>` | Explicit mid-form save (fire-and-forget beacon delivery). |
72
+ | `flush` | `() => Promise<void>` | Resolves when every queued write (including post-finish amends) has settled. Await before programmatic teardown. |
73
+ | `markViewed` | `() => void` | Emits `form_viewed` + first-step `form_step_viewed`, once. The bind layer calls it via IntersectionObserver; headless callers call it themselves. |
74
+ | `state` | `() => FormEngineState` | `{ currentStepId, answers, externalStates, responseId, resumeToken, finished }`. Referentially stable between mutations (safe for `useSyncExternalStore`). |
75
+ | `subscribe` | `(cb) => Unsubscribe` | Store subscription (fires on any state change). |
76
+ | `on` | `(event, cb) => Unsubscribe` | Lifecycle events: `viewed`, `started`, `stepChanged {fromStepId,toStepId}`, `stepCompleted {stepId}`, `completed {formId}`, `disqualified {stepId}`, `saved {responseId, finished, resumeToken}`, `error {scope, error}`, `scheduler` (`SchedulerLifecycleEvent`, §7.8). |
77
+ | `setExternalState` | `(id, {type, ...}) => void` | Writes a pseudo-answer that rides every save (enrichment results, custom attribution). Post-finish it amends the stored response. |
78
+ | `mergeMeta` | `(partial) => void` | Records caller-computed values (qualification score, experiment bucket) onto response `meta`; rides the next save or amends post-finish. Reserved SDK keys (`surfaceTagStatus`, email-validation verdicts) are dropped with a warning. |
79
+ | `identify` | `() => Promise<unknown>` | Re-runs visitor identify (SPA route change); later writes attribute to the fresh session. |
80
+ | `validateEmail` | `(email) => Promise<EmailValidationResult>` | On-demand deliverability verdict (§5). |
81
+ | `capabilities` | `{enrichment, emailValidation, tracking}` | What actually runs in THIS environment — a record can declare features the environment doesn't back. |
82
+ | `runtimeConfig` | `RuntimeConfig?` | The fetched record (undefined when `steps` was caller-provided). |
83
+ | `notifyScheduler` | `(event) => void` | Emits on the `scheduler` channel (used by `mountScheduler`; emit-only, never writes). |
84
+ | `transport` / `identity` / `preview` | advanced | The boot transport, the live identity getter, and the preview flag — for modules acting on the form's behalf (`mountScheduler` uses all three). |
85
+ | `destroy` | `() => void` | Tears down every module binding. One engine per page-life — re-`init` without `destroy` creates duplicate responses. |
84
86
 
85
87
  ## 3. Resume
86
88
 
@@ -184,9 +186,12 @@ Semantics that matter:
184
186
 
185
187
  - Partial saves happen on forward movement only, stamped with the step being
186
188
  left; the first forward move creates the response, later moves update it.
187
- - Every write-initiating call runs through one queue double-clicks advance
188
- once, a `submit()` during an in-flight `next()` queues behind it, nothing
189
- writes after the finishing submit.
189
+ They run BEHIND the step change `next()`/`goToStep()` never wait on the
190
+ network.
191
+ - Every write runs through one queue (the first move's create settles the
192
+ responseId before any update needs it). Finishing is the only navigation
193
+ that waits; a re-entrant `submit()`/`next()`/`disqualify()` while a finish
194
+ is pending joins it, and nothing writes after the finishing submit.
190
195
  - The FINISHING write is an awaited `fetch(keepalive)`; partial updates are
191
196
  beacons. A failed terminal write emits `error` (with a best-effort beacon
192
197
  fallback) and never fires `saved`.
@@ -268,6 +273,16 @@ true` page-view module (tag-compatible cookies, duplicate-URL skip, SPA
268
273
 
269
274
  Full `SchedulerClient` signatures and widget options: [`reference/clients.md`](reference/clients.md) + [`reference/binding.md`](reference/binding.md).
270
275
 
276
+ - **`mountScheduler(options): () => void`** — the DEFAULT scheduler path: one
277
+ call renders the record's scheduler question for ANY supported provider
278
+ (Surface's widget default-styled, or the third-party embed native forms
279
+ use), emits the `SchedulerLifecycleEvent` union on `form.on("scheduler")` +
280
+ `onEvent`, and auto-persists the booking into the response
281
+ (`persist: false` opts out). Options table, provider matrix, and
282
+ persistence semantics: [`reference/binding.md`](reference/binding.md).
283
+ - **`schedulerQuestionsOf(config): SchedulerQuestion[]`** — the record's
284
+ scheduler questions with provider + embed content
285
+ ([`reference/clients.md`](reference/clients.md)).
271
286
  - **`createSchedulerClient({ transport, preview? }): SchedulerClient`** —
272
287
  public Surface-scheduler endpoints: `getPublicEventType`,
273
288
  `getAvailabilitySummary`, `getMonthAvailability` (`YYYY-MM`), `getSlots`,
@@ -303,7 +318,8 @@ dependency; the main entry is React-free.
303
318
  throwing; event-handler exceptions are isolated.
304
319
  - **Preview writes nothing**: identify, responses, events, journey, enrichment,
305
320
  tracking, and email validation (synthetic verdicts) are all blocked.
306
- - **Delivery**: creates + the finishing update are awaited; mid-form updates
321
+ - **Delivery**: the finishing update is awaited; the create and mid-form
322
+ updates run behind the step change (navigation never waits on the network)
307
323
  and analytics are beacon-first fire-and-forget. `flush()` before
308
324
  programmatic teardown.
309
325
  - Send `X-Surface-Sdk-Version` on everything — visible server-side for rate
@@ -63,24 +63,97 @@ flash.)
63
63
  - `expectedQuestionIds` (init derives them from the record) minus the ids actually bound →
64
64
  a console warning naming the unbound ids; the runtime counterpart of `validate_form_html`.
65
65
 
66
+ ## `mountScheduler(options): () => void`
67
+
68
+ The DEFAULT scheduler path: renders the record's scheduler question for any supported provider,
69
+ emits the lifecycle on the form's `scheduler` event channel, and persists the booking into the
70
+ response automatically. Reach for `mountBookingWidget` (below) only when building a custom UI.
71
+
72
+ ### Options (`MountSchedulerOptions`)
73
+
74
+ | Option | Type | Semantics |
75
+ | ------------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
76
+ | `form` | `SurfaceForm` | The `init` handle — config, transport, identity, and persistence all come from it. |
77
+ | `container` | `Element` | The embed mounts here; teardown removes it. |
78
+ | `questionId` | `string?` | Required only when the record has >1 scheduler question (the error names the candidates). |
79
+ | `stepId` | `string?` | Alternative selector: the step carrying the scheduler question. |
80
+ | `styled` | `boolean?` (default `true`) | Injects a stylesheet scoped under `.surface-scheduler` on THIS container (Surface widget look + frame sizing). Override via `--surface-scheduler-*` custom props or the same class names; `false` keeps everything bare. |
81
+ | `persist` | `boolean?` (default `true`) | Auto-persist lifecycle + booking into the response. `false` still emits events but never writes (and never creates a response). |
82
+ | `prefill` | `{name?, lastName?, email?}?` | Overrides the prefill resolved from the record's linked answer refs (`linkNameToStep` etc.). |
83
+ | `formatters` | `{month?, date?, time?}?` | Surface provider only — forwarded to the booking widget. |
84
+ | `calendarId` | `string?` | DynamicScheduler only: pick a routed calendar by name. Without it: the default calendar, or a lone `calendars[0]`; ambiguous routing throws (conditions are your page's own routing). |
85
+ | `onEvent` | `(SchedulerLifecycleEvent) => void?` | Same events as `form.on("scheduler", …)`. |
86
+ | `onComplete` | `(SchedulerBookingSummary) => void?` | Fires alongside `booking_confirmed`. NOT needed for persistence — that's automatic. |
87
+
88
+ ### Lifecycle events (`SchedulerLifecycleEvent`)
89
+
90
+ Every event carries `{questionId, stepId, provider}` plus:
91
+
92
+ ```ts
93
+ | { type: "viewed" } // once per mount
94
+ | { type: "date_selected"; date? /* "YYYY-MM-DD" */ }
95
+ | { type: "slot_selected"; slot?: { start; end? } }
96
+ | { type: "form_opened" }
97
+ | { type: "booking_confirmed"; booking: SchedulerBookingSummary }
98
+ | { type: "booking_cancelled"; reason? }
99
+ ```
100
+
101
+ `SchedulerBookingSummary` = `{provider, uri, meetingTime?, meetingEndTime?, inviteeName?,
102
+ guestEmails?, ownerEmails?, joinLink?, raw?}` — `uri` is the provider-native booking id
103
+ (HubSpot's embed reports none; its uri is the single space native forms persist), `raw` the
104
+ untouched provider payload.
105
+
106
+ ### Providers
107
+
108
+ | Provider (record `calendarType`) | Embed | Booking signal | Extra lifecycle |
109
+ | -------------------------------- | -------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
110
+ | `Surface` | SDK booking widget (styled by default) | widget booking | `date_selected`, `slot_selected`, `form_opened`; sub-steps persist as `schedulerSubStep` |
111
+ | `Calendly` | iframe | postMessage + server-poll fallback | provider-reported `viewed`, `slot_selected`; poll-reported cancellation |
112
+ | `Cal` (Cal.com) | vendor script | `bookingSuccessful` CustomEvent | — |
113
+ | `SavvyCal` | vendor script | `savvycal.scheduled` CustomEvent | — |
114
+ | `ChiliCal` (Chili Piper) | iframe | `booking-confirmed` postMessage | — |
115
+ | `Hubspot` | vendor script | `meetingBookSucceeded` postMessage | — |
116
+ | `RevenueHero` | vendor script + router submit | `MEETING_BOOKED` postMessage | — |
117
+ | `Zoom` | iframe | `bookingForm` postMessage | server-poll backfills host email / times / join link into the answer |
118
+ | `ReclaimAI` | iframe | server poll ONLY (no client events) | poll carries the booking |
119
+ | `Clari` | iframe | **none** — embed-only; nothing is tracked or persisted | — |
120
+
121
+ postMessage listeners are origin-checked strictly (exact origin or anchored host suffix — the
122
+ SDK mirrors the vendor protocols, not the laxness some native checks have).
123
+
124
+ ### Persistence semantics
125
+
126
+ - Confirmed booking → the frozen scheduler answer keys (`eventScheduled`, `uri`,
127
+ `meetingTime`…, `type` = the record's component type) merged via `setAnswer`, then a partial
128
+ save; after `submit()` the engine amends the stored response instead. `meta.bookingId` (Surface
129
+ bookings only) and the `meeting_booked` ad-conversion trigger follow automatically.
130
+ - Poll-reported cancellation (after a confirmed booking) → `eventCancelled: true` + save.
131
+ - Calendly/Zoom/Reclaim embeds carry the `responseId` in their URL, so mounting them creates the
132
+ response with a partial save when none exists yet. The Surface widget resolves identity lazily
133
+ at book time instead — lookers who never book create nothing.
134
+ - `preview: true` on the form blocks every write and poll; the Surface widget books through the
135
+ fabricated preview client, so the whole flow demos offline.
136
+
66
137
  ## `mountBookingWidget(options): () => void`
67
138
 
68
- The unstyled Surface-scheduler booking flow (date → time → name/email form → booked) over a
69
- `SchedulerClient` ([`clients.md`](clients.md)). Never iframe the hosted booking page.
139
+ The custom-UI escape hatch: the unstyled Surface-scheduler booking flow (date → time →
140
+ name/email form → booked) over a `SchedulerClient` ([`clients.md`](clients.md)). Never iframe
141
+ the hosted booking page. For the zero-effort path use `mountScheduler` (above).
70
142
 
71
143
  ### Options (`BookingWidgetOptions`)
72
144
 
73
- | Option | Type | Semantics |
74
- | ------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
75
- | `container` | `Element` | The widget appends its own `div.surface-booking` root here; teardown removes it. |
76
- | `scheduler` | `SchedulerClient` | From `createSchedulerClient` (share the transport; pass `preview` there for synthetic bookings). |
77
- | `eventTypeId` | `string` | From the record's `CalendlyScreen` content. |
78
- | `timezone` | `string?` | Defaults to the visitor's (`Intl.DateTimeFormat().resolvedOptions().timeZone`); slot labels format in it. |
79
- | `duration` | `number?` | Threaded into availability/slots/book queries. |
80
- | `prefill` | `{name?, email?}?` | Pre-fills the widget's name/email inputs. |
81
- | `identity` | `{responseId?, leadSessionId?, fingerprint?}?` | Threaded into `book()` for booking↔response linkage. |
82
- | `formatters` | `{month?, date?, time?}?` | Override the copy: `month("YYYY-MM")`, `date("YYYY-MM-DD")`, `time(slot)` (ISO instants; format in the widget timezone). Defaults are locale-formatted via `Intl`. |
83
- | `onComplete` | `(booking: Booking) => void` | Fires after a successful booking persist via `setAnswer({...toSchedulerAnswerState(booking), type: "<record type>"})` and navigate. |
145
+ | Option | Type | Semantics |
146
+ | ------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
147
+ | `container` | `Element` | The widget appends its own `div.surface-booking` root here; teardown removes it. |
148
+ | `scheduler` | `SchedulerClient` | From `createSchedulerClient` (share the transport; pass `preview` there for synthetic bookings). |
149
+ | `eventTypeId` | `string` | From the record's `CalendlyScreen` content. |
150
+ | `timezone` | `string?` | Defaults to the visitor's (`Intl.DateTimeFormat().resolvedOptions().timeZone`); slot labels format in it. |
151
+ | `duration` | `number?` | Threaded into availability/slots/book queries. |
152
+ | `prefill` | `{name?, email?}?` | Pre-fills the widget's name/email inputs. |
153
+ | `identity` | `BookingIdentity \| (() => BookingIdentity \| Promise<BookingIdentity>)?` | Threaded into `book()` for booking↔response linkage. The getter form resolves at book time — a response created after mount still links. |
154
+ | `formatters` | `{month?, date?, time?}?` | Override the copy: `month("YYYY-MM")`, `date("YYYY-MM-DD")`, `time(slot)` (ISO instants; format in the widget timezone). Defaults are locale-formatted via `Intl`. |
155
+ | `onEvent` | `(BookingWidgetEvent) => void?` | Sub-step transitions: `viewed`, `date_selected {date}`, `slot_selected {slot}`, `form_opened` (fresh opens only, not error re-renders), `booking_confirmed {booking}` (alongside `onComplete`). |
156
+ | `onComplete` | `(booking: Booking) => void` | Fires after a successful booking — persist via `setAnswer({...toSchedulerAnswerState(booking), type: "<record type>"})` and navigate. |
84
157
 
85
158
  ### Behavior + DOM
86
159
 
@@ -158,7 +158,27 @@ server links booking↔response with them; pass what you have.
158
158
  override `type` to the record's component type when persisting
159
159
  (`{...toSchedulerAnswerState(b), type: "CalendlyScreen"}`).
160
160
 
161
- The DOM widget over this client (`mountBookingWidget`) is in [`binding.md`](binding.md).
161
+ The DOM widget over this client (`mountBookingWidget`) and the zero-effort embedded path
162
+ (`mountScheduler`) are in [`binding.md`](binding.md).
163
+
164
+ - **`schedulerQuestionsOf(config): SchedulerQuestion[]`** (`core/client/runtimeConfig`) — the
165
+ record's scheduler questions with everything `mountScheduler` reads:
166
+
167
+ ```ts
168
+ type SchedulerQuestion = {
169
+ questionId;
170
+ stepId;
171
+ componentType: "CalendlyScreen" | "DynamicScheduler"; // also the persisted answer `type`
172
+ calendarType: string; // provider; the record omits it for the original default, Calendly
173
+ content: {
174
+ dataUrl?; eventTypeId?; savvyCalLinkId?; revenueHeroRouterId?;
175
+ dataUrlParams?: Record<string, { stepId; questionId; variable }>; // answer-state refs → embed params
176
+ linkNameToStep? / linkLastNameToStep? / linkEmailToStep?: { stepId; questionId }; // prefill refs
177
+ addSchedulerQueryParams?; skipFormIfPrefilled?;
178
+ calendars?; defaultCalendar?; // DynamicScheduler routing
179
+ };
180
+ };
181
+ ```
162
182
 
163
183
  ## Email validation (`core/client/emailValidation` + `validation/attachEmailValidation`)
164
184
 
@@ -30,7 +30,10 @@ creates one for you; construct directly for offline/custom stacks.
30
30
 
31
31
  - **`setAnswer(questionId, state)`** — merges (`{...prev, ...state}`) into the current step's
32
32
  answer for that question. Fires `started` (and the `form_started` event) exactly once, on the
33
- first deep divergence from the initial answers. Notifies subscribers.
33
+ first deep divergence from the initial answers. Notifies subscribers. After the finishing
34
+ write it amends the stored response (like `setExternalState`/`mergeMeta`) — a booking
35
+ confirmed on a thank-you-step scheduler still persists; before that, it never writes on its
36
+ own (writes ride navigation/`savePartial`).
34
37
  - **`next(): Promise<void>`** — resolves the next step. Not finished → emits `stepCompleted`,
35
38
  pushes the `form_step_submitted`+`form_step_viewed` batch (`stepCompletionTime` = seconds since
36
39
  engine creation), records the journey transition, writes a partial (if enabled) stamped with the
@@ -78,15 +81,19 @@ resumeToken, finished}`. The snapshot is cached until the next mutation, so it i
78
81
  | `disqualified` | `{stepId}` | Disqualified finish. |
79
82
  | `saved` | `{responseId, finished, resumeToken}` | After a CONFIRMED write: every create, every partial dispatch, the awaited terminal update. Never fires for a failed terminal write. |
80
83
  | `error` | `{scope: "submit", error}` | Failed create (typed `TransportError`) or failed terminal update. |
84
+ | `scheduler` | `SchedulerLifecycleEvent` | Scheduler lifecycle, emitted via `notifyScheduler` (the `mountScheduler` path — union in [`binding.md`](binding.md)). Emit-only. |
81
85
 
82
86
  ### Write serialization + delivery
83
87
 
84
- - One queue serializes every write-initiating call: a double-clicked `next()` joins the pending
85
- advance (advances once); `submit()` during an in-flight `next()` queues behind it and then
86
- finishes (submit intent never dropped); queued ops re-check `finished` when they run, so
87
- nothing writes after the finishing submit.
88
- - First forward write CREATES the response (`POST /api/v1/responses/`, awaited, credentialed);
89
- later writes UPDATE it. Every write resends the FULL answer set + external states.
88
+ - Navigation is synchronous: `next()`/`goToStep()` switch the step and resolve immediately; the
89
+ partial save is queued behind the transition. Finishing is the only navigation that waits, and
90
+ a re-entrant `submit()`/`next()`/`disqualify()` while a finish is pending joins it (a
91
+ double-clicked submit finishes once; submit intent never dropped).
92
+ - One queue serializes every write; queued writes re-check `finished` when they run, so nothing
93
+ writes after the finishing submit.
94
+ - First forward write CREATES the response (`POST /api/v1/responses/`, credentialed, settled
95
+ before any later write runs); later writes UPDATE it. Every write resends the FULL answer set +
96
+ external states.
90
97
  - A failed create: `error` event; `response_submission_error` pushEvent only for network-kind
91
98
  failures (the server logs HTTP-level ones itself); the responseId stays null (the next write
92
99
  retries the create).
@@ -2,7 +2,10 @@
2
2
 
3
3
  Exhaustive reference for the `core/events` domain. Map of everything: [`../api.md`](../api.md).
4
4
  (The engine's own lifecycle events — `viewed`/`started`/`stepChanged`/… — are in
5
- [`engine.md`](engine.md); this file covers the modules beneath them.)
5
+ [`engine.md`](engine.md); the `scheduler` channel's `SchedulerLifecycleEvent` union is in
6
+ [`binding.md`](binding.md) with `mountScheduler`. This file covers the modules beneath them.
7
+ Scheduler lifecycle events are client-side only — the push-event vocabulary below is a closed
8
+ server contract and gains no scheduler types.)
6
9
 
7
10
  ## Push events (`core/events/pushEvents`)
8
11
 
package/llms.txt CHANGED
@@ -45,9 +45,11 @@ Options beyond the four above: `preview: true` (no network writes at all — ide
45
45
  responses, events, journey all blocked; pass `previewToken` from the `create_form`
46
46
  or `get_sdk_binding_map` reply alongside it and a never-published form serves its
47
47
  DRAFT config, so you can preview before the first publish), `debug: true`
48
- (logs every captured answer + lifecycle event and warns on states the server
49
- would reject AND on inert configuration like a declared enrichment source with
50
- no enabled provider; development only), `journey: true` (page-view tracking),
48
+ (logs every captured answer + lifecycle event + navigation intent a
49
+ `next requested from step …` line per `next()`/`submit()`/… call, so a click
50
+ that never fired is distinguishable from a blocked advance — and warns on
51
+ states the server would reject AND on inert configuration like a declared
52
+ enrichment source with no enabled provider; development only), `journey: true` (page-view tracking),
51
53
  `urlDataStateId` (attribution pseudo-answer), `emailValidation: true`
52
54
  (automatic verdicts, see below), `resume`, `meta`, `source`.
53
55
 
@@ -95,7 +97,12 @@ Then:
95
97
  ### Choice questions bind as a GROUP
96
98
 
97
99
  Five types are group-built — `MultipleOptionsQuestion`, `MultipleChoiceQuestion`,
98
- `MMChoiceQuestion`, `GraphicOptions`, and `MultipleChoiceSingleAnswer`. Their
100
+ `MMChoiceQuestion`, `GraphicOptions`, and `MultipleChoiceSingleAnswer`. All five
101
+ bind when present in the record, but only the first three are creatable via
102
+ `create_form` (`list_component_types` is the authority) —
103
+ `MultipleChoiceQuestion` and `MultipleChoiceSingleAnswer` are legacy types you
104
+ will only meet on pre-existing forms; build new records with
105
+ `MultipleOptionsQuestion`. Their
99
106
  answers are structured (the record's whole option list / a selection object,
100
107
  and their shapes differ — only `MultipleOptionsQuestion` carries an option
101
108
  `index`; trust each question's `answerShape` from the binding map, never a
@@ -129,9 +136,18 @@ Markers, most specific wins, ONE per element:
129
136
 
130
137
  A `<form>` container's native submit also finishes. Conditional branching =
131
138
  your JS choosing which goto button to show, or calling the API below. Driving
132
- navigation/submit entirely from JS (async gates like email validation)?
133
- Put `data-surface-nav="js"` on any element — it tells `validate_form_html`
134
- the page submits in code, silencing its advance/submit warnings.
139
+ navigation/submit entirely from JS (custom branching, an owner-requested
140
+ gate)? Put `data-surface-nav="js"` on any element — it tells
141
+ `validate_form_html` the page submits in code, silencing its advance/submit
142
+ warnings.
143
+
144
+ Timing: `next()`/`goToStep()` switch the step IMMEDIATELY and resolve on the
145
+ step change — the partial save runs behind it (`saved`/`error` report it). A
146
+ visitor never waits on a partial save, so don't disable the CTA for one, and
147
+ don't put async work of your own in front of the step change unless the form
148
+ owner asked for a gate (see Email validation). `submit()`/`disqualify()` are
149
+ the one wait: they await the finishing write before the ending step shows —
150
+ disable that button while the promise is pending.
135
151
 
136
152
  Finishing reveals the record's matching ending step: after the terminal write,
137
153
  `submit()` moves to the step with `endStepKind: "thank_you"` and `disqualify()`
@@ -152,12 +168,19 @@ SPA route change), `validateEmail(email)`, `mergeMeta(partial)`,
152
168
  `disqualified`, `saved` (carries `responseId` + `resumeToken`), `error`. Every
153
169
  `on`/`subscribe` call returns an unsubscribe function.
154
170
 
171
+ `state().answers` is keyed by stepId, then questionId
172
+ (`Record<stepId, Record<questionId, state>>`) — reading an EARLIER step's
173
+ answer (booking prefill, a summary screen) indexes by that step's id:
174
+ `state().answers["step_about"]?.["q_name"]`, not by the current step.
175
+
155
176
  Delivery guarantees: the FINISHING write is awaited — when `submit()` resolves
156
177
  (and `saved` fires with `finished: true`) the server has the finished response;
157
- a failure emits `error` instead of lying. Mid-form partial saves are
158
- fire-and-forget beacons reconciled server-side. `await form.flush()` before
159
- tearing the page down programmatically (SPA unmount, test harness closing the
160
- browser) so no queued write is cut off.
178
+ a failure emits `error` instead of lying. Partial saves never block: `next()`
179
+ resolves on the step change and the write lands behind it — the first one
180
+ creates the response and settles `responseId` (later writes queue behind it),
181
+ the rest are fire-and-forget beacons reconciled server-side. `await
182
+ form.flush()` before tearing the page down programmatically (SPA unmount, test
183
+ harness closing the browser) so no queued write is cut off.
161
184
 
162
185
  `mergeMeta(partial)` records caller-computed values (a qualification score, an
163
186
  experiment bucket) onto the response `meta` — they ride the next save, or amend
@@ -198,28 +221,38 @@ wire shapes; during development, `debug: true` audits them at runtime too.
198
221
 
199
222
  ## Email validation
200
223
 
224
+ Two mechanisms share one per-address verdict cache (an address is billed once
225
+ no matter which path asks). Automatic mode — `emailValidation: true` on
226
+ `init` — validates email answers as they're captured (debounced) and writes
227
+ the same `meta` verdict keys native forms write (`isEmailValid`,
228
+ `emailValidation`, `emailValidationFailed`) for dashboards and workflows.
201
229
  `form.validateEmail(email)` resolves the server-side deliverability verdict
202
230
  (provider waterfall + the environment's spam blacklist; keys stay
203
- server-side): `{ isValid, status, reason, failed? }`. Verdicts are cached per
204
- address for the form's lifetime and FAIL OPEN: when no validator can run, the
205
- result is `{ isValid: true, failed: true, status: "unknown" }`never lock a
206
- visitor out on a vendor outage. Two consequences to design for: (1) check
207
- `failed` if you want to soft-warn instead of hard-block; (2) the same address
208
- can be accepted today (fail-open) and return a definitive `invalid` tomorrow —
209
- that's the waterfall resolving, not a bug; don't write tests that assume a
210
- fixed verdict for a fresh domain. Use it to drive your own navigation:
231
+ server-side) on demand: `{ isValid, status, reason, failed? }`.
232
+
233
+ **What happens on an invalid address is the form owner's decision ask
234
+ before authoring, never assume.** The default is ANNOTATE ONLY: pass
235
+ `emailValidation: true`, let the visitor continue, and the response carries
236
+ `isEmailValid: false` for the owner's workflows and lead scoring to act on.
237
+ Automatic mode never blocks navigation or submission, and the visitor never
238
+ waits on a verdict. Only when the owner explicitly wants a gate do you call
239
+ `validateEmail` in your own navigation:
211
240
 
212
241
  ```js
242
+ // Owner chose BLOCK: keep the visitor on the step with a message.
213
243
  const { isValid, reason, failed } = await form.validateEmail(emailInput.value);
214
244
  if (isValid) form.next();
215
245
  else showError(`Email rejected: ${reason}`);
246
+ // Owner chose DISQUALIFY: form.disqualify() instead of showError.
216
247
  ```
217
248
 
218
- Pass `emailValidation: true` to `init` for automatic mode: email answers are
219
- validated as they're captured (debounced; per-address, so nothing is billed
220
- twice) and the response carries the same `meta` verdict keys native forms
221
- write (`isEmailValid`, `emailValidation`, `emailValidationFailed`) for
222
- dashboards and workflows. Preview resolves synthetic valid verdicts with no
249
+ Verdicts FAIL OPEN: when no validator can run, the result is
250
+ `{ isValid: true, failed: true, status: "unknown" }` — never lock a visitor
251
+ out on a vendor outage. Two consequences to design for: (1) check `failed` if
252
+ you want to soft-warn instead of hard-block; (2) the same address can be
253
+ accepted today (fail-open) and return a definitive `invalid` tomorrow that's
254
+ the waterfall resolving, not a bug; don't write tests that assume a fixed
255
+ verdict for a fresh domain. Preview resolves synthetic valid verdicts with no
223
256
  network. Offline callers wire it themselves: `createEmailValidator({
224
257
  transport, environmentId })` and optionally `attachEmailValidation({ engine,
225
258
  validator })`.
@@ -241,11 +274,41 @@ callers (caller-provided `steps`) wire it themselves: `attachEnrichment({
241
274
  engine, transport, formId, sources })` with sources from
242
275
  `enrichmentSourcesOf(config)`.
243
276
 
244
- ## Scheduler booking (Surface schedulers)
277
+ ## Scheduler booking
278
+
279
+ **Default path — one call, any provider.** `mountScheduler` reads the record's
280
+ scheduler question (provider, event URL/id, prefill links), renders a working
281
+ scheduler, tracks the lifecycle, and persists the booking into the response.
282
+ No `onComplete` wiring, no answer writing, no HTML binding:
283
+
284
+ ```js
285
+ import { mountScheduler } from "@parag.more.withsurface.com/forms-sdk";
286
+
287
+ const teardown = mountScheduler({
288
+ form, // the SurfaceForms.init handle
289
+ container: document.querySelector("#booking"),
290
+ // questionId: "q_booking", // only when the record has >1 scheduler question
291
+ onEvent: (e) => console.log(e.type), // optional; also on form.on("scheduler", …)
292
+ });
293
+ ```
245
294
 
246
- A record step with a Surface scheduler (`CalendlyScreen` with
247
- `calendarType: "Surface"`) renders through the SDK's own unstyled widget do
248
- NOT iframe the hosted booking page, and do NOT bind the question in HTML (the
295
+ Works for every provider native forms support — Surface's own scheduler
296
+ (rendered through the SDK widget, default-styled; `styled: false` keeps it
297
+ bare), Calendly, Cal.com, SavvyCal, Chili Piper, HubSpot, RevenueHero, Zoom,
298
+ Reclaim.ai, Clari — embedding each third-party widget and listening to its
299
+ booking events exactly like native forms do. Lifecycle events
300
+ (`viewed` / `date_selected` / `slot_selected` / `form_opened` /
301
+ `booking_confirmed` / `booking_cancelled`) emit on `form.on("scheduler")` and
302
+ the `onEvent` option; the confirmed booking is written to the response
303
+ automatically (`persist: false` opts out). Caveats: Clari exposes no booking
304
+ signal (embed-only — the visitor advances manually); Calendly/Zoom/Reclaim
305
+ create the response at mount when none exists yet (their embeds carry the
306
+ `responseId` for booking↔response linkage). Full options + provider matrix:
307
+ `docs/reference/binding.md`.
308
+
309
+ **Custom-UI path (advanced).** When you're building your own booking UI for a
310
+ Surface scheduler, mount the SDK's unstyled widget directly — do NOT iframe
311
+ the hosted booking page, and do NOT bind the question in HTML (the
249
312
  `onComplete` below writes the answer):
250
313
 
251
314
  ```js
@@ -269,7 +332,10 @@ The widget walks date → time → name/email form → booked. Labels are
269
332
  human-readable by default (`Intl` in the visitor's locale; every month/date/slot
270
333
  element also carries its raw value in `data-value` — `YYYY-MM`, `YYYY-MM-DD`,
271
334
  ISO instant); pass `formatters: { month?, date?, time? }` to override the copy.
272
- It injects no CSS — style it from your page. The rendered tree:
335
+ The widget itself injects no CSS — style it from your page. (`mountScheduler`'s
336
+ default styling is a stylesheet scoped under `.surface-scheduler` on ITS
337
+ container only; bare `mountBookingWidget` mounts are never restyled.) The
338
+ rendered tree:
273
339
 
274
340
  ```
275
341
  div.surface-booking
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parag.more.withsurface.com/forms-sdk",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Thin-client Surface Forms SDK: headless core over the public runtime endpoints (field-test build).",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,