@sonata-innovations/fiber-types 2.2.0 → 2.5.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.
@@ -0,0 +1,428 @@
1
+ ---
2
+ title: Custom Presets & Templates (FBT)
3
+ applies-to:
4
+ - "@sonata-innovations/fiber-fbt@^2.2"
5
+ - "@sonata-innovations/fiber-types@^2.2"
6
+ read-when: "Extending FBT's pool with custom presets/templates: data-based vs factory definitions, icon catalog, collision rules, server-stored definitions."
7
+ ---
8
+
9
+ <!-- Generated from the Fiber repo's docs/ tree by project/scripts/sync-package-docs.mjs. Do not edit here. -->
10
+ # Custom Presets & Templates
11
+
12
+ Parent applications extend FBT's pool with custom **presets** and **templates** via the optional `customPresets` and `customTemplates` props. This is the deep-dive companion to the short "Custom Presets & Templates" section in `docs/integration/fbt.md`.
13
+
14
+ ## Concepts
15
+
16
+ - A **preset** is one or more pre-configured components. Dropping it onto the stage batch-adds those components to the **current screen** (at the drop position if dropped over an existing component, otherwise appended).
17
+ - A **template** is a complete `Flow`. Dropping it **merges** into the current flow: its screens are **appended** to the existing screen list via `mergeFlow`, and the first appended screen becomes the active screen. Nothing is replaced — the existing screens, metadata, and config are untouched, and the template's own `metadata`/`config` are ignored by the merge (only its screens are ingested).
18
+
19
+ Whether a definition is treated as a preset or a template is determined by **which prop array it arrives in** (`customPresets` vs `customTemplates`), not by its `type` string.
20
+
21
+ ## Two definition kinds, one set of props
22
+
23
+ Both props accept a union type:
24
+
25
+ ```ts
26
+ // fbt/src/types/custom.ts
27
+ export type AnyPresetDefinition = CustomPresetDefinition | PresetData;
28
+ export type AnyTemplateDefinition = CustomTemplateDefinition | TemplateData;
29
+ ```
30
+
31
+ - **Data-based** (`PresetData` / `TemplateData`, exported from `@sonata-innovations/fiber-types`) — plain serializable JSON. **Preferred**: it can be stored in a database, sent over the wire, and edited without code. FBT regenerates UUIDs automatically on every drop.
32
+ - **Factory-based** (`CustomPresetDefinition` / `CustomTemplateDefinition`, exported from `@sonata-innovations/fiber-fbt`) — a `factory()` function called on every drop. Kept for backward compatibility.
33
+
34
+ FBT distinguishes the two kinds structurally (`"factory" in definition`), so you can mix both kinds in the same array.
35
+
36
+ ## Data-based definitions (primary)
37
+
38
+ ### Shapes
39
+
40
+ ```ts
41
+ // @sonata-innovations/fiber-types
42
+ export type PresetData = {
43
+ type: string; // unique identifier (see collision rules below)
44
+ label: string; // display name in the pool
45
+ icon: string; // key into FBT's ICON_MAP (see icon catalog below)
46
+ components: Component[];
47
+ };
48
+
49
+ export type TemplateData = {
50
+ type: string;
51
+ label: string;
52
+ icon: string;
53
+ flow: Flow;
54
+ };
55
+ ```
56
+
57
+ ### UUID regeneration semantics
58
+
59
+ The UUIDs you write into a data-based definition are **placeholders**. On every drop, FBT calls `regenerateComponentUUIDs` (presets) or `regenerateFlowUUIDs` (templates) from `@sonata-innovations/fiber-types`:
60
+
61
+ - Every component gets a fresh UUID; `properties` are deep-cloned (`structuredClone`), and nested `components` arrays (groups) are regenerated recursively.
62
+ - For templates, the flow UUID and every screen UUID are also replaced.
63
+ - The original definition object is never mutated, so the same definition can be dropped any number of times without duplicate-UUID bugs.
64
+
65
+ This means placeholder UUIDs only need to be unique **within** the definition (so group nesting is unambiguous); any string works.
66
+
67
+ ### Full preset example
68
+
69
+ `Component` is a discriminated union keyed on `type` — `properties` is checked against the shape for that `type` (e.g. `label` is required for `"inputText"`, `value` for `"header"`, `options` for `"dropDown"`). Group children live on the component's top-level `components` field, not inside `properties`.
70
+
71
+ ```ts
72
+ import type { PresetData } from "@sonata-innovations/fiber-types";
73
+
74
+ const ssnPreset: PresetData = {
75
+ type: "custom-ssn",
76
+ label: "SSN",
77
+ icon: "ssn",
78
+ components: [
79
+ {
80
+ uuid: "ssn-field",
81
+ type: "inputText",
82
+ properties: {
83
+ label: "Social Security Number",
84
+ placeholder: "XXX-XX-XXXX",
85
+ validation: {
86
+ rules: [
87
+ { type: "required" },
88
+ // NOTE: the param key is `regex`, not `pattern`
89
+ { type: "pattern", params: { regex: "^\\d{3}-\\d{2}-\\d{4}$" } },
90
+ ],
91
+ },
92
+ },
93
+ },
94
+ ],
95
+ };
96
+
97
+ const emergencyContactPreset: PresetData = {
98
+ type: "custom-emergencyContact",
99
+ label: "Emergency Contact",
100
+ icon: "emergencyContact",
101
+ components: [
102
+ {
103
+ uuid: "ec-group",
104
+ type: "group",
105
+ properties: { label: "Emergency Contact", showLabel: true },
106
+ components: [
107
+ {
108
+ uuid: "ec-name",
109
+ type: "inputText",
110
+ properties: {
111
+ label: "Contact Name",
112
+ width: "half",
113
+ validation: { rules: [{ type: "required" }] },
114
+ },
115
+ },
116
+ {
117
+ uuid: "ec-phone",
118
+ type: "inputText",
119
+ properties: {
120
+ label: "Contact Phone",
121
+ placeholder: "(555) 555-5555",
122
+ inputType: "tel",
123
+ width: "half",
124
+ validation: { rules: [{ type: "required" }, { type: "phone" }] },
125
+ },
126
+ },
127
+ {
128
+ uuid: "ec-relationship",
129
+ type: "dropDown",
130
+ properties: {
131
+ label: "Relationship",
132
+ options: [
133
+ { label: "Spouse", value: "spouse" },
134
+ { label: "Parent", value: "parent" },
135
+ { label: "Sibling", value: "sibling" },
136
+ { label: "Friend", value: "friend" },
137
+ { label: "Other", value: "other" },
138
+ ],
139
+ validation: { rules: [{ type: "required" }] },
140
+ },
141
+ },
142
+ ],
143
+ },
144
+ ],
145
+ };
146
+ ```
147
+
148
+ ### Full template example
149
+
150
+ ```ts
151
+ import type { TemplateData } from "@sonata-innovations/fiber-types";
152
+
153
+ const onboardingTemplate: TemplateData = {
154
+ type: "custom-employeeOnboarding",
155
+ label: "Employee Onboarding",
156
+ icon: "employeeOnboarding",
157
+ flow: {
158
+ uuid: "onboarding-flow",
159
+ metadata: { name: "Employee Onboarding" },
160
+ config: {},
161
+ screens: [
162
+ {
163
+ uuid: "onboarding-screen-1",
164
+ label: "Personal Info",
165
+ components: [
166
+ {
167
+ uuid: "ob-header-1",
168
+ type: "header",
169
+ properties: { value: "Employee Information" },
170
+ },
171
+ {
172
+ uuid: "ob-first-name",
173
+ type: "inputText",
174
+ properties: {
175
+ label: "First Name",
176
+ width: "half",
177
+ validation: { rules: [{ type: "required" }] },
178
+ },
179
+ },
180
+ {
181
+ uuid: "ob-last-name",
182
+ type: "inputText",
183
+ properties: {
184
+ label: "Last Name",
185
+ width: "half",
186
+ validation: { rules: [{ type: "required" }] },
187
+ },
188
+ },
189
+ {
190
+ uuid: "ob-email",
191
+ type: "inputText",
192
+ properties: {
193
+ label: "Email Address",
194
+ placeholder: "name@company.com",
195
+ inputType: "email",
196
+ validation: { rules: [{ type: "required" }, { type: "email" }] },
197
+ },
198
+ },
199
+ ],
200
+ },
201
+ {
202
+ uuid: "onboarding-screen-2",
203
+ label: "Employment Details",
204
+ components: [
205
+ {
206
+ uuid: "ob-header-2",
207
+ type: "header",
208
+ properties: { value: "Employment Details" },
209
+ },
210
+ {
211
+ uuid: "ob-job-title",
212
+ type: "inputText",
213
+ properties: {
214
+ label: "Job Title",
215
+ validation: { rules: [{ type: "required" }] },
216
+ },
217
+ },
218
+ {
219
+ uuid: "ob-employment-type",
220
+ type: "dropDown",
221
+ properties: {
222
+ label: "Employment Type",
223
+ options: [
224
+ { label: "Full-Time", value: "full-time" },
225
+ { label: "Part-Time", value: "part-time" },
226
+ { label: "Contract", value: "contract" },
227
+ { label: "Intern", value: "intern" },
228
+ ],
229
+ validation: { rules: [{ type: "required" }] },
230
+ },
231
+ },
232
+ ],
233
+ },
234
+ ],
235
+ },
236
+ };
237
+ ```
238
+
239
+ Remember: dropping this template does **not** overwrite the builder's current flow — its two screens are appended after the existing ones, and "Personal Info" becomes the active screen. The `metadata.name` above documents the template but is not merged into the current flow.
240
+
241
+ ### Passing to FBT
242
+
243
+ ```tsx
244
+ import { FBT } from "@sonata-innovations/fiber-fbt";
245
+ import "@sonata-innovations/fiber-fbt/styles";
246
+ import type { AnyPresetDefinition, AnyTemplateDefinition } from "@sonata-innovations/fiber-fbt";
247
+
248
+ const MY_PRESETS: AnyPresetDefinition[] = [ssnPreset, emergencyContactPreset];
249
+ const MY_TEMPLATES: AnyTemplateDefinition[] = [onboardingTemplate];
250
+
251
+ function App() {
252
+ return (
253
+ <FBT
254
+ flow={existingFlow}
255
+ onFlowChange={handleFlowChange}
256
+ customPresets={MY_PRESETS}
257
+ customTemplates={MY_TEMPLATES}
258
+ />
259
+ );
260
+ }
261
+ ```
262
+
263
+ Both props are optional; if omitted, FBT shows only its built-in items. Keep the arrays referentially stable (module constants or memoized) — the context memoizes its lookup maps on the array identities.
264
+
265
+ ## Factory-based definitions (back-compat)
266
+
267
+ ```ts
268
+ // @sonata-innovations/fiber-fbt
269
+ export type CustomPresetDefinition = {
270
+ type: string;
271
+ label: string;
272
+ icon: string;
273
+ factory: () => Component[];
274
+ };
275
+
276
+ export type CustomTemplateDefinition = {
277
+ type: string;
278
+ label: string;
279
+ icon: string;
280
+ factory: () => Flow;
281
+ };
282
+ ```
283
+
284
+ With a factory, **you** are responsible for returning fresh UUIDs on every call — FBT does not regenerate UUIDs for factory-based definitions. You would still use a factory when the definition must be computed at drop time (e.g. injecting the current date, user data, or environment-dependent options):
285
+
286
+ ```ts
287
+ import type { CustomPresetDefinition } from "@sonata-innovations/fiber-fbt";
288
+
289
+ const visitDatePreset: CustomPresetDefinition = {
290
+ type: "custom-visitDate",
291
+ label: "Visit Date",
292
+ icon: "date",
293
+ factory: () => [
294
+ {
295
+ uuid: crypto.randomUUID(),
296
+ type: "date",
297
+ properties: {
298
+ label: "Visit Date",
299
+ min: new Date().toISOString().slice(0, 10), // today, computed at drop
300
+ validation: { rules: [{ type: "required" }] },
301
+ },
302
+ },
303
+ ],
304
+ };
305
+ ```
306
+
307
+ For anything static, prefer `PresetData`/`TemplateData`.
308
+
309
+ ## How resolution works
310
+
311
+ 1. **Context injection** — the FBT provider builds `presetMap` / `templateMap` (`Map<string, () => Component[] | Flow>`) from the prop arrays, keyed by `type`. Factory definitions map to their `factory`; data definitions map to a wrapper that regenerates UUIDs. If two custom definitions share a `type`, the later one in the array wins (plain `Map.set` overwrite).
312
+ 2. **Kind by prop array** — items from `customPresets` are tagged `kind: "preset"`, items from `customTemplates` are tagged `kind: "template"` when the pool renders them. The `type` string plays no part in this.
313
+ 3. **Built-in-first lookup at drop** — the DnD handler resolves the dropped `type` against the **built-in** definitions first, and only falls back to the custom map if that returns nothing:
314
+
315
+ ```ts
316
+ // fbt/src/ui/dnd/dnd-wrapper.tsx
317
+ const flow = createTemplate(item.type) ?? templateMap.get(item.type)?.();
318
+ const components = createPreset(item.type) ?? presetMap.get(item.type)?.();
319
+ ```
320
+
321
+ ### Collision rules
322
+
323
+ There is **no required prefix** on custom `type` strings — FBT accepts any string. But because built-in lookup runs first, a custom definition whose `type` equals a built-in key is **silently shadowed**: your pool entry shows your label and icon, but dropping it produces the built-in content.
324
+
325
+ Built-in keys to avoid — presets: `preset-phone`, `preset-email`, `preset-date`, `preset-url`, `preset-fullName`, `preset-dollarAmount`, `preset-password`, `preset-addressBlock`, `preset-contactInfo`, `preset-nameFields`, `preset-agreement`; templates: `template-contactForm`, `template-signUp`, `template-patientIntake`, `template-feedbackSurvey`.
326
+
327
+ **Recommendation:** use a distinct namespace such as `custom-*`. No built-in uses it, and it matches what the Fiber server enforces for stored definitions (see below).
328
+
329
+ ## Icon catalog
330
+
331
+ `icon` is a key into FBT's `ICON_MAP` (`fbt/src/lib/icons.tsx`) — 46 built-in SVG icons. Icon keys are their own namespace: they often mirror component types but are not the same set (e.g. the paragraph-text icon is `descriptionText` while the component type is `text`; `select` is the dropdown icon). If the key is unknown, the item renders with an empty icon slot — no error, graceful fallback.
332
+
333
+ ### Display & structure
334
+
335
+ | Key | Glyph |
336
+ |-----|-------|
337
+ | `header` | Heading lines |
338
+ | `descriptionText` | Paragraph lines |
339
+ | `divider` | Horizontal rule |
340
+ | `callout` | Info box |
341
+ | `table` | Grid |
342
+
343
+ ### Text & number inputs
344
+
345
+ | Key | Glyph |
346
+ |-----|-------|
347
+ | `textInput` | Single-line field |
348
+ | `textArea` | Multi-line field |
349
+ | `numberInput` | Field with `#` |
350
+ | `dollarAmount` | Field with `$` |
351
+ | `phone` | Phone handset/device |
352
+ | `email` | Envelope |
353
+ | `url` | Chain link |
354
+ | `password` | Padlock |
355
+
356
+ ### Selection
357
+
358
+ | Key | Glyph |
359
+ |-----|-------|
360
+ | `select` | Dropdown |
361
+ | `multiSelect` | Checked list |
362
+ | `checkbox` | Checked box |
363
+ | `radio` | Radio dot |
364
+ | `toggleSwitch` | Switch |
365
+ | `yesNo` | Check/cross pair |
366
+ | `confirm` | Checked box (shares the `checkbox` glyph) |
367
+ | `cardSelect` | Card grid |
368
+
369
+ ### Date & time
370
+
371
+ | Key | Glyph |
372
+ |-----|-------|
373
+ | `date` | Calendar |
374
+ | `time` | Clock |
375
+ | `dateTime` | Calendar + clock |
376
+ | `dateRange` | Two calendars |
377
+ | `timeRange` | Two clocks |
378
+ | `dateTimeRange` | Two calendar+clock pairs |
379
+
380
+ ### Interactive & special
381
+
382
+ | Key | Glyph |
383
+ |-----|-------|
384
+ | `fileUpload` | Upload arrow |
385
+ | `rating` | Star |
386
+ | `slider` | Slider track |
387
+ | `colorPicker` | Color wheel |
388
+ | `signature` | Signature stroke |
389
+ | `repeater` | Stacked dashed rows |
390
+ | `calculated` | `fx` in a box |
391
+
392
+ ### People & preset-flavored
393
+
394
+ | Key | Glyph |
395
+ |-----|-------|
396
+ | `fullName` | Person |
397
+ | `addressBlock` | Map pin |
398
+ | `contactInfo` | Contact card |
399
+ | `nameFields` | Paired fields |
400
+ | `agreement` | Document with check |
401
+ | `ssn` | Masked field (`***`) |
402
+ | `emergencyContact` | Person with plus |
403
+
404
+ ### Template-flavored
405
+
406
+ | Key | Glyph |
407
+ |-----|-------|
408
+ | `signUpForm` | Form with avatar |
409
+ | `patientIntake` | Bulleted form |
410
+ | `feedbackSurvey` | Form with star |
411
+ | `contactForm` | Form lines |
412
+ | `employeeOnboarding` | Document lines |
413
+
414
+ ## Server-stored definitions
415
+
416
+ The Fiber server (private package, not published) provides tenant-scoped storage for data-based definitions:
417
+
418
+ - CRUD endpoints at `/api/v1/presets` and `/api/v1/templates`, storing `PresetData` / `TemplateData` JSON per tenant.
419
+ - The server **rejects** any stored definition whose `type` does not start with `custom-` — this prefix guarantees no collision with built-in keys.
420
+ - The portal provides management pages for presets and templates, and its flow editor fetches the tenant's custom definitions on mount.
421
+
422
+ From FBT's perspective there is nothing special about server-stored definitions: the parent app fetches them and passes them through the same `customPresets` / `customTemplates` props as data-based definitions.
423
+
424
+ ## UI behavior
425
+
426
+ - When either prop array is non-empty, the corresponding pool tab (Presets or Templates) appends a **CUSTOM** section after the built-in sections, containing one item per definition.
427
+ - Custom items carry a **star badge** in place of the colored accent that built-in preset/template items get; search results label them with a "Custom" badge.
428
+ - Items are drag-and-drop onto the stage. Icon resolution is `ICON_MAP[item.icon]` guarded by `{Icon && <Icon />}` — an unknown key renders nothing.