@sonata-innovations/fiber-types 2.3.0 → 3.0.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.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +39 -0
- package/README.md +8 -1
- package/dist/flow.d.ts +68 -4
- package/dist/flow.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/style-family.d.ts +8 -0
- package/dist/style-family.d.ts.map +1 -0
- package/dist/style-family.js +27 -0
- package/docs/features/custom-presets-and-templates.md +428 -0
- package/docs/features/style-families.md +217 -0
- package/docs/fiber-concepts.md +25 -12
- package/docs/schema/flow-data-schema.md +2 -2
- package/docs/schema/flow-quick-reference.md +3 -4
- package/docs/schema/flow-schema.json +83 -10
- package/docs/schema/flow-schema.md +93 -30
- package/package.json +1 -1
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Custom Presets & Templates (FBT)
|
|
3
|
+
applies-to:
|
|
4
|
+
- "@sonata-innovations/fiber-fbt@^3.0"
|
|
5
|
+
- "@sonata-innovations/fiber-types@^3.0"
|
|
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.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Style Families & Advance Behaviors
|
|
3
|
+
applies-to:
|
|
4
|
+
- "@sonata-innovations/fiber-fbre@^4.0"
|
|
5
|
+
- "@sonata-innovations/fiber-types@^3.0"
|
|
6
|
+
- "@sonata-innovations/fiber-fbt@^3.0"
|
|
7
|
+
- "@sonata-innovations/fiber-fbtl@^3.0"
|
|
8
|
+
- "@sonata-innovations/fiber-theme-editor@^2.0"
|
|
9
|
+
read-when: "Choosing a form style, understanding the focused presentation, or turning auto-advance / Enter-to-advance on and off. Also: migrating off config.mode."
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<!-- Generated from the Fiber repo's docs/ tree by project/scripts/sync-package-docs.mjs. Do not edit here. -->
|
|
13
|
+
# Style Families & Advance Behaviors
|
|
14
|
+
|
|
15
|
+
A Fiber form's presentation is decided by one setting — `config.theme.style` —
|
|
16
|
+
and its two advance behaviors by two more — `config.navigation.autoAdvance` and
|
|
17
|
+
`config.navigation.advanceOnEnter`. That is the whole model. There is no
|
|
18
|
+
presentation mode.
|
|
19
|
+
|
|
20
|
+
> **Migrating from an earlier version?** `config.mode` (`"standard"` /
|
|
21
|
+
> `"conversational"`) was removed. Jump to [Migrating off `config.mode`](#migrating-off-configmode).
|
|
22
|
+
|
|
23
|
+
## The ten styles are one flat vocabulary
|
|
24
|
+
|
|
25
|
+
Any style is valid on any flow. Four of them share an extra presentation
|
|
26
|
+
treatment, and that shared half is called a **family**:
|
|
27
|
+
|
|
28
|
+
| Family | Styles |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `"form"` | `clean`, `outlined`, `refined-clean`, `airy-clean`, `soft-outlined`, `defined-outlined` |
|
|
31
|
+
| `"focused"` | `centered-minimal`, `stacked-cards`, `soft-float`, `bold-statement` |
|
|
32
|
+
|
|
33
|
+
On top of whichever of the four you pick, the focused family adds:
|
|
34
|
+
|
|
35
|
+
| Treatment | What it does |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| **Vertical centering** | Content sits centered in a narrow column, falling back to top-aligned and scrollable when a screen is taller than the viewport |
|
|
38
|
+
| **Animated entry** | Components fade and scale in with staggered delays. Respects `prefers-reduced-motion` |
|
|
39
|
+
| **Larger tap targets** | Option items, Yes/No buttons, card-select cards and inputs are enlarged |
|
|
40
|
+
| **Bolder type** | Headers, labels, prompts and inputs step up in size |
|
|
41
|
+
|
|
42
|
+
Each of the four styles then applies its own look — underline inputs, filled
|
|
43
|
+
cards, pill options, heavy borders — exactly as each of the six form styles
|
|
44
|
+
does.
|
|
45
|
+
|
|
46
|
+
### The family is derived, never authored
|
|
47
|
+
|
|
48
|
+
It does not appear in Flow JSON. FBRE computes it from `theme.style` and emits
|
|
49
|
+
it on the container as `data-style-family`, next to `data-style`:
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<div class="fbre-container" data-style="soft-float" data-style-family="focused">
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That is what lets the ~32 shared rules live under one selector instead of a
|
|
56
|
+
repeated four-way list. If you write custom CSS against a focused form, target
|
|
57
|
+
`[data-style-family="focused"]` for anything that should apply to all four and
|
|
58
|
+
`[data-style="…"]` for one style.
|
|
59
|
+
|
|
60
|
+
`fiber-types` (and `fiber-fbre`, which re-exports it) publishes the derivation
|
|
61
|
+
so a builder can group its own style picker from one list:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { FOCUSED_STYLES, styleFamily } from "@sonata-innovations/fiber-types";
|
|
65
|
+
|
|
66
|
+
styleFamily("bold-statement"); // "focused"
|
|
67
|
+
styleFamily("clean"); // "form"
|
|
68
|
+
styleFamily(undefined); // "form" — the `clean` default
|
|
69
|
+
|
|
70
|
+
FOCUSED_STYLES.has("soft-float"); // true
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Why this matters for server-driven rendering
|
|
74
|
+
|
|
75
|
+
The session protocol sends `config.theme`. Because presentation is *derived
|
|
76
|
+
from* `theme.style`, a focused-styled flow renders identically whether it is
|
|
77
|
+
rendered locally, fetched remotely, or served one screen at a time by a
|
|
78
|
+
session — no extra field has to be carried, and none can be forgotten.
|
|
79
|
+
|
|
80
|
+
## Advance behaviors
|
|
81
|
+
|
|
82
|
+
Auto-advance and Enter-to-advance are independent `navigation` flags. Neither is
|
|
83
|
+
tied to a style: a `clean` form with one question per screen advances on Enter,
|
|
84
|
+
and a `bold-statement` flow only auto-advances if it asks to.
|
|
85
|
+
|
|
86
|
+
| Flag | Default | Behavior |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| `navigation.autoAdvance` | `false` | Advance to the next screen ~500ms after a single-select choice — `radio`, `yesNo`, `cardSelect`, `dropDown`. Multi-select (`checkbox`, `dropDownMulti`) never auto-advances |
|
|
89
|
+
| `navigation.advanceOnEnter` | `true` | Advance when Enter is pressed in an `inputText` or `inputNumber`. `inputTextArea` is excluded — Enter inserts a newline there |
|
|
90
|
+
|
|
91
|
+
```jsonc
|
|
92
|
+
{
|
|
93
|
+
"config": {
|
|
94
|
+
"theme": { "style": "centered-minimal" },
|
|
95
|
+
"navigation": {
|
|
96
|
+
"transition": "scaleFade",
|
|
97
|
+
"autoAdvance": true,
|
|
98
|
+
"advanceOnEnter": true
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The defaults are deliberately asymmetric. A form that moves without a click is
|
|
105
|
+
surprising, and shifting content and focus ~500ms after a selection carries a
|
|
106
|
+
real accessibility cost — so `autoAdvance` is opt-in. Enter-to-advance is an
|
|
107
|
+
ordinary form convention, so it is on.
|
|
108
|
+
|
|
109
|
+
### Guards
|
|
110
|
+
|
|
111
|
+
Both behaviors share the same guards. None of them are configurable — they are
|
|
112
|
+
correctness guards, not policy:
|
|
113
|
+
|
|
114
|
+
- Neither fires on a screen with **more than one visible input**. A shared
|
|
115
|
+
screen behaves like a normal form, so components after the first are never
|
|
116
|
+
skipped. (Conditionally hidden inputs don't count toward the total.)
|
|
117
|
+
- Neither fires on the last screen — so Enter can never submit the form.
|
|
118
|
+
- Neither fires when the screen fails validation.
|
|
119
|
+
- Both respect condition-hidden screens, skipping to the next reachable one.
|
|
120
|
+
- Auto-advance does not fire during an active screen transition.
|
|
121
|
+
- Enter-to-advance stands down inside an open popup (`.fbre-popup`), so Enter
|
|
122
|
+
there commits the value rather than skipping the screen. Today this affects
|
|
123
|
+
one component: the colour picker's hex field is the only `<input>` rendered
|
|
124
|
+
inside an overlay. The date/time pickers and both dropdowns use buttons and
|
|
125
|
+
`role="option"` elements, which Enter-to-advance already ignores.
|
|
126
|
+
|
|
127
|
+
The single-visible-input guard is worth designing around: turning `autoAdvance`
|
|
128
|
+
on for a flow whose screens hold several questions each does nothing at all.
|
|
129
|
+
Both builders say so — FBT warns on a multi-input screen tab, FBTL notes it on
|
|
130
|
+
the divider that merges two cards onto one screen.
|
|
131
|
+
|
|
132
|
+
## In the builders
|
|
133
|
+
|
|
134
|
+
Neither builder stores a mode. Each offers a preset that writes the settings the
|
|
135
|
+
focused look is made of, after which every one of them stays individually
|
|
136
|
+
editable:
|
|
137
|
+
|
|
138
|
+
- **FBT** — a *Focused presentation* preset in the Appearance section writes
|
|
139
|
+
`theme.style: "centered-minimal"` and `navigation: { transition: "scaleFade",
|
|
140
|
+
autoAdvance: true }`. The style picker lists all ten styles, captioned *Form*
|
|
141
|
+
and *Focused*.
|
|
142
|
+
- **FBTL** — its default config already uses `centered-minimal` with
|
|
143
|
+
`autoAdvance: true`. Pacing (one question per screen vs all on one page) is a
|
|
144
|
+
separate concern there: see `screenModel` in the
|
|
145
|
+
[FBTL Integration Guide](@sonata-innovations/fiber-fbtl/docs/integration/fbtl.md#screen-model).
|
|
146
|
+
- **Theme Editor** — the style dropdown lists all ten, grouped under *Form* and
|
|
147
|
+
*Focused* headings. Its value is `{ theme }`.
|
|
148
|
+
|
|
149
|
+
## Migrating off `config.mode`
|
|
150
|
+
|
|
151
|
+
`FlowConfiguration.mode`, the `FlowModeType` type, and FBRE's `mode` prop are
|
|
152
|
+
gone. Each of `mode`'s jobs moved somewhere it belongs:
|
|
153
|
+
|
|
154
|
+
| Was | Now |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| `mode: "conversational"` for the centered, animated presentation | the focused **style** already on the flow — nothing to set |
|
|
157
|
+
| `mode: "conversational"` for auto-advance | `navigation.autoAdvance: true` |
|
|
158
|
+
| `mode: "conversational"` for Enter-to-advance | `navigation.advanceOnEnter` — on by default now, for every flow |
|
|
159
|
+
| `mode` deciding which styles were legal | nothing — the vocabulary is flat |
|
|
160
|
+
|
|
161
|
+
### Updating a stored flow
|
|
162
|
+
|
|
163
|
+
A flow that had `mode: "conversational"` already carried a focused
|
|
164
|
+
`theme.style`, so **its look survives untouched**. The only behavior that
|
|
165
|
+
changes is auto-advance, which is now off unless asked for:
|
|
166
|
+
|
|
167
|
+
```jsonc
|
|
168
|
+
// before
|
|
169
|
+
{ "config": {
|
|
170
|
+
"mode": "conversational",
|
|
171
|
+
"theme": { "style": "centered-minimal" },
|
|
172
|
+
"navigation": { "transition": "scaleFade" }
|
|
173
|
+
} }
|
|
174
|
+
|
|
175
|
+
// after
|
|
176
|
+
{ "config": {
|
|
177
|
+
"theme": { "style": "centered-minimal" },
|
|
178
|
+
"navigation": { "transition": "scaleFade", "autoAdvance": true }
|
|
179
|
+
} }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
A flow that had `mode: "standard"` (or no `mode` at all) needs only the field
|
|
183
|
+
deleted. It gains Enter-to-advance on its one-question screens, which was
|
|
184
|
+
previously withheld for no reason anyone could see.
|
|
185
|
+
|
|
186
|
+
`mode` is not read anywhere any more, so leaving a stale `"mode"` key in stored
|
|
187
|
+
JSON is inert rather than harmful — but the schema no longer describes it, and
|
|
188
|
+
`npm run validate` will not vouch for it.
|
|
189
|
+
|
|
190
|
+
### Updating code
|
|
191
|
+
|
|
192
|
+
```diff
|
|
193
|
+
-<FBRE flow={flow} mode="conversational" onFlowComplete={done} />
|
|
194
|
+
+<FBRE
|
|
195
|
+
+ flow={flow}
|
|
196
|
+
+ theme={{ style: "centered-minimal" }}
|
|
197
|
+
+ navigation={{ autoAdvance: true }}
|
|
198
|
+
+ onFlowComplete={done}
|
|
199
|
+
+/>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```diff
|
|
203
|
+
-<ThemeEditor mode={value.mode} theme={value.theme} onChange={setValue} />
|
|
204
|
+
+<ThemeEditor theme={value.theme} onChange={setValue} />
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The theme editor's `STANDARD_STYLE_OPTIONS`, `CONVERSATIONAL_STYLE_OPTIONS`,
|
|
208
|
+
`styleOptionsForMode` and `reconcileStyle` exports are replaced by a single
|
|
209
|
+
`STYLE_OPTIONS` array; use `styleFamily()` from `fiber-types` if you need the
|
|
210
|
+
partition.
|
|
211
|
+
|
|
212
|
+
## Related
|
|
213
|
+
|
|
214
|
+
- [Flow Schema → Style Families](../schema/flow-schema.md#style-families)
|
|
215
|
+
- [FBRE Theming Guide](@sonata-innovations/fiber-fbre/docs/features/fbre-theming.md) — palette tokens, brand fonts, and how a
|
|
216
|
+
style's preset interacts with the knobs
|
|
217
|
+
- [FBRE Integration Guide → Focused Presentation](@sonata-innovations/fiber-fbre/docs/integration/fbre.md#focused-presentation)
|