@habit.analytics/habit-claims-journey-components 1.1.13 → 2.1.0

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 (129) hide show
  1. package/README.md +646 -151
  2. package/dist/components/NavLink.d.ts +8 -0
  3. package/dist/components/form-configurator/CreateFieldDialog.d.ts +10 -0
  4. package/dist/components/form-configurator/FieldPropertiesDialog.d.ts +7 -0
  5. package/dist/components/form-configurator/FormConfigurator.d.ts +11 -0
  6. package/dist/components/form-configurator/JourneyBuilderWidget.d.ts +21 -0
  7. package/dist/components/form-configurator/fields/FieldsTab.d.ts +7 -0
  8. package/dist/components/form-configurator/init/InitTab.d.ts +3 -0
  9. package/dist/components/form-configurator/json/JsonTab.d.ts +3 -0
  10. package/dist/components/form-configurator/steps/StepEditor.d.ts +7 -0
  11. package/dist/components/form-configurator/steps/StepNavigationEditor.d.ts +7 -0
  12. package/dist/components/form-configurator/steps/StepsList.d.ts +3 -0
  13. package/dist/components/form-configurator/summary/SummaryPanelEditor.d.ts +3 -0
  14. package/dist/components/journey-builder/ConditionalEdge.d.ts +10 -0
  15. package/dist/components/journey-builder/CreateNodeDialog.d.ts +18 -0
  16. package/dist/components/journey-builder/EdgePropertiesEditor.d.ts +1 -0
  17. package/dist/components/journey-builder/FormPreviewPanel.d.ts +3 -0
  18. package/dist/components/journey-builder/GraphValidationPanel.d.ts +1 -0
  19. package/dist/components/journey-builder/GroupNode.d.ts +4 -0
  20. package/dist/components/journey-builder/ImportJourneyDialog.d.ts +1 -0
  21. package/dist/components/journey-builder/JourneyBuilder.d.ts +12 -0
  22. package/dist/components/journey-builder/JourneyBuilderWidget.d.ts +81 -0
  23. package/dist/components/journey-builder/JourneyCanvas.d.ts +4 -0
  24. package/dist/components/journey-builder/KeyboardShortcutsHelp.d.ts +6 -0
  25. package/dist/components/journey-builder/NodePropertiesEditor.d.ts +1 -0
  26. package/dist/components/journey-builder/NodeSearchPalette.d.ts +6 -0
  27. package/dist/components/journey-builder/PortalContext.d.ts +7 -0
  28. package/dist/components/journey-builder/PropertiesPanel.d.ts +1 -0
  29. package/dist/components/journey-builder/QuestionNode.d.ts +11 -0
  30. package/dist/components/journey-builder/SpecsCatalog.d.ts +17 -0
  31. package/dist/components/journey-builder/SpecsContext.d.ts +3 -0
  32. package/dist/components/journey-builder/ThemePanel.d.ts +1 -0
  33. package/dist/components/journey-builder/edge-editor/ConditionRow.d.ts +18 -0
  34. package/dist/components/journey-builder/edge-editor/ConditionTemplates.d.ts +6 -0
  35. package/dist/components/journey-builder/edge-editor/ConditionTestPanel.d.ts +9 -0
  36. package/dist/components/journey-builder/edge-editor/VisualConditionEditor.d.ts +15 -0
  37. package/dist/components/journey-builder/edge-editor/condition-utils.d.ts +24 -0
  38. package/dist/components/journey-builder/preview-widgets/BooleanWidget.d.ts +13 -0
  39. package/dist/components/journey-builder/preview-widgets/CurrencyWidget.d.ts +9 -0
  40. package/dist/components/journey-builder/preview-widgets/DateTimeWidget.d.ts +10 -0
  41. package/dist/components/journey-builder/preview-widgets/DateWidget.d.ts +10 -0
  42. package/dist/components/journey-builder/preview-widgets/EmailWidget.d.ts +10 -0
  43. package/dist/components/journey-builder/preview-widgets/LocationWidget.d.ts +9 -0
  44. package/dist/components/journey-builder/preview-widgets/PhoneWidget.d.ts +10 -0
  45. package/dist/components/journey-builder/preview-widgets/SelectWidget.d.ts +12 -0
  46. package/dist/components/journey-builder/preview-widgets/TextWidget.d.ts +10 -0
  47. package/dist/components/journey-builder/preview-widgets/TimeWidget.d.ts +14 -0
  48. package/dist/components/journey-builder/preview-widgets/UploadWidget.d.ts +12 -0
  49. package/dist/components/journey-builder/preview-widgets/WidgetRenderer.d.ts +12 -0
  50. package/dist/components/runner/AnswerHistoryList.d.ts +11 -0
  51. package/dist/components/runner/FormRunnerWidget.d.ts +41 -0
  52. package/dist/components/runner/GroupedFormScreen.d.ts +14 -0
  53. package/dist/components/ui/accordion.d.ts +7 -0
  54. package/dist/components/ui/alert-dialog.d.ts +20 -0
  55. package/dist/components/ui/alert.d.ts +8 -0
  56. package/dist/components/ui/aspect-ratio.d.ts +3 -0
  57. package/dist/components/ui/avatar.d.ts +6 -0
  58. package/dist/components/ui/badge.d.ts +9 -0
  59. package/dist/components/ui/breadcrumb.d.ts +19 -0
  60. package/dist/components/ui/button.d.ts +11 -0
  61. package/dist/components/ui/calendar.d.ts +8 -0
  62. package/dist/components/ui/card.d.ts +8 -0
  63. package/dist/components/ui/carousel.d.ts +18 -0
  64. package/dist/components/ui/chart.d.ts +62 -0
  65. package/dist/components/ui/checkbox.d.ts +4 -0
  66. package/dist/components/ui/collapsible.d.ts +5 -0
  67. package/dist/components/ui/command.d.ts +82 -0
  68. package/dist/components/ui/context-menu.d.ts +27 -0
  69. package/dist/components/ui/dialog.d.ts +19 -0
  70. package/dist/components/ui/drawer.d.ts +22 -0
  71. package/dist/components/ui/dropdown-menu.d.ts +27 -0
  72. package/dist/components/ui/form.d.ts +23 -0
  73. package/dist/components/ui/hover-card.d.ts +6 -0
  74. package/dist/components/ui/input-otp.d.ts +34 -0
  75. package/dist/components/ui/input.d.ts +3 -0
  76. package/dist/components/ui/label.d.ts +5 -0
  77. package/dist/components/ui/menubar.d.ts +33 -0
  78. package/dist/components/ui/navigation-menu.d.ts +12 -0
  79. package/dist/components/ui/pagination.d.ts +28 -0
  80. package/dist/components/ui/popover.d.ts +6 -0
  81. package/dist/components/ui/progress.d.ts +4 -0
  82. package/dist/components/ui/radio-group.d.ts +5 -0
  83. package/dist/components/ui/resizable.d.ts +23 -0
  84. package/dist/components/ui/scroll-area.d.ts +5 -0
  85. package/dist/components/ui/select.d.ts +13 -0
  86. package/dist/components/ui/separator.d.ts +4 -0
  87. package/dist/components/ui/sheet.d.ts +25 -0
  88. package/dist/components/ui/sidebar.d.ts +66 -0
  89. package/dist/components/ui/skeleton.d.ts +2 -0
  90. package/dist/components/ui/slider.d.ts +4 -0
  91. package/dist/components/ui/sonner.d.ts +4 -0
  92. package/dist/components/ui/switch.d.ts +4 -0
  93. package/dist/components/ui/table.d.ts +10 -0
  94. package/dist/components/ui/tabs.d.ts +7 -0
  95. package/dist/components/ui/textarea.d.ts +5 -0
  96. package/dist/components/ui/toast.d.ts +15 -0
  97. package/dist/components/ui/toaster.d.ts +1 -0
  98. package/dist/components/ui/toggle-group.d.ts +12 -0
  99. package/dist/components/ui/toggle.d.ts +12 -0
  100. package/dist/components/ui/tooltip.d.ts +7 -0
  101. package/dist/components/ui/use-toast.d.ts +2 -0
  102. package/dist/data/country-codes.d.ts +7 -0
  103. package/dist/hooks/use-mobile.d.ts +1 -0
  104. package/dist/hooks/use-toast.d.ts +44 -0
  105. package/dist/hooks/useKeyboardShortcuts.d.ts +20 -0
  106. package/dist/hooks/useThemePortalVars.d.ts +2 -0
  107. package/dist/i18n/index.d.ts +2 -0
  108. package/dist/i18n/locales/en.d.ts +165 -0
  109. package/dist/i18n/locales/es.d.ts +3 -0
  110. package/dist/i18n/locales/pl.d.ts +3 -0
  111. package/dist/i18n/locales/pt.d.ts +3 -0
  112. package/dist/i18n/useTranslation.d.ts +19 -0
  113. package/dist/index.cjs +63 -129
  114. package/dist/index.d.ts +10 -0
  115. package/dist/index.mjs +22574 -28441
  116. package/dist/lib/format-answer.d.ts +23 -0
  117. package/dist/lib/graph-validation.d.ts +15 -0
  118. package/dist/lib/theme-css.d.ts +5 -0
  119. package/dist/lib/utils.d.ts +2 -0
  120. package/dist/schemas/form-config.schema.json.d.ts +220 -0
  121. package/dist/schemas/index.d.ts +3 -0
  122. package/dist/schemas/journey-spec.schema.json.d.ts +49 -0
  123. package/dist/stores/formConfiguratorStore.d.ts +81 -0
  124. package/dist/stores/journeyBuilderStore.d.ts +99 -0
  125. package/dist/types/journey.d.ts +530 -0
  126. package/package.json +6 -7
  127. package/src/index.ts +0 -31
  128. package/src/types/journey.ts +0 -273
  129. package/src/types/json-logic-js.d.ts +0 -5
package/README.md CHANGED
@@ -1,33 +1,36 @@
1
1
  # Journey Builder & Form Runner
2
2
 
3
- A visual graph-based form builder and step-by-step form runner, built as two independent, embeddable React widgets.
3
+ A linear, step-based form configurator and a self-contained form runner, distributed as two embeddable React widgets.
4
4
 
5
- **Builder** — drag-and-drop field specs onto a canvas, connect them with conditional edges, and export a portable `JourneySpec` JSON.
5
+ - **Builder (`JourneyBuilderWidget`)** — configure a journey as ordered **Steps → Rows → Fields**, define per-step **navigation rules** (JSON Logic) and optional **API calls**, plus a **Summary** panel and **Init** values. Outputs a portable `JourneySpec` (v2.0).
6
+ - **Runner (`FormRunnerWidget`)** — feeds that `JourneySpec` to a step-by-step engine that evaluates conditions, optionally calls APIs between steps (delegated to the host), collects answers, and renders a review/summary screen.
6
7
 
7
- **Runner**feed that JSON into a self-contained form engine that navigates users through the journey, evaluates branching conditions, and collects answers.
8
+ > **v2.0breaking change vs v1.x.** The visual graph canvas (React Flow / `@xyflow/react`) has been removed. Legacy v1.x journeys (`nodes`/`edges`) are still accepted and auto-converted on import for read-only consumption, but new journeys are authored against the **`form_config`** model described below.
8
9
 
9
10
  ---
10
11
 
11
12
  ## Architecture
12
13
 
13
14
  ```
14
- ┌─────────────────────────────────────────────────────┐
15
- Host Application
16
-
17
- ┌──────────────────────┐ ┌──────────────────────┐
18
- │ │ JourneyBuilderWidget FormRunnerWidget │
19
- │ │
20
- │ │ specs ──► Catalog │ journey ──► Engine
21
- │ │ Canvas │ Widgets │ │
22
- │ │ Props │ History
23
- │ │ Summary │ │
24
- │ │ onSave(journey) ◄── onSubmit(answers) ◄─
25
- │ │ onChange(journey) ◄─ │ │ │ │
26
- └──────────────────────┘ └──────────────────────┘
27
-
28
- Both consume JourneySpec + types from
29
- src/types/journey.ts
30
- └─────────────────────────────────────────────────────┘
15
+ ┌─────────────────────────────────────────────────────────────┐
16
+ Host Application
17
+
18
+ ┌──────────────────────────┐ ┌────────────────────────┐
19
+ │ │ JourneyBuilderWidget FormRunnerWidget │
20
+ │ │
21
+ │ │ specs ──► Catalog │ journey ──► Engine
22
+ │ │ journey ──► Editor │ │ initialAnswers ──►
23
+ │ │ Steps/Rows │ │ prefill
24
+ │ │ Fields tab │ │ onStepApiCall ◄────
25
+ │ │ Init / JSON onStepApiError ◄────
26
+ │ │ Summary │ │ onFileSelect ◄──── │ │
27
+ onSave / onPublish ──► │ onSubmit(answers) ──► │ │
28
+ onChange ──► │ │ onSave (draft) ──► │ │
29
+ └──────────────────────────┘ └────────────────────────┘
30
+
31
+ │ Both consume `JourneySpec` + types from │
32
+ │ `src/types/journey.ts` │
33
+ └─────────────────────────────────────────────────────────────┘
31
34
  ```
32
35
 
33
36
  ---
@@ -37,69 +40,86 @@ A visual graph-based form builder and step-by-step form runner, built as two ind
37
40
  | Layer | Technology |
38
41
  |---|---|
39
42
  | UI framework | React 18 + TypeScript |
40
- | Graph canvas | `@xyflow/react` (React Flow v12) |
41
- | State management | Zustand (scoped per widget instance) |
42
- | Edge conditions | `json-logic-js` |
43
- | Styling | Tailwind CSS + shadcn/ui |
43
+ | State management | Zustand (scoped per widget instance, 50-step undo/redo, dirty tracking) |
44
+ | Condition engine | `json-logic-js` |
45
+ | Styling | Tailwind CSS (prefixed `cj-`) + scoped CSS variables (`--claims-journey-*`) |
44
46
  | Build tool | Vite |
45
47
 
48
+ Peer deps: `react`, `react-dom`. The previous `@xyflow/react` peer dep has been **removed** in v2.0.
49
+
46
50
  ---
47
51
 
48
52
  ## Quick Start
49
53
 
50
54
  ```bash
51
- # Clone and install
52
- git clone <YOUR_GIT_URL>
53
- cd journey-builder
54
55
  npm install
55
-
56
- # Start dev server
57
56
  npm run dev
58
57
  ```
59
58
 
60
- The demo app runs at `http://localhost:5173`:
61
- - `/` — Builder with sample property specs
62
- - `/runner` — Runner loaded from the builder's current graph
59
+ Demo app at `http://localhost:5173`:
60
+ - `/` — Builder with sample specs
61
+ - `/runner` — Runner loaded from the builder's current journey
63
62
 
64
63
  ---
65
64
 
66
65
  ## Installation (as a library)
67
66
 
68
67
  ```bash
69
- npm install journey-builder
68
+ npm install habit-claims-journey-components
70
69
  ```
71
70
 
72
- ```typescript
71
+ ```tsx
73
72
  import {
74
73
  JourneyBuilderWidget,
75
74
  FormRunnerWidget,
76
- } from 'journey-builder';
75
+ } from 'habit-claims-journey-components';
77
76
 
78
77
  import type {
79
78
  JourneySpec,
80
79
  ClaimPropertySpec,
81
- } from 'journey-builder';
80
+ FormConfig,
81
+ EnrichedAnswer,
82
+ StepApiRequest,
83
+ InitApiRequest,
84
+ } from 'habit-claims-journey-components';
82
85
  ```
83
86
 
87
+ CSS is auto-injected — **do not import a separate `style.css`**.
88
+
89
+ The parent of `JourneyBuilderWidget` must have an explicit height (the widget fills `100%`).
90
+
91
+ See [INTEGRATION.md](./INTEGRATION.md) for full install methods and copy-paste setup.
92
+
84
93
  ---
85
94
 
86
95
  ## Usage
87
96
 
88
97
  ### JourneyBuilderWidget
89
98
 
90
- The visual graph editor. Provide field specifications and receive the journey graph via callbacks.
91
-
92
99
  ```tsx
93
- import { JourneyBuilderWidget } from 'journey-builder';
94
-
95
- function BuilderPage({ specs, existingJourney }) {
100
+ import { JourneyBuilderWidget } from 'habit-claims-journey-components';
101
+ import type { ClaimPropertySpec, JourneySpec } from 'habit-claims-journey-components';
102
+
103
+ function BuilderPage({
104
+ specs,
105
+ existing,
106
+ }: {
107
+ specs: ClaimPropertySpec[];
108
+ existing?: JourneySpec | null;
109
+ }) {
96
110
  return (
97
- <JourneyBuilderWidget
98
- specs={specs}
99
- journey={existingJourney}
100
- onSave={(journey) => saveToApi(journey)}
101
- onChange={(journey) => console.log('Graph changed', journey)}
102
- />
111
+ <div style={{ width: '100%', height: '100vh' }}>
112
+ <JourneyBuilderWidget
113
+ specs={specs}
114
+ journey={existing}
115
+ onSave={(spec) => saveDraft(spec)}
116
+ onPublish={(spec) => publish(spec)}
117
+ onChange={(spec) => console.log('changed', spec)}
118
+ onOpenRunner={() => navigate('/runner')}
119
+ readOnly={false}
120
+ googleMapsApiKey={import.meta.env.VITE_GOOGLE_MAPS_KEY}
121
+ />
122
+ </div>
103
123
  );
104
124
  }
105
125
  ```
@@ -108,25 +128,48 @@ function BuilderPage({ specs, existingJourney }) {
108
128
 
109
129
  | Prop | Type | Required | Description |
110
130
  |---|---|---|---|
111
- | `specs` | `ClaimPropertySpec[]` | | Field specifications shown in the drag-and-drop catalog |
112
- | `journey` | `JourneySpec \| null` | — | Existing journey to load on mount |
113
- | `onSave` | `(spec: JourneySpec) => void` | — | Called when the user clicks Export/Save |
114
- | `onChange` | `(spec: JourneySpec) => void` | — | Called on every graph mutation (node/edge add, move, delete, edit) |
131
+ | `specs` | `ClaimPropertySpec[]` | | Field specifications shown in the catalog and "Add field" dialogs |
132
+ | `journey` | `JourneySpec \| null` | — | Existing journey to load on mount. v1.x graph and v2.x `form_config` both accepted |
133
+ | `onSave` | `(spec: JourneySpec) => void` | — | Fired by the Save button. Marks the store as clean |
134
+ | `onPublish` | `(spec: JourneySpec) => void` | — | Fired by the Publish button |
135
+ | `onChange` | `(spec: JourneySpec) => void` | — | Fired on every config mutation (steps/rows/fields/navigation/summary/init/theme) |
136
+ | `onUnlockEdit` | `() => void` | — | Shown as "Unlock edit" when `readOnly` is `true` |
137
+ | `onOpenRunner` | `() => void` | — | Shown as a "Preview" button in the header |
138
+ | `readOnly` | `boolean` | — | Disables editing, hides destructive controls, surfaces the unlock action |
139
+ | `googleMapsApiKey` | `string` | — | Enables the Google Places address widget in previews |
140
+
141
+ The builder UI exposes four tabs: **Steps**, **Fields**, **Init**, **JSON**.
115
142
 
116
143
  ---
117
144
 
118
145
  ### FormRunnerWidget
119
146
 
120
- The step-by-step form engine. Provide a journey spec and receive collected answers.
121
-
122
147
  ```tsx
123
- import { FormRunnerWidget } from 'journey-builder';
148
+ import { FormRunnerWidget } from 'habit-claims-journey-components';
149
+ import type { JourneySpec, StepApiRequest } from 'habit-claims-journey-components';
124
150
 
125
- function RunnerPage({ journey }) {
151
+ function RunnerPage({ journey }: { journey: JourneySpec }) {
126
152
  return (
127
153
  <FormRunnerWidget
128
154
  journey={journey}
155
+ initialAnswers={{ /* optional pre-fill */ }}
156
+ enrichedOutput
129
157
  onSubmit={(answers) => submitToApi(answers)}
158
+ onSave={(partial) => saveDraft(partial)}
159
+ onFileSelect={async (file, key) => uploadToS3(file, key)}
160
+ onStepApiCall={async (req: StepApiRequest) => {
161
+ const res = await fetch(req.url, {
162
+ method: req.method,
163
+ headers: { 'Content-Type': 'application/json', ...req.headers },
164
+ body: req.body ? JSON.stringify(req.body) : undefined,
165
+ });
166
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
167
+ return res.json();
168
+ }}
169
+ onStepApiError={(err, req) => trackError(err, req)}
170
+ onInitApiCall={async (req) => (await fetch(req.url, { method: req.method, headers: req.headers })).json()}
171
+ googleMapsApiKey={import.meta.env.VITE_GOOGLE_MAPS_KEY}
172
+ fullScreen
130
173
  />
131
174
  );
132
175
  }
@@ -136,120 +179,271 @@ function RunnerPage({ journey }) {
136
179
 
137
180
  | Prop | Type | Required | Description |
138
181
  |---|---|---|---|
139
- | `journey` | `JourneySpec` | ✅ | The journey graph to run |
140
- | `specs` | `ClaimPropertySpec[]` | — | Optional specs for label/schema lookups |
141
- | `onSubmit` | `(answers: Record<string, unknown>) => void` | — | Called on form submission. Defaults to copying JSON to clipboard |
182
+ | `journey` | `JourneySpec` | ✅ | The journey to run. v1.x or v2.x |
183
+ | `onSubmit` | `(answers: Record<string, unknown>) => void` | — | Called when the user confirms the summary screen. Defaults to copying JSON to clipboard |
184
+ | `onSave` | `(partial: Record<string, unknown>) => void` | — | Called whenever the user reaches a checkpoint mid-journey, suitable for draft persistence |
185
+ | `onFileSelect` | `(file: File, questionKey: string) => Promise<string \| UploadResult \| void> \| void` | — | Host-side upload handler for upload widgets. Returns a URL or `{ url, name, ... }` |
186
+ | `onStepApiCall` | `(req: StepApiRequest) => Promise<unknown>` | — | **Recommended.** Delegates per-step API calls to the host (auth, CORS, retries, telemetry). Return value is stored as that step's `api_response` and is referenceable from later JSON Logic and the Summary |
187
+ | `onStepApiError` | `(err: unknown, req: StepApiRequest) => void` | — | Called when a step API call fails. When provided, the runner skips its default error toast. Fires regardless of the `on_error` policy. Exceptions thrown by this handler are caught and logged — they never crash the runner |
188
+ | `onInitApiCall` | `(req: InitApiRequest) => Promise<unknown>` | — | Delegates the optional init/prefill API call to the host |
189
+ | `initialAnswers` | `Record<string, unknown>` | — | Seeds the runner with existing values. The engine walks forward until it hits the first unanswered question, or straight to the summary if everything is filled |
190
+ | `enrichedOutput` | `boolean` | — | When `true`, `onSubmit` receives `{ [key]: { data, schema } }` instead of the flat answers map |
191
+ | `googleMapsApiKey` | `string` | — | Enables the Google Places address widget |
192
+ | `fullScreen` | `boolean` | — | Renders the runner full-viewport (otherwise it fills its parent) |
193
+
194
+ ##### `StepApiRequest`
195
+
196
+ ```ts
197
+ type StepApiRequest = {
198
+ step_id: string;
199
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
200
+ url: string;
201
+ headers: Record<string, string>;
202
+ body?: unknown; // already-interpolated JSON
203
+ answers: Record<string, unknown>; // current answers snapshot
204
+ };
205
+ ```
142
206
 
143
- ---
207
+ ##### `InitApiRequest`
144
208
 
145
- ## Data Types
209
+ ```ts
210
+ type InitApiRequest = {
211
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
212
+ url: string;
213
+ headers?: Record<string, string>;
214
+ };
215
+ ```
146
216
 
147
- ### `ClaimPropertySpec`
217
+ ##### Step API `on_error` policy
148
218
 
149
- A field definition from your data model.
219
+ Each step's `navigation.api_call.on_error` controls what happens when the call (or `onStepApiCall`) throws:
150
220
 
151
- ```typescript
152
- interface ClaimPropertySpec {
153
- id: string;
154
- namespace: string; // e.g. "insured.full_name"
155
- claimspec_id: string;
156
- parent_id: string | null;
157
- order_index: number;
158
- schema: string; // e.g. "v1_string", "v2_date-1", "v2_currency-1"
159
- label: string; // Human-readable label
160
- options: ClaimPropertySpecOption[] | null;
161
- help_text: string | null;
162
- placeholder: string | null;
163
- // ...additional display fields
164
- }
165
- ```
221
+ | Value | Behavior |
222
+ |---|---|
223
+ | `block` *(default)* | Stay on the current step, surface an error. `apiResponses[step_id]` is left untouched (preserves any previous successful response) |
224
+ | `continue` | Proceed using the step's navigation rules. `api_response` for this step stays unset; any previous successful value is preserved |
225
+ | `goto:<step_id>` | Jump straight to the given step (e.g. an error screen), bypassing rules |
226
+
227
+ `onStepApiError` is always called when present, regardless of policy. If the handler itself throws, the runner catches the error, logs it (`onStepApiError handler threw`), and still honors the configured `on_error` policy.
228
+
229
+ ---
166
230
 
167
- ### `JourneySpec`
231
+ ## Data Model — `JourneySpec` (v2.0)
168
232
 
169
- The portable journey graph format output of the Builder, input of the Runner.
233
+ A `JourneySpec` is a portable envelope that wraps a `form_config` plus journey-level metadata.
170
234
 
171
- ```typescript
235
+ ```ts
172
236
  interface JourneySpec {
173
237
  journey_id: string;
174
238
  claimspec_id: string;
175
- version: string;
176
- nodes: JourneySpecNode[];
177
- edges: JourneySpecEdge[];
178
- start_node_id: string | null;
239
+ version: string; // "2.0.x" for the linear model
240
+ form_config: FormConfig; // ◀ the new authoring model
241
+ theme?: JourneyTheme;
242
+ mode?: JourneyMode;
243
+
244
+ // Legacy v1.x — preserved when re-exporting an imported v1 spec.
245
+ nodes?: JourneySpecNode[];
246
+ edges?: JourneySpecEdge[];
247
+ start_node_id?: string | null;
248
+ }
249
+ ```
250
+
251
+ ### `FormConfig`
252
+
253
+ ```ts
254
+ interface FormConfig {
255
+ steps: FormStep[]; // ordered
256
+ fields: FormField[]; // flat catalog, referenced by id
257
+ summary: SummaryPanelConfig;
258
+ init: InitConfig;
179
259
  }
180
260
  ```
181
261
 
182
- ### `JourneySpecNode`
183
-
184
- ```typescript
185
- interface JourneySpecNode {
186
- node_id: string;
187
- question_key: string; // Namespace used as answer key
188
- source_property_spec_id: string;
189
- schema: string;
190
- options?: ClaimPropertySpecOption[];
191
- actor: 'customer' | 'operator';
192
- bindings: { target_namespace: string }[];
193
- ui: { widget: string };
194
- layout: { x: number; y: number };
195
- label_override?: string;
262
+ ### `FormStep`
263
+
264
+ ```ts
265
+ interface FormStep {
266
+ step_id: string;
267
+ title: string;
268
+ description?: string;
269
+ rows: FormRow[]; // each row has field_ids[]
270
+ navigation: {
271
+ api_call?: StepApiCall; // fired when leaving the step
272
+ rules: NavRule[]; // JSON Logic; first match wins
273
+ button_label?: string;
274
+ };
275
+ }
276
+ ```
277
+
278
+ ### `NavRule`
279
+
280
+ ```ts
281
+ interface NavRule {
282
+ rule_id: string;
283
+ condition: Record<string, unknown> | null; // null = always (default)
284
+ next_step_id: string; // or "__submit__"
285
+ priority?: number;
286
+ }
287
+ ```
288
+
289
+ ### `StepApiCall`
290
+
291
+ ```ts
292
+ interface StepApiCall {
293
+ enabled: boolean;
294
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
295
+ url: string;
296
+ headers?: Record<string, string>;
297
+ body_template?: string; // tokens like {{answers.foo}} interpolated by the runner
298
+ on_error?: 'block' | 'continue' | `goto:${string}`; // defaults to 'block'
299
+ }
300
+ ```
301
+
302
+ ### `FormField`
303
+
304
+ ```ts
305
+ interface FormField {
306
+ field_id: string;
307
+ namespace: string; // answer key
308
+ label: string;
309
+ labelOverride?: string;
310
+ specId?: string; // or "custom"
311
+ schema: string; // e.g. "v1_string", "v2_date-1"
312
+ options?: ClaimPropertySpecOption[] | null;
313
+ widget?: string; // override (e.g. "email", "textarea")
196
314
  required?: boolean;
315
+ actor?: 'customer' | 'operator';
316
+ bindings?: { target_namespace: string }[];
317
+ uploadConfig?: UploadConfig;
318
+ allowFuture?: boolean;
319
+ placeholder?: string;
320
+ helpText?: string;
321
+ }
322
+ ```
323
+
324
+ ### `SummaryPanelConfig`
325
+
326
+ ```ts
327
+ interface SummaryPanelConfig {
328
+ title: string;
329
+ show_pricing?: boolean;
330
+ sections: SummarySection[];
331
+ }
332
+
333
+ interface SummarySection {
334
+ section_id: string;
335
+ title: string;
336
+ visible_from_step?: number; // 1-based
337
+ until_step?: number; // 1-based
338
+ display_fields: SummaryDisplayField[];
339
+ }
340
+
341
+ interface SummaryDisplayField {
342
+ display_id: string;
343
+ source: 'form_field' | 'api_response';
344
+ field_id?: string; // when source === 'form_field'
345
+ step_id?: string; // when source === 'api_response'
346
+ response_path?: string; // dot-path inside api_response
347
+ label: string;
348
+ format?: 'text' | 'date' | 'datetime' | 'currency' | 'number';
197
349
  }
198
350
  ```
199
351
 
200
- ### `JourneySpecEdge`
352
+ ### `InitConfig`
353
+
354
+ ```ts
355
+ interface InitConfig {
356
+ values: Record<string, unknown>;
357
+ api?: {
358
+ enabled: boolean;
359
+ method: 'GET' | 'POST';
360
+ url: string;
361
+ headers?: Record<string, string>;
362
+ mapping?: Record<string, string>; // namespace -> response_path
363
+ };
364
+ }
365
+ ```
201
366
 
202
- ```typescript
203
- interface JourneySpecEdge {
204
- edge_id: string;
205
- from_node_id: string;
206
- to_node_id: string;
207
- condition: Record<string, unknown> | null; // JSON Logic expression
208
- priority: number;
209
- is_default: boolean;
367
+ ### `JourneyTheme`
368
+
369
+ CSS variables injected on the runner/builder root, overriding `--claims-journey-*` defaults. Colors are **HSL triplets** (e.g. `"222 47% 11%"`).
370
+
371
+ ```ts
372
+ interface JourneyTheme {
373
+ primaryColor?: string;
374
+ backgroundColor?: string;
375
+ cardColor?: string;
376
+ foregroundColor?: string;
377
+ fontFamilyHeading?: string;
378
+ fontFamilyBody?: string;
379
+ fontSizeBase?: number; // px
380
+ borderRadius?: number; // rem
381
+ spacing?: 'compact' | 'cozy' | 'spacious';
210
382
  }
211
383
  ```
212
384
 
213
385
  ---
214
386
 
387
+ ## JSON Schema validation
388
+
389
+ Draft-07 JSON Schemas describing `FormConfig` and `JourneySpec` v2 ship with the package and are re-exported as plain objects:
390
+
391
+ ```ts
392
+ import Ajv from 'ajv';
393
+ import { formConfigSchema, journeySpecSchema } from 'habit-claims-journey-components';
394
+
395
+ const ajv = new Ajv({ allErrors: true, strict: false });
396
+ ajv.addSchema(formConfigSchema); // resolved by $id from journeySpecSchema
397
+ const validate = ajv.compile(journeySpecSchema);
398
+
399
+ if (!validate(spec)) console.error(validate.errors);
400
+ ```
401
+
402
+ Schema sources:
403
+ - [`src/schemas/form-config.schema.json`](./src/schemas/form-config.schema.json)
404
+ - [`src/schemas/journey-spec.schema.json`](./src/schemas/journey-spec.schema.json)
405
+
406
+ See [docs/SCHEMAS.md](./docs/SCHEMAS.md) for the full reference of required fields, types, and enumerations.
407
+
408
+ ---
409
+
215
410
  ## Supported Schema Types
216
411
 
217
412
  | Schema | Widget | Validation |
218
413
  |---|---|---|
219
414
  | `v1_string` | Text input | — |
220
415
  | `v2_single_option_select-1` | Dropdown select | Must pick an option |
221
- | `v2_date-1` | Date picker | Valid date |
416
+ | `v2_date-1` | Date picker | Valid date; `allowFuture` controls future dates |
222
417
  | `v2_currency-1` | Currency input | Numeric |
223
- | `v2_single_asset_upload` | File upload | |
418
+ | `v2_single_asset_upload` | File upload (via `onFileSelect`) | Type/size from `uploadConfig` |
224
419
  | `v2_phone-1` | Phone with country code | Length check |
225
420
  | `v2_email-1` | Email input | Format regex |
226
421
 
227
- Nodes support **widget overrides** any field can adopt a different widget (e.g., a string field rendered as an email input).
422
+ Fields can override the default widget via `widget` (e.g. a `v1_string` rendered as `"textarea"` or `"email"`).
228
423
 
229
424
  ---
230
425
 
231
426
  ## Builder Features
232
427
 
233
- - **Drag-and-drop** specs from a searchable, filterable catalog
234
- - **Custom nodes** — create fields not tied to existing specs
235
- - **Visual edge condition editor** — AND/OR rules with comparison operators
236
- - **Advanced JSON Logic mode** — direct editing for complex conditions
237
- - **Condition testing panel** — validate edge logic with sample data
238
- - **Graph validation** — detects loops, orphans, missing conditions, duplicate bindings
239
- - **Auto-layout** — automatic graph arrangement
240
- - **Keyboard shortcuts** — Delete, Ctrl+Z/Y (undo/redo), Ctrl+C/V (copy/paste), Ctrl+D (duplicate)
241
- - **Import/Export** — full-fidelity JourneySpec JSON via file upload or paste
242
- - **Path highlighting** — visual tracking during preview
428
+ - **Steps tab** — three-pane layout (Steps list / Step editor / Summary editor) with drag-and-drop rows and inline field reuse from the catalog
429
+ - **Fields tab** — global catalog with Used/Unused filter and quick-jump chips that take you straight to the Step using each field
430
+ - **Init tab** — static initial values + optional prefill API call mapping (`namespace → response_path`)
431
+ - **JSON tab** — full `JourneySpec` view with copy / download / paste-to-import escape hatch
432
+ - **Per-step API calls** — method, URL, headers, JSON body template (`{{answers.foo}}`), and `on_error` policy
433
+ - **Per-step navigation rules** — JSON Logic conditions, priority, `__submit__` target
434
+ - **Theme panel** — primary/background/card/foreground colors, fonts, base size, radius, spacing scale
435
+ - **Persistence** — dirty-state tracking, Save / Publish, 50-action undo/redo, read-only mode with unlock action
243
436
 
244
437
  ## Runner Features
245
438
 
246
- - **Step-by-step navigation** with progress bar
247
- - **Conditional branching** JSON Logic evaluation at each edge
248
- - **Answer history** scrollable list with jump-back navigation
249
- - **Schema-aware widgets** — renders the correct input for each field type
250
- - **Required field enforcement** — blocks navigation until valid
251
- - **Review screen** summary of all answers before submission
252
- - **Bindings propagation** answers auto-mapped to target namespaces
439
+ - **Step-by-step navigation** with non-destructive back/forward and mid-journey draft persistence (via `initialAnswers`)
440
+ - **Auto-progression** through steps whose only effect is a navigation rule
441
+ - **JSON Logic** evaluation against `{ answers, api_responses }`
442
+ - **Host-delegated APIs** — per-step (`onStepApiCall`) and init (`onInitApiCall`), with three `on_error` policies
443
+ - **File upload** — async `onFileSelect` returning a URL or rich `UploadResult`
444
+ - **Schema-aware widgets** with mobile-first touch targets (48px min) and iOS-zoom-safe inputs
445
+ - **Vertical summary screen** rendering the configured `SummaryPanelConfig`
446
+ - **Enriched output** — `enrichedOutput` flag wraps each answer as `{ data, schema }`
253
447
 
254
448
  ---
255
449
 
@@ -257,40 +451,341 @@ Nodes support **widget overrides** — any field can adopt a different widget (e
257
451
 
258
452
  ```
259
453
  src/
260
- ├── index.ts # Barrel exports
261
- ├── types/journey.ts # Shared types & schema helpers
454
+ ├── index.ts # Barrel exports
455
+ ├── types/journey.ts # Public types & helpers
262
456
  ├── components/
263
- │ ├── journey-builder/
264
- │ │ ├── JourneyBuilderWidget.tsx # Entry point widget
265
- │ │ ├── JourneyBuilder.tsx # Layout (catalog + canvas + props panel)
266
- │ │ ├── JourneyCanvas.tsx # React Flow canvas
267
- │ │ ├── QuestionNode.tsx # Custom node component
268
- │ │ ├── SpecsCatalog.tsx # Draggable spec list
269
- │ │ ├── PropertiesPanel.tsx # Node/edge property editors
270
- │ │ ├── NodePropertiesEditor.tsx
271
- │ │ ├── EdgePropertiesEditor.tsx
272
- │ │ ├── ConditionalEdge.tsx # Custom edge with labels
273
- │ │ ├── CreateNodeDialog.tsx # Custom field creation
274
- │ │ ├── ImportJourneyDialog.tsx
275
- │ │ ├── FormPreviewPanel.tsx # Inline preview
276
- │ │ ├── GraphValidationPanel.tsx # Validation results
277
- │ │ ├── edge-editor/ # Condition editing components
278
- │ │ └── preview-widgets/ # Schema-specific input widgets
457
+ │ ├── form-configurator/ # ◀ v2.0 Builder
458
+ │ │ ├── JourneyBuilderWidget.tsx # Entry point
459
+ │ │ ├── FormConfigurator.tsx # Tabs + layout
460
+ │ │ ├── steps/ # Steps tab
461
+ │ │ ├── fields/ # Fields tab (Used/Unused filter)
462
+ │ │ ├── init/ # Init tab
463
+ │ │ ├── json/ # JSON tab
464
+ │ │ └── summary/ # Summary editor
279
465
  │ ├── runner/
280
- │ │ ├── FormRunnerWidget.tsx # ◀ Entry point widget
281
- │ └── AnswerHistoryList.tsx
282
- │ └── ui/ # shadcn/ui components
466
+ │ │ └── FormRunnerWidget.tsx # ◀ Runner entry point
467
+ ├── journey-builder/ # Shared previews, theme panel, portal context
468
+ └── preview-widgets/ # Schema-specific widgets
469
+ │ └── ui/ # shadcn/ui primitives
283
470
  ├── stores/
284
- │ └── journeyBuilderStore.ts # Zustand store (internal)
471
+ │ └── formConfiguratorStore.ts # Zustand store + legacy converter
285
472
  ├── lib/
286
- │ └── graph-validation.ts # Validation engine
287
- └── pages/ # Demo app pages
473
+ │ └── theme-css.ts # CSS-variable builder for JourneyTheme
474
+ └── pages/ # Demo app
288
475
  ├── Index.tsx
289
476
  └── Runner.tsx
290
477
  ```
291
478
 
292
479
  ---
293
480
 
481
+ ## Migration v1 → v2
482
+
483
+ v2.0 replaces the React Flow graph canvas with a linear **Steps → Rows → Fields** configurator. Legacy v1.x specs (`nodes`/`edges`/`start_node_id`) are still accepted on import and auto-converted to a `form_config`.
484
+
485
+ ### Removed dependency
486
+
487
+ ```diff
488
+ - "@xyflow/react": "^12.x" // no longer a peer dependency
489
+ ```
490
+
491
+ ### CSS
492
+
493
+ ```diff
494
+ - import 'habit-claims-journey-components/style.css';
495
+ + // CSS is auto-injected by the bundle — no separate import needed
496
+ ```
497
+
498
+ ### `JourneyBuilderWidget` props
499
+
500
+ | v1.x | v2.x | Notes |
501
+ |---|---|---|
502
+ | `specs` | `specs` | Unchanged |
503
+ | `journey` | `journey` | Accepts both v1 and v2 shapes |
504
+ | `onSave(spec)` | `onSave(spec)` | Now also marks the store as clean |
505
+ | — | `onPublish(spec)` | **New** — dedicated publish action |
506
+ | — | `onChange(spec)` | **New** — fires on every mutation |
507
+ | — | `onOpenRunner()` | **New** — header "Preview" button |
508
+ | — | `readOnly` | **New** — disables editing |
509
+ | — | `onUnlockEdit()` | **New** — shown when `readOnly` is on |
510
+ | — | `googleMapsApiKey` | **New** — enables Location widget previews |
511
+ | `onNodeClick`, `onEdgeClick`, `nodeTypes`, `edgeTypes` | — | **Removed** (no canvas) |
512
+
513
+ ```diff
514
+ <JourneyBuilderWidget
515
+ specs={specs}
516
+ journey={existing}
517
+ onSave={spec => save(spec)}
518
+ + onPublish={spec => publish(spec)}
519
+ + onChange={spec => setDraft(spec)}
520
+ + onOpenRunner={() => navigate('/runner')}
521
+ + readOnly={false}
522
+ + googleMapsApiKey={import.meta.env.VITE_GOOGLE_MAPS_KEY}
523
+ />
524
+ ```
525
+
526
+ ### `FormRunnerWidget` props
527
+
528
+ | v1.x | v2.x | Notes |
529
+ |---|---|---|
530
+ | `journey` | `journey` | Now drives `form_config` (or auto-converted legacy graph) |
531
+ | `onSubmit(answers)` | `onSubmit(answers)` | Set `enrichedOutput` for `{ data, schema }` shape |
532
+ | `onFileSelect(file, key)` | `onFileSelect(file, key)` | May now return `UploadResult` `{ url, filename, mimetype?, size? }` |
533
+ | `onApiCall(req)` | **`onStepApiCall(req: StepApiRequest)`** | Renamed; request includes `step_id` and `answers` snapshot |
534
+ | — | `onStepApiError(err, req)` | **New** — suppresses the default error toast; safe even if it throws |
535
+ | — | `onInitApiCall(req: InitApiRequest)` | **New** — host-delegated init/prefill call |
536
+ | — | `onSave(partial)` | **New** — mid-journey draft persistence |
537
+ | `initialValues` | **`initialAnswers`** | Renamed for consistency with the engine |
538
+ | — | `enrichedOutput` | **New** — wraps every answer as `{ data, schema }` |
539
+ | — | `fullScreen` | **New** — viewport-fill layout |
540
+ | `mode`, `groups` | — | Ignored; layout is now driven by `form_config.steps`/`rows` |
541
+
542
+ ```diff
543
+ <FormRunnerWidget
544
+ journey={journey}
545
+ - initialValues={draft}
546
+ + initialAnswers={draft}
547
+ + enrichedOutput
548
+ - onApiCall={async (req) => fetchJson(req.url, req)}
549
+ + onStepApiCall={async (req) => fetchJson(req.url, req)}
550
+ + onStepApiError={(err, req) => logger.warn(err, req)}
551
+ + onInitApiCall={async (req) => fetchJson(req.url, req)}
552
+ + onSave={(partial) => saveDraft(partial)}
553
+ onSubmit={(answers) => submit(answers)}
554
+ />
555
+ ```
556
+
557
+ ### `JourneySpec` shape
558
+
559
+ ```diff
560
+ {
561
+ "journey_id": "j-001",
562
+ "claimspec_id": "cs-001",
563
+ - "version": "1.0",
564
+ - "nodes": [ /* JourneySpecNode[] */ ],
565
+ - "edges": [ /* JourneySpecEdge[] */ ],
566
+ - "start_node_id": "node-1",
567
+ - "mode": "sequential",
568
+ - "groups": []
569
+ + "version": "2.0",
570
+ + "form_config": {
571
+ + "steps": [ /* FormStep[] — rows -> field_ids */ ],
572
+ + "fields": [ /* FormField[] — flat catalog */ ],
573
+ + "summary": { "title": "Review", "sections": [] },
574
+ + "init": { "values": {} }
575
+ + }
576
+ }
577
+ ```
578
+
579
+ Importing a v1 spec is transparent: the store converts `nodes`/`edges` to `form_config` on load. Re-exporting preserves the legacy fields for downstream consumers that still read them.
580
+
581
+ ### Step API errors
582
+
583
+ v1 always blocked navigation on a failing API call. v2 introduces an explicit per-step policy on `navigation.api_call.on_error`:
584
+
585
+ | Value | Behavior |
586
+ |---|---|
587
+ | `block` *(default)* | Stay on the step; `apiResponses[step_id]` is preserved |
588
+ | `continue` | Run navigation rules; `api_response` for this step stays unset |
589
+ | `goto:<step_id>` | Jump to the named step (e.g. an error screen) |
590
+
591
+ ---
592
+
593
+ ## Full Examples
594
+
595
+ ### Minimal `FormConfig`
596
+
597
+ A two-step journey with a select that branches to a follow-up step or skips to submit.
598
+
599
+ ```json
600
+ {
601
+ "steps": [
602
+ {
603
+ "step_id": "step-vehicle",
604
+ "title": "Your vehicle",
605
+ "rows": [
606
+ { "row_id": "r1", "layout": "vertical", "field_ids": ["f-plate", "f-has-damage"] }
607
+ ],
608
+ "navigation": {
609
+ "rules": [
610
+ {
611
+ "rule_id": "rule-damage",
612
+ "condition": { "==": [{ "var": "answers.vehicle.has_damage" }, "yes"] },
613
+ "next_step_id": "step-damage",
614
+ "priority": 10
615
+ },
616
+ { "rule_id": "rule-default", "condition": null, "next_step_id": "__submit__" }
617
+ ]
618
+ }
619
+ },
620
+ {
621
+ "step_id": "step-damage",
622
+ "title": "Describe the damage",
623
+ "rows": [{ "row_id": "r2", "layout": "vertical", "field_ids": ["f-damage-notes"] }],
624
+ "navigation": {
625
+ "rules": [{ "rule_id": "r-end", "condition": null, "next_step_id": "__submit__" }]
626
+ }
627
+ }
628
+ ],
629
+ "fields": [
630
+ { "field_id": "f-plate", "namespace": "vehicle.plate", "label": "License plate", "schema": "v1_string", "required": true },
631
+ {
632
+ "field_id": "f-has-damage",
633
+ "namespace": "vehicle.has_damage",
634
+ "label": "Is the vehicle damaged?",
635
+ "schema": "v2_single_option_select-1",
636
+ "required": true,
637
+ "options": [
638
+ { "data": "yes", "label": "Yes" },
639
+ { "data": "no", "label": "No" }
640
+ ]
641
+ },
642
+ { "field_id": "f-damage-notes", "namespace": "vehicle.damage_notes", "label": "Damage description", "schema": "v1_string", "widget": "textarea" }
643
+ ],
644
+ "summary": {
645
+ "title": "Review your claim",
646
+ "sections": [
647
+ {
648
+ "section_id": "sec-vehicle",
649
+ "title": "Vehicle",
650
+ "display_fields": [
651
+ { "display_id": "d-plate", "source": "form_field", "field_id": "f-plate", "label": "Plate" },
652
+ { "display_id": "d-damage", "source": "form_field", "field_id": "f-damage-notes", "label": "Damage" }
653
+ ]
654
+ }
655
+ ]
656
+ },
657
+ "init": { "values": { "vehicle.country": "BR" } }
658
+ }
659
+ ```
660
+
661
+ ### Full `JourneySpec` with API call + init prefill
662
+
663
+ Per-step `api_call` with `on_error: "goto:..."`, a summary section reading from `api_response`, and an `init.api` prefill.
664
+
665
+ ```json
666
+ {
667
+ "journey_id": "j-auto-claim-001",
668
+ "claimspec_id": "cs-auto-1",
669
+ "version": "2.0",
670
+ "form_config": {
671
+ "steps": [
672
+ {
673
+ "step_id": "step-id",
674
+ "title": "Find your policy",
675
+ "rows": [{ "row_id": "r1", "layout": "horizontal", "field_ids": ["f-doc"] }],
676
+ "navigation": {
677
+ "api_call": {
678
+ "enabled": true,
679
+ "method": "POST",
680
+ "url": "https://api.example.com/policies/lookup",
681
+ "headers": { "x-tenant": "acme" },
682
+ "body_template": "{\"document\":\"{{answers.customer.document}}\"}",
683
+ "on_error": "goto:step-not-found"
684
+ },
685
+ "rules": [
686
+ { "rule_id": "r-ok", "condition": null, "next_step_id": "step-confirm" }
687
+ ]
688
+ }
689
+ },
690
+ {
691
+ "step_id": "step-confirm",
692
+ "title": "Confirm your policy",
693
+ "rows": [{ "row_id": "r2", "layout": "vertical", "field_ids": ["f-accept"] }],
694
+ "navigation": {
695
+ "rules": [{ "rule_id": "r-end", "condition": null, "next_step_id": "__submit__" }]
696
+ }
697
+ },
698
+ {
699
+ "step_id": "step-not-found",
700
+ "title": "We couldn't find your policy",
701
+ "rows": [],
702
+ "navigation": { "rules": [] }
703
+ }
704
+ ],
705
+ "fields": [
706
+ { "field_id": "f-doc", "namespace": "customer.document", "label": "Document ID", "schema": "v1_string", "required": true },
707
+ { "field_id": "f-accept", "namespace": "consent.accepted", "label": "I confirm the details above", "schema": "v2_single_option_select-1", "required": true,
708
+ "options": [{ "data": "yes", "label": "Yes" }] }
709
+ ],
710
+ "summary": {
711
+ "title": "Review",
712
+ "sections": [
713
+ {
714
+ "section_id": "sec-policy",
715
+ "title": "Policy",
716
+ "display_fields": [
717
+ { "display_id": "d-holder", "source": "api_response", "step_id": "step-id", "response_path": "policy.holder.name", "label": "Holder" },
718
+ { "display_id": "d-plate", "source": "api_response", "step_id": "step-id", "response_path": "policy.vehicle.plate", "label": "Plate" }
719
+ ]
720
+ }
721
+ ]
722
+ },
723
+ "init": {
724
+ "values": {},
725
+ "api": {
726
+ "enabled": true,
727
+ "method": "GET",
728
+ "url": "https://api.example.com/session/prefill",
729
+ "headers": { "x-tenant": "acme" },
730
+ "mapping": {
731
+ "customer.full_name": "user.name",
732
+ "customer.email": "user.email"
733
+ }
734
+ }
735
+ }
736
+ },
737
+ "theme": {
738
+ "primaryColor": "222 47% 11%",
739
+ "fontFamilyHeading": "Poppins, sans-serif",
740
+ "fontFamilyBody": "Roboto, sans-serif",
741
+ "borderRadius": 0.75,
742
+ "spacing": "cozy"
743
+ }
744
+ }
745
+ ```
746
+
747
+ ### Handling `StepApiRequest` on the host
748
+
749
+ The runner builds a `StepApiRequest` whenever a step has an enabled `api_call`. `body` is already interpolated from `body_template` against the live `answers`; `answers` is a snapshot for host-side enrichment (auth, tenant context, etc.).
750
+
751
+ ```ts
752
+ import type { StepApiRequest } from 'habit-claims-journey-components';
753
+
754
+ async function onStepApiCall(req: StepApiRequest) {
755
+ // req = { step_id, method, url, headers, body?, answers }
756
+ const res = await fetch(req.url, {
757
+ method: req.method,
758
+ headers: {
759
+ 'Content-Type': 'application/json',
760
+ authorization: `Bearer ${getToken()}`,
761
+ ...req.headers,
762
+ },
763
+ body: req.body !== undefined ? JSON.stringify(req.body) : undefined,
764
+ });
765
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
766
+ // The returned value is stored as api_responses[req.step_id]
767
+ // and is referenceable from later JSON Logic rules and the Summary.
768
+ return res.json();
769
+ }
770
+ ```
771
+
772
+ ### Handling `InitApiRequest` on the host
773
+
774
+ Fired once on runner mount when `form_config.init.api.enabled` is `true`. The response is walked through `init.api.mapping` (namespace → dot-path) to seed `answers` before the first step renders.
775
+
776
+ ```ts
777
+ import type { InitApiRequest } from 'habit-claims-journey-components';
778
+
779
+ async function onInitApiCall(req: InitApiRequest) {
780
+ // req = { method, url, headers? }
781
+ const res = await fetch(req.url, { method: req.method, headers: req.headers });
782
+ if (!res.ok) throw new Error(`Init HTTP ${res.status}`);
783
+ return res.json();
784
+ }
785
+ ```
786
+
787
+ ---
788
+
294
789
  ## License
295
790
 
296
791
  MIT