@picked/client 0.0.70-next.5 → 0.0.70

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 (78) hide show
  1. package/dist/{api-DQ1DqG_F.js → api-CPy9G49C.js} +1 -1
  2. package/dist/api-Ce6xYI--.cjs +1 -0
  3. package/dist/cjs/index.js +2 -2
  4. package/dist/cjs/plugins/dynamic-experiences/index.js +1 -1
  5. package/dist/cjs/server/index.js +1 -1
  6. package/dist/docs/actions.mdx +38 -18
  7. package/dist/docs/api/authentication.md +75 -48
  8. package/dist/docs/api/authorization-and-scopes.md +63 -47
  9. package/dist/docs/api/error-handling.md +70 -54
  10. package/dist/docs/api/index.md +41 -22
  11. package/dist/docs/api/pagination.md +30 -20
  12. package/dist/docs/api-keys.mdx +11 -6
  13. package/dist/docs/chrome-extension.md +22 -5
  14. package/dist/docs/conditions.mdx +32 -19
  15. package/dist/docs/content/asset-library.mdx +65 -34
  16. package/dist/docs/content/content-entries.mdx +8 -2
  17. package/dist/docs/content/layouts.mdx +45 -29
  18. package/dist/docs/content/pages.mdx +29 -14
  19. package/dist/docs/content/sections.mdx +19 -8
  20. package/dist/docs/delivery-api/experience-content.md +10 -5
  21. package/dist/docs/delivery-api/index.md +19 -12
  22. package/dist/docs/delivery-api/preview-caching-crawling.md +27 -14
  23. package/dist/docs/delivery-api/project-config.md +6 -4
  24. package/dist/docs/experiences.mdx +26 -10
  25. package/dist/docs/features.mdx +75 -20
  26. package/dist/docs/fields.mdx +42 -27
  27. package/dist/docs/flow-builder/building-flows.md +38 -20
  28. package/dist/docs/flow-builder/condition-builder.md +32 -21
  29. package/dist/docs/flow-builder/keyboard-shortcuts.md +18 -18
  30. package/dist/docs/flow-builder/nodes.md +26 -13
  31. package/dist/docs/flow-builder/overview.mdx +28 -15
  32. package/dist/docs/gtm-template.md +35 -26
  33. package/dist/docs/guides/build-a-personalized-page.mdx +36 -18
  34. package/dist/docs/image-optimisation-service.md +33 -17
  35. package/dist/docs/index.md +28 -17
  36. package/dist/docs/mcp.md +46 -38
  37. package/dist/docs/pickedql/data-sources.md +65 -43
  38. package/dist/docs/pickedql/dimensions.md +27 -12
  39. package/dist/docs/pickedql/examples.md +38 -26
  40. package/dist/docs/pickedql/filtering.md +10 -6
  41. package/dist/docs/pickedql/index.md +32 -14
  42. package/dist/docs/pickedql/schema.md +85 -76
  43. package/dist/docs/pickedql/virtual-functions.md +62 -36
  44. package/dist/docs/projects.mdx +14 -7
  45. package/dist/docs/reports.mdx +5 -3
  46. package/dist/docs/sdk/actions.md +33 -17
  47. package/dist/docs/sdk/conditions.md +8 -5
  48. package/dist/docs/sdk/content-security-policy.md +5 -4
  49. package/dist/docs/sdk/experiences.md +29 -11
  50. package/dist/docs/sdk/feature-allocation.md +79 -47
  51. package/dist/docs/sdk/features.md +130 -76
  52. package/dist/docs/sdk/fields.md +66 -42
  53. package/dist/docs/sdk/index.md +22 -13
  54. package/dist/docs/sdk/plugins/create-experience.md +18 -15
  55. package/dist/docs/sdk/plugins/dynamic-experiences.md +42 -16
  56. package/dist/docs/sdk/predicates.md +53 -37
  57. package/dist/docs/sdk/ssr/index.md +25 -8
  58. package/dist/docs/sdk/ssr/rendering-and-hydration.md +37 -16
  59. package/dist/docs/sdk/ssr/serialize.md +38 -20
  60. package/dist/docs/sdk/ssr/slots-and-placements.md +28 -10
  61. package/dist/docs/sdk/steps.md +100 -55
  62. package/dist/docs/steps.mdx +20 -11
  63. package/dist/docs/team-management.md +17 -11
  64. package/dist/docs/vite-plugin.md +26 -19
  65. package/dist/docs/workspace-versioning.mdx +14 -8
  66. package/dist/docs/workspaces.mdx +55 -24
  67. package/dist/esm/index.js +257 -238
  68. package/dist/esm/plugins/dynamic-experiences/index.js +2 -2
  69. package/dist/esm/server/index.js +1 -1
  70. package/dist/{plugin-EPx6sE_l.js → plugin-ChPkTj__.js} +58 -53
  71. package/dist/plugin-DHn11gn6.cjs +1 -0
  72. package/dist/types/entities/feature/feature.d.ts.map +1 -1
  73. package/dist/types/plugins/dynamic-experiences/hash.d.ts +40 -5
  74. package/dist/types/plugins/dynamic-experiences/hash.d.ts.map +1 -1
  75. package/dist/types/plugins/dynamic-experiences/plugin.d.ts.map +1 -1
  76. package/package.json +1 -1
  77. package/dist/api-B9koex39.cjs +0 -1
  78. package/dist/plugin-YbSSGrM7.cjs +0 -1
@@ -4,11 +4,17 @@ layout: docs
4
4
 
5
5
  # Server-Side Rendering
6
6
 
7
- Render Picked Pages on your server for SEO, then hydrate them on the client. This page shows the end-to-end shape; the following pages cover each piece.
7
+ Render a Picked Page on your server so that search engines can read it. Then hydrate
8
+ the page on the client. This page shows the whole shape. The pages after it cover
9
+ each part.
8
10
 
9
11
  ## Why server-side rendering
10
12
 
11
- Pages are SEO-critical, so their content must be in the HTML your server returns. Crawlers and social unfurls read that HTML directly and do not wait for client-side JavaScript. Rendering a Page on the server puts its content in the initial response, then the client takes over.
13
+ The search rank of a Page matters, so the HTML that your server returns must hold the
14
+ content of that Page. A crawler and a social preview both read that HTML directly.
15
+ Neither waits for the JavaScript in the browser. A Page that the server renders
16
+ therefore has its content in the first response, and the client takes over after
17
+ that.
12
18
 
13
19
  ## The flow
14
20
 
@@ -20,13 +26,19 @@ projectConfig (Delivery API)
20
26
  ```
21
27
 
22
28
  1. Fetch your published `projectConfig` from the [Delivery API](/delivery-api).
23
- 2. Call `serialize()` on the server. It evaluates which experiences match the URL, fetches their content, and returns a plain-JSON `PickedSnapshot`. See [Server Snapshot](/sdk/ssr/serialize).
29
+ 2. Call `serialize()` on the server. It finds which experiences match the URL, reads
30
+ their content, and returns a `PickedSnapshot` as plain JSON. See
31
+ [Server Snapshot](/sdk/ssr/serialize).
24
32
  3. Embed the snapshot in your HTML payload.
25
- 4. On the client, seed a Picked client from the snapshot so the first paint matches the server markup, then hydrate. See [Rendering & Hydration](/sdk/ssr/rendering-and-hydration).
33
+ 4. On the client, seed a Picked client from the snapshot. The first paint then
34
+ matches the markup from the server. Hydrate after that. See
35
+ [Rendering and Hydration](/sdk/ssr/rendering-and-hydration).
26
36
 
27
- Because the snapshot carries the exact config the server selected with and the memoized condition results, the client's first render reuses the server's decisions instead of re-evaluating them.
37
+ The snapshot carries the exact options that the server chose with, and the condition
38
+ results that it kept. The first render on the client therefore reuses the decisions
39
+ of the server. It does not evaluate them again.
28
40
 
29
- ## React vs Svelte at a glance
41
+ ## React and Svelte side by side
30
42
 
31
43
  :::code-group
32
44
 
@@ -52,11 +64,16 @@ import { PickedProvider, ExperienceSlot } from '@picked/react'
52
64
 
53
65
  ## The server entry point
54
66
 
55
- The server helpers live in a dedicated entry so browser bundles never pull them in:
67
+ The server helpers live in a dedicated entry point, so a browser bundle never holds
68
+ them:
56
69
 
57
70
  ```ts
58
71
  import { serialize, registerProjectConfig } from '@picked/client/server'
59
72
  import type { PickedSnapshot } from '@picked/client/server'
60
73
  ```
61
74
 
62
- Continue to [Server Snapshot](/sdk/ssr/serialize) for the `serialize()` reference, [Rendering & Hydration](/sdk/ssr/rendering-and-hydration) for wiring React and Svelte, and [Slots & Placements](/sdk/ssr/slots-and-placements) for how a slot selects experiences. New to the SDK? Start with [Installation](/sdk).
75
+ Go to [Server Snapshot](/sdk/ssr/serialize) for the `serialize()` reference. Go to
76
+ [Rendering and Hydration](/sdk/ssr/rendering-and-hydration) to connect React and
77
+ Svelte. Go to [Slots and Placements](/sdk/ssr/slots-and-placements) for how a slot
78
+ chooses an experience. If the SDK is new to you, read
79
+ [Installation](/sdk) first.
@@ -2,20 +2,25 @@
2
2
  layout: docs
3
3
  ---
4
4
 
5
- # Rendering & Hydration
5
+ # Rendering and Hydration
6
6
 
7
- Seed a Picked client from the server snapshot, render your slots, and hydrate. React and Svelte below.
7
+ Seed a Picked client from the server snapshot, render your slots, and hydrate. This
8
+ page covers React and Svelte.
8
9
 
9
- ## React: PickedProvider
10
+ ## React: `PickedProvider`
10
11
 
11
- `PickedProvider` supplies a client to descendant components. Its props are a union of two construction modes:
12
+ `PickedProvider` gives a client to each component below it. Its props are a union of
13
+ two modes:
12
14
 
13
15
  | Props | Mode | Description |
14
16
  | --- | --- | --- |
15
- | `{ options, snapshot? }` | Create | The provider calls `createClient(options)` and hydrates it from the snapshot. |
16
- | `{ client, snapshot? }` | Adopt | The provider uses an existing client, such as an app-wide global, and optionally seeds it from the snapshot. |
17
+ | `{ options, snapshot? }` | Create | The provider calls `createClient(options)` and hydrates the client from the snapshot. |
18
+ | `{ client, snapshot? }` | Adopt | The provider uses a client that is there already, such as a global one for the whole application. It can also seed that client from the snapshot. |
17
19
 
18
- The provider seeds synchronously, so the server render and the client's first paint produce the same markup. In the browser it calls `client.start()` from an effect; `start()` is idempotent, so an already-started client is left untouched.
20
+ The provider seeds the client at once, so the server and the first paint on the
21
+ client give the same markup. In the browser the provider calls `client.start()` from
22
+ an effect. `start()` is idempotent, so it changes nothing on a client that already
23
+ started.
19
24
 
20
25
  ```tsx
21
26
  import { PickedProvider, ExperienceSlot } from '@picked/react'
@@ -25,9 +30,11 @@ import { PickedProvider, ExperienceSlot } from '@picked/react'
25
30
  </PickedProvider>
26
31
  ```
27
32
 
28
- ## Svelte: setPickedContext
33
+ ## Svelte: `setPickedContext`
29
34
 
30
- `setPickedContext` is the Svelte-idiomatic equivalent. Call it once at the top of a component's `<script>`. It takes the same two modes, `{ options, snapshot? }` or `{ client, snapshot? }`, and returns the client.
35
+ `setPickedContext` does the same work in the Svelte style. Call it one time at the
36
+ top of the `<script>` block of a component. It takes the same two modes,
37
+ `{ options, snapshot? }` or `{ client, snapshot? }`, and it returns the client.
31
38
 
32
39
  ```svelte
33
40
  <script lang="ts">
@@ -39,13 +46,20 @@ import { PickedProvider, ExperienceSlot } from '@picked/react'
39
46
  <ExperienceSlot name="page" />
40
47
  ```
41
48
 
42
- ## Placing the slot
49
+ ## Place the slot
43
50
 
44
- Drop `<ExperienceSlot name="page" />` where the Page should render. The slot renders every experience that targets it and whose condition is active. See [Slots & Placements](/sdk/ssr/slots-and-placements) for how a slot selects and orders experiences.
51
+ Put `<ExperienceSlot name="page" />` where the Page must render. The slot renders
52
+ every experience that points at it and whose condition is active. See
53
+ [Slots and Placements](/sdk/ssr/slots-and-placements) for how a slot chooses and
54
+ orders your experiences.
45
55
 
46
- ## Rendering the page component
56
+ ## Render the page component
47
57
 
48
- The SDK does not map content to components. You own the component a Page renders and register it under the name the Page uses, which is the same name the dashboard Page Layout maps to. The component reads the resolved entry data through the content hook: `useStepContent()` in React and `getStepContent()` in Svelte.
58
+ The SDK does not connect content to a component. You own the component that a Page
59
+ renders. Register it under the name that the Page uses. The Page Layout in the dashboard
60
+ points at that same name. The component reads the resolved entry data
61
+ through a content hook: `useStepContent()` in React, and `getStepContent()` in
62
+ Svelte.
49
63
 
50
64
  :::code-group
51
65
 
@@ -85,11 +99,15 @@ export function CMSPage() {
85
99
 
86
100
  :::
87
101
 
88
- `useStepContent()` returns the entry's `data` object directly, or `null` while no content is registered. `getStepContent()` returns a store you read with `$content`.
102
+ `useStepContent()` returns the `data` object of the entry directly. It returns `null`
103
+ while no content is registered. `getStepContent()` returns a store, and you read it
104
+ with `$content`.
89
105
 
90
106
  ## Full example
91
107
 
92
- This is the verified page-slot wiring. The page client fetches the project config and renders whichever Page experience targets `.picked-slot-page` and matches the current URL.
108
+ This is the page-slot setup that we tested. The page client reads the project options
109
+ and renders the Page experience that points at `.picked-slot-page` and matches the
110
+ current URL.
93
111
 
94
112
  :::code-group
95
113
 
@@ -138,4 +156,7 @@ export function PageSlot({ snapshot }: { snapshot?: PickedSnapshot }) {
138
156
 
139
157
  :::
140
158
 
141
- For the end-to-end version with the server snapshot, see [Build a Personalized Page](/guides/build-a-personalized-page). For the snapshot reference, see [Server Snapshot](/sdk/ssr/serialize). For how experiences register, see [SDK Experiences](/sdk/experiences).
159
+ For the full version with the server snapshot, see
160
+ [Build a Personalized Page](/guides/build-a-personalized-page). For the snapshot
161
+ reference, see [Server Snapshot](/sdk/ssr/serialize). For how an experience
162
+ registers, see [SDK Experiences](/sdk/experiences).
@@ -4,9 +4,11 @@ layout: docs
4
4
 
5
5
  # Server Snapshot
6
6
 
7
- `serialize()` evaluates which experiences match a URL and fetches their content on the server, returning a plain-JSON snapshot you pass to the client.
7
+ `serialize()` finds which experiences match a URL and reads their content on the
8
+ server. It returns a snapshot as plain JSON, and you pass that snapshot to the
9
+ client.
8
10
 
9
- ## serialize(client, params)
11
+ ## The `serialize(client, params)` function
10
12
 
11
13
  ```ts
12
14
  import { createClient } from '@picked/client'
@@ -20,7 +22,14 @@ const snapshot = await serialize(client, {
20
22
  })
21
23
  ```
22
24
 
23
- `serialize()` registers the project config on the client synchronously, evaluates each experience's condition against `attributes.location`, fetches content for the experiences whose steps reference layout content, dedupes the content blobs by id, and returns a `PickedSnapshot`. It expects a per-request client; concurrent calls on a single client are not supported.
25
+ `serialize()` does this. It registers the project options on the client at once. It
26
+ evaluates the condition of each experience against `attributes.location`. It reads
27
+ the content for each experience whose steps point at layout content. It removes the
28
+ duplicate content blobs, which it finds by their IDs. It then returns a
29
+ `PickedSnapshot`.
30
+
31
+ Give `serialize()` one client for each request. It does not support two calls at the
32
+ same time on one client.
24
33
 
25
34
  ```ts
26
35
  serialize(client: Picked, params: SerializeParams): Promise<PickedSnapshot>
@@ -30,13 +39,15 @@ serialize(client: Picked, params: SerializeParams): Promise<PickedSnapshot>
30
39
 
31
40
  | Parameter | Type | Default | Description |
32
41
  | --- | --- | --- | --- |
33
- | `projectConfig` | `ProjectConfig` | required | The published or preview config to register and select against. |
34
- | `attributes.location` | `Partial<SerializeLocation>` | none | URL fields injected into the selection context, such as `pathname`. |
35
- | `getContent` | `SerializeGetContent` | runtime API | A custom content fetcher used in place of the runtime API. |
36
- | `previewToken` | `string` | none | Forwarded to the content fetch so preview content resolves during preview SSR. |
37
- | `timeoutMs` | `number` | `10000` | Per-content-fetch timeout. On timeout the blob is skipped and the render proceeds. |
42
+ | `projectConfig` | `ProjectConfig` | required | The published options, or the preview options, to register and choose against. |
43
+ | `attributes.location` | `Partial<SerializeLocation>` | none | The URL fields that go into the selection context, such as `pathname`. |
44
+ | `getContent` | `SerializeGetContent` | runtime API | Your own function to read content, in place of the runtime API. |
45
+ | `previewToken` | `string` | none | The SDK sends this with the content request, so that preview content resolves during a preview render. |
46
+ | `timeoutMs` | `number` | `10000` | The limit for each content request. After the limit, the SDK skips that blob and continues. |
38
47
 
39
- `SerializeLocation` carries the plain URL fields `href`, `pathname`, `search`, `hostname`, `origin`, `protocol`, `port`, `host`, and `hash`. Pass only the ones your conditions read.
48
+ `SerializeLocation` carries the URL fields `href`, `pathname`, `search`, `hostname`,
49
+ `origin`, `protocol`, `port`, `host`, and `hash`. Pass only the fields that your
50
+ conditions read.
40
51
 
41
52
  ## The snapshot
42
53
 
@@ -51,16 +62,18 @@ interface PickedSnapshot {
51
62
 
52
63
  | Field | Description |
53
64
  | --- | --- |
54
- | `versionId` | The project version the snapshot was selected against, or `null` when unknown (for example on preview). |
55
- | `projectConfig` | The config the server selected with. It travels so the client renders against the exact same config. |
56
- | `content` | The deduped content blobs for the selected experiences. |
57
- | `conditionResults` | The memoized condition results, keyed by rule hash, so the client reuses the server's selection. |
65
+ | `versionId` | The project version that the snapshot was chosen against. It is `null` when the version is not known, for example in a preview. |
66
+ | `projectConfig` | The options that the server chose with. They travel to the client, so that the client renders against exactly the same options. |
67
+ | `content` | The content blobs for the chosen experiences, with the duplicates removed. |
68
+ | `conditionResults` | The condition results that the server kept, under a key made from the rule hash. The client then reuses the choice of the server. |
58
69
 
59
- The snapshot is plain JSON. Serialize it into your HTML response with `JSON.stringify` and read it back on the client.
70
+ The snapshot is plain JSON. Write it into your HTML response with `JSON.stringify`
71
+ and read it back on the client.
60
72
 
61
- ## Passing the snapshot to the client
73
+ ## Pass the snapshot to the client
62
74
 
63
- Embed the snapshot in your HTML payload, then hand it to the provider (React) or context (Svelte):
75
+ Embed the snapshot in your HTML payload. Then give it to the provider in React, or to
76
+ the context in Svelte:
64
77
 
65
78
  ```ts
66
79
  const html = `
@@ -69,11 +82,14 @@ const html = `
69
82
  `
70
83
  ```
71
84
 
72
- On the client, read `window.__PICKED__` and pass it as the `snapshot` prop. See [Rendering & Hydration](/sdk/ssr/rendering-and-hydration).
85
+ On the client, read `window.__PICKED__` and pass it as the `snapshot` prop. See
86
+ [Rendering and Hydration](/sdk/ssr/rendering-and-hydration).
73
87
 
74
- ## Preview rendering
88
+ ## Render a preview
75
89
 
76
- Pass `previewToken` so unpublished content resolves during preview SSR. The server has no access to the request's query string, so the token cannot be recovered any other way:
90
+ Pass `previewToken`, so that unpublished content resolves during a preview render.
91
+ The server cannot read the query string of the request, so there is no other way to
92
+ give it the token:
77
93
 
78
94
  ```ts
79
95
  const snapshot = await serialize(client, {
@@ -83,4 +99,6 @@ const snapshot = await serialize(client, {
83
99
  })
84
100
  ```
85
101
 
86
- Use the same token when you fetch `projectConfig` from the [Delivery API](/delivery-api), so the config and the content come from the same preview surface.
102
+ Use the same token when you read `projectConfig` from the
103
+ [Delivery API](/delivery-api). The options and the content then come from the same
104
+ preview.
@@ -2,34 +2,52 @@
2
2
  layout: docs
3
3
  ---
4
4
 
5
- # Slots & Placements
5
+ # Slots and Placements
6
6
 
7
- A slot is a named mount point. `<ExperienceSlot name="page" />` renders every experience that targets that slot and whose condition matches.
7
+ A slot is a named mount point. `<ExperienceSlot name="page" />` renders every
8
+ experience that points at that slot and whose condition matches.
8
9
 
9
10
  ## What a slot is
10
11
 
11
- A slot's `name` maps to the `.picked-slot-<name>` selector. `<ExperienceSlot name="page" />` renders every experience whose target selector is `.picked-slot-page` and whose condition is active. The slot reads each experience's active state from the seeded snapshot, so it renders synchronously during server-side rendering and the hydration first paint.
12
+ The `name` of a slot points at the `.picked-slot-<name>` selector. So
13
+ `<ExperienceSlot name="page" />` renders every experience with the target selector
14
+ `.picked-slot-page`, when the condition of that experience is active.
15
+
16
+ The slot reads the active state of each experience from the seeded snapshot. It
17
+ therefore renders at once, both in server-side rendering and at the first paint of
18
+ the hydration.
12
19
 
13
20
  ```tsx
14
21
  <ExperienceSlot name="page" />
15
22
  ```
16
23
 
17
- ## Targeting an experience to a slot
24
+ ## Point an experience at a slot
18
25
 
19
- Set the experience's target selector to `.picked-slot-<name>`. An experience targeting `.picked-slot-page` renders into `<ExperienceSlot name="page" />`. The slot name and the selector suffix must match.
26
+ Set the target selector of the experience to `.picked-slot-<name>`. An experience
27
+ that points at `.picked-slot-page` renders into `<ExperienceSlot name="page" />`. The
28
+ name of the slot and the end of the selector must match.
20
29
 
21
30
  ## Placement order
22
31
 
23
- When several experiences target the same slot, they render in placement precedence:
32
+ When more than one experience points at the same slot, they render in this placement
33
+ order:
24
34
 
25
35
  ```
26
36
  replace -> before -> prepend -> append -> after
27
37
  ```
28
38
 
29
- `replace` wins. If an active experience uses the `replace` placement, it renders alone and replaces every other match in the slot. Otherwise every active experience renders, in placement order.
39
+ `replace` wins. An active experience with the `replace` placement renders alone, and
40
+ it replaces every other match in the slot. In every other case each active experience
41
+ renders, in placement order.
30
42
 
31
- ## Multiple experiences in one slot
43
+ ## More than one experience in one slot
32
44
 
33
- The slot collects each experience that targets its selector, drops any whose condition is not active, and sorts the rest by placement. If a `replace` experience is among the active ones, only that experience renders. With no active `replace`, the slot renders all active experiences in `before, prepend, append, after` order.
45
+ The slot collects each experience that points at its selector. It drops each one
46
+ whose condition is not active. It then sorts the rest into placement order. When a `replace`
47
+ experience is among the active ones, only that experience renders. With no active
48
+ `replace`, the slot renders every active experience in the order `before`, `prepend`,
49
+ `append`, `after`.
34
50
 
35
- This lets one Page take over a slot with `replace`, while other slots can stack several experiences by using the non-`replace` placements. Conditions decide which experiences are active; see [Conditions](/conditions).
51
+ One Page can therefore take a whole slot with `replace`, while another slot holds
52
+ several experiences that use the other placements. A condition decides which
53
+ experiences are active. See [Conditions](/conditions).
@@ -4,23 +4,33 @@ layout: docs
4
4
 
5
5
  # Steps
6
6
 
7
- Render steps with `<StepRouter>` and `<Step>` from `@picked/react` or `@picked/svelte`, and access the active step through `useStep()` / `getStep()`.
7
+ Render your steps with `<StepRouter>` and `<Step>` from `@picked/react` or
8
+ `@picked/svelte`. Reach the active step through `useStep()` or `getStep()`.
8
9
 
9
10
  ## How steps work
10
11
 
11
- When an experience starts, its steps are connected in a directed node graph. The SDK traverses this graph to determine which step to display:
12
-
13
- 1. **Initialization**: When the experience opens, the SDK starts at the origin node and traverses the graph to find the first step
14
- 2. **Step Entry**: The active step is set and a `step_enter` event is emitted on the event bus
15
- 3. **Field Evaluation**: The step's fields are evaluated — fields with conditions are shown or hidden based on the current state
16
- 4. **User Input**: The user interacts with the step's fields. Client-side validation runs on blur and on submit
17
- 5. **Step Submission**: When the user submits, a `step_submit` event fires and field responses are committed to the client state
18
- 6. **Navigation**: The SDK follows the edge from the current step node to the next, traversing any conditionals or actions before arriving at the next step
19
- 7. **Completion**: When no further steps exist, the experience flow is complete
12
+ When an experience starts, its steps form a directed graph of nodes. The SDK walks
13
+ this graph to find which step to show:
14
+
15
+ 1. **Initialization**: when the experience opens, the SDK starts at the origin node.
16
+ It walks the graph to find the first step.
17
+ 2. **Step entry**: the SDK sets the active step. It sends a `step_enter` event on the
18
+ event bus.
19
+ 3. **Field evaluation**: the SDK evaluates the fields of the step. It shows or hides
20
+ a field with a condition, and the current state decides which.
21
+ 4. **User input**: the user fills in the fields of the step. Validation runs in the
22
+ browser on blur and on submit.
23
+ 5. **Step submission**: when the user submits, a `step_submit` event fires. The SDK
24
+ commits the field responses to the client state.
25
+ 6. **Navigation**: the SDK follows the edge from the current step node to the next
26
+ one. It walks through each condition and action on the way.
27
+ 7. **Completion**: when no step remains, the experience flow is complete.
20
28
 
21
29
  ## Step routing
22
30
 
23
- `StepRouter` listens to the experience's current step state and activates the matching `Step` by its `id`. Each `Step` registers itself on mount and unregisters on unmount, so lazy-rendered tabs or drawers work automatically.
31
+ `StepRouter` watches the current step state of the experience. It activates the
32
+ `Step` whose `id` matches. Each `Step` registers itself on mount and unregisters on
33
+ unmount, so a tab or a drawer that renders lazily works with no extra code.
24
34
 
25
35
  :::code-group
26
36
 
@@ -69,12 +79,12 @@ function MyExperience() {
69
79
  :::
70
80
 
71
81
  :::note
72
- All Steps must be wrapped in a router, even if it is a single Step
82
+ A router must hold every Step, even when there is only one Step.
73
83
  :::
74
84
 
75
85
  ### Lazy-loaded steps
76
86
 
77
- For large experiences, steps can be lazily loaded using dynamic imports:
87
+ For a large experience, load a step lazily with a dynamic import:
78
88
 
79
89
  :::code-group
80
90
 
@@ -98,11 +108,13 @@ const WelcomeScreen = lazy(() => import('./WelcomeScreen'))
98
108
 
99
109
  :::
100
110
 
101
- The `component` prop accepts a `Promise<Component>` — the step's content is not loaded until it becomes active.
111
+ The `component` prop takes a `Promise<Component>`. The SDK does not load the content
112
+ of the step until that step becomes active.
102
113
 
103
- ## Accessing step context
114
+ ## Reach the step context
104
115
 
105
- Inside a `Step` component (or any component rendered within one), call `getStep()` / `useStep()` to access the step's reactive context:
116
+ Inside a `Step` component, or inside any component that a `Step` renders, call
117
+ `getStep()` or `useStep()` to reach the reactive context of the step:
106
118
 
107
119
  :::code-group
108
120
 
@@ -161,24 +173,29 @@ function ContactForm() {
161
173
 
162
174
  :::
163
175
 
164
- `getStep()` (Svelte) / `useStep()` (React) returns a `StepContext` with the following properties:
176
+ `getStep()` in Svelte, and `useStep()` in React, return a `StepContext` with these
177
+ properties:
165
178
 
166
179
  | Property | Type | Description |
167
180
  | ---------- | --------------------------------------------- | ------------------------------------------------------------------------------ |
168
- | `step` | `Store<ExperienceStep>` | Reactive store containing the step's `id` and `label` |
169
- | `fields` | `Store<FormField[]>` | Reactive store of fields with their current values, errors, and event handlers |
170
- | `isActive` | `Store<boolean>` (Svelte) / `boolean` (React) | Whether this step is currently active in the router |
171
- | `form` | Object | Form submission handlers (see [Forms](#forms)) |
181
+ | `step` | `Store<ExperienceStep>` | A reactive store that holds the `id` and the `label` of the step |
182
+ | `fields` | `Store<FormField[]>` | A reactive store of the fields, with their current values, errors, and handlers |
183
+ | `isActive` | `Store<boolean>` (Svelte) / `boolean` (React) | Whether the router holds this step as the active one |
184
+ | `form` | Object | The handlers for a form submission. See [Forms](#forms). |
172
185
 
173
186
  ## Forms
174
187
 
175
- When a step has fields, `getStep()` / `useStep()` provides a `form` object for handling submission, validation, and navigation.
188
+ When a step has fields, `getStep()` and `useStep()` give you a `form` object. Use it
189
+ for the submission, the validation, and the move to the next step.
176
190
 
177
- Both accept an optional callbacks object — `getStep({ onSuccess })` / `useStep({ onSuccess })` — which is the recommended way to react to submission. The available slots are `onBeforeSubmit`, `onSuccess` and `onError`, all covered below.
191
+ Both take an optional object of callbacks, as in `getStep({ onSuccess })` or
192
+ `useStep({ onSuccess })`. This is the recommended way to act on a submission. The
193
+ slots are `onBeforeSubmit`, `onSuccess`, and `onError`. This page covers each one
194
+ below.
178
195
 
179
- ### Handling submission
196
+ ### Handle a submission
180
197
 
181
- Bind `form.props` to your `<form>` element's `onsubmit` handler:
198
+ Bind `form.props` to the `onsubmit` handler of your `<form>` element:
182
199
 
183
200
  :::code-group
184
201
 
@@ -242,19 +259,22 @@ function StepForm() {
242
259
 
243
260
  :::
244
261
 
245
- Submitting the form:
262
+ When you submit the form, the SDK does this:
246
263
 
247
- 1. Collects field values from the HTML form
248
- 2. Runs `onBeforeSubmit` transform callbacks in series — a throw here aborts the submit before anything is committed
249
- 3. Validates all required fields and field validation rules
250
- 4. Commits the responses to the experience state
251
- 5. Fires `form_submit_success` or `form_submit_error` on the event bus
252
- 6. Runs your `onSuccess` or `onError` callbacks
253
- 7. Navigates to the next step (unless `stopNavigation` is called, and regardless of whether a callback in step 6 failed)
264
+ 1. It collects the field values from the HTML form.
265
+ 2. It runs each `onBeforeSubmit` callback, one after another. A throw here aborts the
266
+ submit and commits nothing.
267
+ 3. It checks each required field and each validation rule.
268
+ 4. It commits the responses to the experience state.
269
+ 5. It fires `form_submit_success` or `form_submit_error` on the event bus.
270
+ 6. It runs your `onSuccess` or `onError` callback.
271
+ 7. It moves to the next step. A call to `stopNavigation` stops this. A callback that
272
+ failed in step 6 does not stop it.
254
273
 
255
- ### Transforming data before submission
274
+ ### Transform data before a submission
256
275
 
257
- Use `onBeforeSubmit` to intercept and transform field data before it is committed. Callbacks run in series, each receiving the output of the previous:
276
+ Use `onBeforeSubmit` to intercept and transform the field data before the SDK stores it. The callbacks run one after another. Each one takes the output of the one before
277
+ it:
258
278
 
259
279
  :::code-group
260
280
 
@@ -264,7 +284,7 @@ Use `onBeforeSubmit` to intercept and transform field data before it is committe
264
284
 
265
285
  const { form } = getStep({
266
286
  onBeforeSubmit: async (data) => {
267
- // Normalize email to lowercase before committing
287
+ // Normalize the email address to lowercase first.
268
288
  return {
269
289
  ...data,
270
290
  'email-address': String(data['email-address']).toLowerCase(),
@@ -282,7 +302,7 @@ import { useStep } from '@picked/react/hooks'
282
302
  function ContactStep() {
283
303
  const { form } = useStep({
284
304
  onBeforeSubmit: async (data) => {
285
- // Normalize email to lowercase before committing
305
+ // Normalize the email address to lowercase first.
286
306
  return {
287
307
  ...data,
288
308
  'email-address': String(data['email-address']).toLowerCase(),
@@ -297,12 +317,16 @@ function ContactStep() {
297
317
  :::
298
318
 
299
319
  :::warning
300
- If an `onBeforeSubmit` callback throws, the submit is aborted before anything is committed and the step does not advance. This is deliberate — committing a half-transformed payload would persist wrong-but-plausible data. Use it for transforms and validation, and put side effects such as analytics or backend calls in `onSuccess` instead.
320
+ When an `onBeforeSubmit` callback throws, the SDK aborts the submit before it commits
321
+ anything, and the step does not advance. This is deliberate. A payload that is only
322
+ part transformed would store data that looks correct but is wrong. Use
323
+ `onBeforeSubmit` for a transform and for a validation check. Put a side effect, such as an
324
+ analytics call or a backend call, in `onSuccess`.
301
325
  :::
302
326
 
303
327
  ### Success and error callbacks
304
328
 
305
- Pass `onSuccess` and `onError` to react to submission outcomes:
329
+ Pass `onSuccess` and `onError` to act on the outcome of a submission:
306
330
 
307
331
  :::code-group
308
332
 
@@ -342,13 +366,20 @@ function ContactStep() {
342
366
 
343
367
  :::
344
368
 
345
- Callbacks are scoped to the component that passes them, and are cleaned up automatically when that component unmounts — no manual teardown is needed. Passing a new inline function on a re-render replaces the previous one rather than adding another, so a single submit runs each callback exactly once.
369
+ A callback belongs to the component that passes it. The SDK releases the callback
370
+ when that component unmounts, so you write no teardown code. A new inline function on
371
+ a re-render replaces the earlier one and does not add a second one. One submit
372
+ therefore runs each callback exactly one time.
346
373
 
347
- A throwing or rejecting `onSuccess`/`onError` callback cannot block the flow: the step still advances, and the failure is reported on the client event bus as `form_submit_callback_error` so it stays observable.
374
+ An `onSuccess` or `onError` callback that throws or rejects cannot stop the flow. The
375
+ step still advances, and the SDK reports the failure on the client event bus as
376
+ `form_submit_callback_error`, so the failure stays observable.
348
377
 
349
- #### Registering callbacks imperatively
378
+ #### Register a callback in code
350
379
 
351
- `form.onSuccess`, `form.onError` and `form.onBeforeSubmit` are also available as an escape hatch, for cases where a callback has to be attached conditionally. They return nothing — cleanup is automatic, as above.
380
+ `form.onSuccess`, `form.onError`, and `form.onBeforeSubmit` are an escape hatch, for
381
+ a callback that you must attach only when a condition holds. They return nothing. The
382
+ SDK releases them for you, as above.
352
383
 
353
384
  ```tsx [React]
354
385
  function ContactStep({ trackConversions }: { trackConversions: boolean }) {
@@ -363,12 +394,17 @@ function ContactStep({ trackConversions }: { trackConversions: boolean }) {
363
394
  ```
364
395
 
365
396
  :::warning
366
- Register from the component body, not from an event handler. A registration made inside a click handler is added each time the handler runs and is only released when the component unmounts, so it accumulates. Prefer the options object above wherever you can.
397
+ Register from the body of the component, and not from an event handler. A
398
+ registration inside a click handler is added each time the handler runs, and the SDK
399
+ releases it only when the component unmounts. The registrations therefore accumulate.
400
+ Use the options object above where you can.
367
401
  :::
368
402
 
369
- ### Preventing navigation on success
403
+ ### Stop the move to the next step
370
404
 
371
- By default, the experience advances to the next step after a successful submission. Call `stopNavigation()` inside `onSuccess` to prevent this, for example when you need to handle navigation manually:
405
+ By default the experience advances to the next step after a successful submission.
406
+ Call `stopNavigation()` inside `onSuccess` to stop this. Do it when you must choose
407
+ the next step yourself:
372
408
 
373
409
  :::code-group
374
410
 
@@ -383,7 +419,7 @@ By default, the experience advances to the next step after a successful submissi
383
419
  onSuccess: ({ data, stopNavigation }) => {
384
420
  stopNavigation()
385
421
 
386
- // Navigate to step based on submitted data
422
+ // Choose the next step from the data that the user sent.
387
423
  if (data['plan-type'] === 'enterprise') {
388
424
  experience.goToStepById('enterprise-contact')
389
425
  } else {
@@ -407,7 +443,7 @@ function PlanStep() {
407
443
  onSuccess: ({ data, stopNavigation }) => {
408
444
  stopNavigation()
409
445
 
410
- // Decide the next step based on submitted data
446
+ // Choose the next step from the data that the user sent.
411
447
  if (data['plan-type'] === 'enterprise') {
412
448
  experience.goToStepById('enterprise-contact')
413
449
  } else {
@@ -422,13 +458,22 @@ function PlanStep() {
422
458
 
423
459
  :::
424
460
 
425
- Navigate by step `id` with `goToStepById('<step-id>')`, or by the step's semantic `key` with `goToStepByKey('<step-key>')`. Both jump straight to the matching step node; if no step matches, the call is a no-op.
461
+ Move by step `id` with `goToStepById('<step-id>')`, or by the `key` of the step with
462
+ `goToStepByKey('<step-key>')`. Both jump directly to the step node that matches. When
463
+ no step matches, the call does nothing.
426
464
 
427
465
  ## Best practices
428
466
 
429
- - **Give steps meaningful labels**: The `label` is used in analytics events and the dashboard — prefer descriptive names like `"Contact Details"` over `"Step 2"`
430
- - **Keep steps focused**: Each step should collect a small, related set of information. Prefer more steps over longer forms to reduce abandonment
431
- - **Use conditionals for branching**: Avoid duplicating steps — model branching logic with `nextConditional()` so shared steps can be reused
432
- - **Use `stopNavigation` sparingly**: Automatic navigation after submit is the expected behavior. Only disable it when you need explicit control over the next destination
433
- - **Validate eagerly on blur, not on change**: The SDK follows this pattern automatically — fields show errors after the user leaves them, not as they type
434
- - **Lazy-load large step components**: Use the `component` prop on `<Step>` with a dynamic import for content that is only shown to a subset of users
467
+ - **Give each step a meaningful label**: analytics events and the dashboard both show
468
+ the `label`. Use a name that says what the step does, such as `"Contact Details"`
469
+ and not `"Step 2"`.
470
+ - **Keep each step small**: each step must collect one small set of related
471
+ information. More steps with fewer fields each reduce the number of users who leave.
472
+ - **Use a condition to take a branch**: do not make a second copy of a step. Write the
473
+ branch with `nextConditional()`, so that you can reuse a shared step.
474
+ - **Use `stopNavigation` only when you must**: an automatic move after a submit is
475
+ what a user expects. Stop it only when you must choose the next step yourself.
476
+ - **Check a field on blur, not on change**: the SDK does this for you. A field shows
477
+ an error after the user leaves it, and not while the user types.
478
+ - **Load a large step component lazily**: use the `component` prop on `<Step>` with a
479
+ dynamic import, for content that only some users see.