npm package: @sonata-innovations/fiber-fbre
Description: Fiber Render Engine — renders Flow JSON forms with conditional logic, validation, and screen transitions
Latest version: 2.0.4

# @sonata-innovations/fiber-fbre v2

Fiber Render Engine — consumes Flow JSON and renders data collection forms. Handles conditional logic, validation, screen transitions, and outputs collected FlowData back to the parent application.

## Quick Start

### Local Mode

Pass a Flow object directly:

```tsx
import { FBRE } from "@sonata-innovations/fiber-fbre";
import "@sonata-innovations/fiber-fbre/styles";

function App() {
  return (
    <FBRE
      flow={myFlow}
      screenIndex={0}
      onFlowComplete={(data) => console.log(data)}
    />
  );
}
```

### Remote Mode

Fetch a published flow from a Fiber API:

```tsx
import { FBRE } from "@sonata-innovations/fiber-fbre";
import "@sonata-innovations/fiber-fbre/styles";

function App() {
  return (
    <FBRE
      flowId="your-flow-id"
      apiEndpoint="https://your-api.example.com/api/v1"
      apiKey="your-api-key"
      onFlowComplete={(data) => console.log(data)}
    />
  );
}
```

### Server-Driven Mode

Session-based rendering where the server evaluates conditions and validation:

```tsx
import { FBRE } from "@sonata-innovations/fiber-fbre";
import "@sonata-innovations/fiber-fbre/styles";

function App() {
  return (
    <FBRE
      sessionEndpoint="https://your-api.example.com/api/v1/public/sessions"
      flowId="your-flow-id"
      onFlowComplete={(data) => console.log(data)}
    />
  );
}
```

## Installation

```bash
npm install @sonata-innovations/fiber-fbre
```

Then import the CSS in your app entry point:

```ts
import "@sonata-innovations/fiber-fbre/styles";
```

**Peer dependencies:** `react ^18.0.0 || ^19.0.0`, `react-dom ^18.0.0 || ^19.0.0`

**Runtime dependencies:** `zustand`, `@sonata-innovations/fiber-types`, `@sonata-innovations/fiber-shared`

## API

### `<FBRE />` Props

FBRE supports three rendering modes, selected by which props you pass. The modes are mutually exclusive.

#### Local Mode

Render a Flow object you already have in memory.

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `flow` | `Flow` | Yes | Flow JSON object |
| `data` | `FlowData` | No | Pre-populated form data |
| `mode` | `FlowModeType` | No | Override form mode (`"standard"` \| `"conversational"`) |
| `screenIndex` | `number` | No | Initial screen index (default `0`) |
| `theme` | `ThemeConfig` | No | Override theme settings (merged over `flow.config.theme`) |
| `navigation` | `NavigationConfig` | No | Override navigation settings (merged over `flow.config.navigation`) |
| `controls` | `ControlsConfig` | No | Override controls settings (merged over `flow.config.controls`) |
| `context` | `Record<string, string \| boolean \| number>` | No | External context values for condition evaluation and calculations |
| `storeRef` | `MutableRefObject<StoreApi<FBREStoreState> \| null>` | No | Ref to access the Zustand store |
| `onFlowComplete` | `(data: FlowData) => void` | Yes | Called when the user completes the flow |
| `onScreenChange` | `(index: number, data: FlowData) => void` | No | Called on screen navigation |
| `onScreenValidationChange` | `(index: number, data: any) => void` | No | Called when screen validity changes |

#### Remote Mode

Fetch a published flow from a Fiber API by ID.

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `flowId` | `string` | Yes | ID of the published flow |
| `apiEndpoint` | `string` | Yes | Base URL of the Fiber API |
| `apiKey` | `string` | No | API key for authentication |
| `data` | `FlowData` | No | Pre-populated form data |
| `mode` | `FlowModeType` | No | Override form mode (`"standard"` \| `"conversational"`) |
| `screenIndex` | `number` | No | Initial screen index (default `0`) |
| `theme` | `ThemeConfig` | No | Override theme settings |
| `navigation` | `NavigationConfig` | No | Override navigation settings |
| `controls` | `ControlsConfig` | No | Override controls settings |
| `context` | `Record<string, string \| boolean \| number>` | No | External context values for condition evaluation and calculations |
| `storeRef` | `MutableRefObject<StoreApi<FBREStoreState> \| null>` | No | Ref to access the Zustand store |
| `onFlowComplete` | `(data: FlowData) => void` | Yes | Called when the user completes the flow |
| `onScreenChange` | `(index: number, data: FlowData) => void` | No | Called on screen navigation |
| `onScreenValidationChange` | `(index: number, data: any) => void` | No | Called when screen validity changes |

#### Server-Driven Mode

Session-based rendering. The server evaluates conditions and validation; the client renders one screen at a time.

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `sessionEndpoint` | `string` | Yes | Session API base URL |
| `flowId` | `string` | Yes | ID of the flow to start a session for |
| `apiKey` | `string` | No | API key for authentication |
| `mode` | `FlowModeType` | No | Override form mode (`"standard"` \| `"conversational"`) |
| `theme` | `ThemeConfig` | No | Override theme settings |
| `context` | `Record<string, string \| boolean \| number>` | No | External context values for condition evaluation and calculations |
| `onFlowComplete` | `(data: FlowData) => void` | Yes | Called when the user completes the flow |
| `onScreenChange` | `(screenNumber: number) => void` | No | Called on screen navigation (note: receives screen number, not index + data) |

### Imperative Access

Access the store directly for programmatic control:

```tsx
import { useFBREStore, useFBREStoreApi, useFBREApi } from "@sonata-innovations/fiber-fbre";

// Inside a child of <FBRE />
const flowData = useFBREStore((s) => s.getFlowData());
const storeApi = useFBREStoreApi();
const data = storeApi.getState().getFlowData();

// In remote mode — access API config, flowId, tenantId
const apiContext = useFBREApi();
```

### Events

```tsx
import { addFBREEventListener, removeFBREEventListener } from "@sonata-innovations/fiber-fbre";

const handler = (id, data) => console.log("file uploaded:", id, data);
addFBREEventListener("file-upload", handler);
removeFBREEventListener("file-upload", handler);
```

### Exports

```ts
// Components
import { FBRE } from "@sonata-innovations/fiber-fbre";

// Store hooks
import { useFBREStore, useFBREStoreApi, useFBREApi } from "@sonata-innovations/fiber-fbre";

// Event system
import { addFBREEventListener, removeFBREEventListener } from "@sonata-innovations/fiber-fbre";

// Error classes
import { ApiError, TimeoutError } from "@sonata-innovations/fiber-fbre";

// Types
import type {
  FBREProps,
  FBRELocalProps,
  FBRERemoteProps,
  FBREServerDrivenModeProps,
  FBREStoreState,
  FBREApiConfig,
  Flow,
  FlowScreen,
  FlowConfiguration,
  ThemeConfig,
  NavigationConfig,
  ControlsConfig,
  FlowMetadata,
  Component,
  ComponentProperties,
  ComponentDisplayProperties,
  ComponentDividerProperties,
  ComponentInputProperties,
  ComponentOptionProperties,
  ComponentSliderProperties,
  ComponentSwitchProperties,
  ComponentRatingProperties,
  ComponentFileUploadProperties,
  ComponentGroupProperties,
  FlowData,
  ScreenData,
  ComponentData,
  FileUploadData,
  FileUploadBase64Data,
  FileUploadS3Data,
  FlowConditionConfig,
  ConditionOperator,
  ConditionRule,
  ConditionGroup,
  ConditionDependency,
} from "@sonata-innovations/fiber-fbre";
```

## Flow JSON Schema

```
Flow
├── uuid: string
├── metadata: { name?, description?, ...}
├── config?: { mode?, theme?: { color?, darkMode?, style? }, navigation?: { transition?, allowInvalidTransition? }, controls?: { show?, layout?, showStepper? }, summary? }
└── screens: FlowScreen[]
    ├── uuid: string
    ├── label?: string
    ├── conditions?: FlowConditionConfig
    └── components: Component[]
        ├── uuid: string
        ├── type: string
        ├── properties: { ... }
        ├── conditions?: FlowConditionConfig
        └── components?: Component[]  (groups and repeaters)
```

### Component Types

| Type | Category | Description |
|------|----------|-------------|
| `header` | Display | Heading text |
| `text` | Display | Rich text with markup support |
| `divider` | Display | Horizontal rule with optional label |
| `callout` | Display | Styled alert/info box with variant coloring (info, success, warning, neutral) |
| `table` | Display | Static comparison/data table with optional column highlighting |
| `inputText` | Input | Single-line text field |
| `inputTextArea` | Input | Multi-line text field |
| `inputNumber` | Input | Numeric input (supports decimal restriction and start adornment) |
| `dropDown` | Selection | Native select dropdown |
| `dropDownMulti` | Selection | Multi-select with tag chips |
| `checkbox` | Selection | Checkbox group |
| `radio` | Selection | Radio button group |
| `toggleSwitch` | Selection | On/off toggle |
| `yesNo` | Selection | Two large tappable buttons for binary yes/no |
| `cardSelect` | Selection | Card-based visual selection with image/icon support |
| `date` | Date & Time | Calendar popup date picker |
| `time` | Date & Time | Hour/minute time selector |
| `dateTime` | Date & Time | Combined date + time picker |
| `dateRange` | Date & Time | Two date pickers for start/end |
| `timeRange` | Date & Time | Two time pickers for start/end |
| `dateTimeRange` | Date & Time | Two datetime pickers for start/end |
| `fileUpload` | Interactive | File picker with size validation |
| `rating` | Interactive | Star rating with half-star precision |
| `slider` | Interactive | Range slider with value label |
| `colorPicker` | Interactive | Saturation/hue picker with hex input |
| `computed` | Display | Calculated field with formula-based computed values |
| `signature` | Interactive | Signature pad — draw on canvas, type name, or both |
| `group` | Container | Component container with layout grid, collapsible |
| `repeater` | Container | Iterable component container (add/remove iterations, pre-populated rows) |

### Conditions

Components and screens can be conditionally shown/hidden using `FlowConditionConfig`:

```json
{
  "action": "show",
  "when": {
    "logic": "and",
    "rules": [
      {
        "source": "other-component-uuid",
        "operator": "equals",
        "value": "yes"
      }
    ]
  }
}
```

**Logic:** `"and"` (all rules must match) or `"or"` (any rule matches).

**Context rules:** Set `sourceType: "context"` on a rule to reference an external context value instead of a component. In this case, `source` is a context key (not a component UUID).

**Operators:**

| Category | Operators |
|----------|-----------|
| Equality | `equals`, `notEquals` |
| String | `contains`, `notContains`, `startsWith`, `endsWith` |
| Presence | `isEmpty`, `isNotEmpty` |
| Numeric | `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual` |
| Set (single) | `isOneOf`, `isNotOneOf` |
| Set (multi) | `includesAny`, `includesAll`, `includesNone` |
| Boolean | `isTrue`, `isFalse` |

Condition results are stored separately from the Flow JSON in a `conditionResults` map (keyed by target UUID). Hidden components are excluded from FlowData output.

### Validation

Components support a rules-based validation system via `properties.validation`:

```json
{
  "validation": {
    "rules": [
      { "type": "required" },
      { "type": "email" },
      { "type": "minLength", "params": { "min": 5 } }
    ]
  }
}
```

**17 built-in validators:** `required`, `email`, `phone`, `url`, `minLength`, `maxLength`, `exactLength`, `minValue`, `maxValue`, `pattern`, `minSelected`, `maxSelected`, `fileType`, `fileSize`, `contains`, `excludes`, `matchesField`

Each validator has a default error message. Custom messages can be set per rule via the `message` field.

**Error display:** The first failing message is shown below the field with an "(and N more)" count when multiple rules fail.

**Cross-field validation:** `matchesField` enables bidirectional re-evaluation — changing either field re-validates the other.

**Migration:** Legacy `required` and `regex` flat properties are automatically converted to validation rules on load.

Validation errors are stored in `validationErrors: Record<string, string[]>` on the store (keyed by component UUID). Hidden components (by conditions) do not block screen validity.

### Calculations

Flows can include formula-based computed values via `FlowCalculation`:

```json
{
  "calculations": [
    {
      "uuid": "calc-uuid",
      "formula": "{{comp-a-uuid}} + {{comp-b-uuid}} * 0.1",
      "label": "Total"
    }
  ]
}
```

- Formulas reference component values via `{uuid}` tokens and support standard arithmetic (`+`, `-`, `*`, `/`)
- Aggregation functions: `SUM()`, `COUNT()`, `AVG()`, `MIN()`, `MAX()` over repeater iterations
- Scalar `MIN(expr, ...)` / `MAX(expr, ...)` for capping values (e.g. `MIN({discount}, 25)`)
- `IF(condition, then, else)` for conditional formulas (e.g. tiered pricing)
- Comparison operators: `>`, `<`, `>=`, `<=`, `==`, `!=` (return 1/0)
- Option metadata is accessible for option-based components
- Results update reactively when source values change
- Access results via `getCalculationResult(uuid)` on the store

### Markup

Text components and `detail` fields support inline markup:

- `[b]bold[/b]` → **bold**
- `[i]italic[/i]` → *italic*
- `[l href="url"]link[/l]` → [link](url)

## Conversational Mode

Set `mode: "conversational"` to transform FBRE into a one-question-per-screen experience:

```tsx
// Via JSX prop
<FBRE flow={myFlow} mode="conversational" onFlowComplete={handleComplete} />

// Via flow config
const flow = {
  ...myFlow,
  config: { ...myFlow.config, mode: "conversational" }
};
<FBRE flow={flow} onFlowComplete={handleComplete} />
```

**What changes:**

- Content is vertically and horizontally centered
- Single-select components (`radio`, `yesNo`, `cardSelect`, `dropDown`) auto-advance ~500ms after selection
- Pressing Enter on `inputText` / `inputNumber` advances to the next screen
- Components animate in with fade + scale + stagger on screen transitions (respects `prefers-reduced-motion`)
- Option items, Yes/No buttons, card-select cards, and input fields are enlarged for easier tapping
- Each of the 6 existing styles has a tailored conversational presentation (e.g. clean = bordered cards, soft-outlined = pill-shaped options, defined-outlined = inverted selection)

**What doesn't change:**

- FlowData output is identical
- Conditions, validation, calculations all work the same
- Navigation buttons still work alongside auto-advance
- Works with all 6 transition types and dark mode

## Theming

FBRE uses CSS custom properties. Override on the container:

```css
.fbre-container {
  --fbre-theme-color: #1976d2;
  --fbre-theme-light: #e3f2fd;
  --fbre-theme-dark: #1565c0;
  --fbre-error: #d32f2f;
  --fbre-text: #212121;
  --fbre-text-secondary: #666;
  --fbre-border: #ccc;
  --fbre-bg: #fff;
  --fbre-radius: 4px;
  --fbre-font: "Segoe UI", system-ui, sans-serif;
}
```

`theme.color` from `flow.config.theme.color` is applied automatically via inline style.

### Dark Mode

Enable dark mode via the `theme` prop or config:

```tsx
// Via theme prop (takes precedence)
<FBRE flow={myFlow} theme={{ darkMode: true }} onFlowComplete={handleComplete} />

// Via flow config
const flow = {
  ...myFlow,
  config: { ...myFlow.config, theme: { darkMode: true } }
};
<FBRE flow={flow} onFlowComplete={handleComplete} />
```

Dark mode applies `data-mode="dark"` on the container and overrides all CSS custom properties with a dark palette. Custom `theme.color` values are preserved — the inline style takes precedence over the dark mode default.

### Screen Transitions

Animate screen changes by setting `navigation.transition` in the flow config:

```json
{
  "config": {
    "navigation": { "transition": "slideFade" }
  }
}
```

| Type | Effect | Direction-aware? |
|------|--------|-----------------|
| `"none"` | Instant swap (default) | — |
| `"slide"` | Full horizontal slide | Yes |
| `"fade"` | Crossfade | No |
| `"slideFade"` | 30px slide + opacity | Yes |
| `"rise"` | Vertical rise/sink | Yes |
| `"scaleFade"` | Scale + opacity | No |

Direction-aware transitions reverse when navigating backward. Buttons are disabled during animation to prevent overlapping transitions.

Customize timing via CSS custom properties:

```css
.fbre-container {
  --fbre-transition-duration: 250ms;
  --fbre-transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
}
```

`@media (prefers-reduced-motion: reduce)` sets duration to `0ms` automatically.

## Architecture

| Concern | Implementation |
|---------|---------------|
| UI framework | Zero-dependency custom CSS |
| Styling | CSS custom properties + BEM (`fbre-` prefix) |
| Display detection | `isDisplayComponent()` checks type against constant set (`header`, `text`, `divider`, `callout`, `table`) |
| State | Zustand store factory, one store per `<FBRE />` instance via React Context |
| Re-renders | Zustand selectors — granular, automatic |
| Build | Vite library mode |
| Components | Registry pattern (`Map<string, FC>`), one file per type |
| Dependencies | react, zustand, @sonata-innovations/fiber-types, @sonata-innovations/fiber-shared |

## FlowData Output

`getFlowData()` returns collected form data. Display-only components (`header`, `text`, `divider`, `callout`, `table`) are excluded automatically based on their type.

```
FlowData
├── uuid: string
├── metadata: Record<string, string>
└── screens: ScreenData[]
    ├── uuid: string
    ├── label?: string
    └── components: ComponentData[]
        ├── uuid: string
        ├── type: string
        ├── label?: string          (omitted when empty)
        ├── value: string | string[] | number | number[] | boolean | null | FileUploadData
        └── components?: ComponentData[][]  (groups and repeaters)
```

## Build

```bash
npm run build
```

Output:

```
dist/
├── index.d.ts        # Bundled type declarations
├── fiber-fbre.js     # ES module
├── fiber-fbre.cjs    # CommonJS
└── fiber-fbre.css    # All styles
```

## Responsive Sizing

FBRE uses CSS container queries to automatically scale controls in narrow containers (under 400px). Buttons, stepper dots, and toggle switches shrink proportionally. No configuration needed — just embed in a smaller container and the compact sizing kicks in.

## Multiple Instances

Each `<FBRE />` creates its own isolated Zustand store. Multiple instances can run simultaneously on the same page without state conflicts.

```tsx
<FBRE flow={flowA} onFlowComplete={handleA} />
<FBRE flow={flowB} onFlowComplete={handleB} />
```