{"id":46747596,"url":"https://github.com/bghcore/form-engine","last_synced_at":"2026-03-12T23:01:17.810Z","repository":{"id":342700979,"uuid":"1174715588","full_name":"bghcore/form-engine","owner":"bghcore","description":null,"archived":false,"fork":false,"pushed_at":"2026-03-11T21:57:57.000Z","size":1702,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-12T01:11:17.835Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bghcore.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING_ADAPTER.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-06T18:58:07.000Z","updated_at":"2026-03-11T21:21:13.000Z","dependencies_parsed_at":null,"dependency_job_id":"492791a7-7278-4f93-95bc-4ebba2860ab0","html_url":"https://github.com/bghcore/form-engine","commit_stats":null,"previous_names":["bghcore/form-engine"],"tags_count":147,"template":false,"template_full_name":null,"purl":"pkg:github/bghcore/form-engine","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bghcore%2Fform-engine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bghcore%2Fform-engine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bghcore%2Fform-engine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bghcore%2Fform-engine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bghcore","download_url":"https://codeload.github.com/bghcore/form-engine/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bghcore%2Fform-engine/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30448566,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-12T21:31:01.033Z","status":"ssl_error","status_checked_at":"2026-03-12T21:30:43.161Z","response_time":114,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-03-09T20:11:56.419Z","updated_at":"2026-03-12T23:01:17.770Z","avatar_url":"https://github.com/bghcore.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Form Engine\n\n[![npm core](https://img.shields.io/npm/v/@form-eng/core?label=core)](https://www.npmjs.com/package/@form-eng/core)\n[![npm fluent](https://img.shields.io/npm/v/@form-eng/fluent?label=fluent)](https://www.npmjs.com/package/@form-eng/fluent)\n[![npm mui](https://img.shields.io/npm/v/@form-eng/mui?label=mui)](https://www.npmjs.com/package/@form-eng/mui)\n[![npm headless](https://img.shields.io/npm/v/@form-eng/headless?label=headless)](https://www.npmjs.com/package/@form-eng/headless)\n[![npm designer](https://img.shields.io/npm/v/@form-eng/designer?label=designer)](https://www.npmjs.com/package/@form-eng/designer)\n[![CI](https://github.com/bghcore/form-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/bghcore/form-engine/actions/workflows/ci.yml)\n\n[Storybook](https://bghcore.github.io/form-engine/storybook/) | [Designer Demo](https://bghcore.github.io/form-engine/designer/) | [npm](https://www.npmjs.com/org/form-engine)\n\nA React library for rendering complex, configuration-driven forms with a built-in rules engine. Define your forms as a single `IFormConfig` JSON object -- field definitions, declarative rules with rich conditions, validation, ordering -- and the library handles rendering, validation, auto-save, and field interactions automatically.\n\n## When to Use This Library\n\n**Use this if you need:**\n- Forms defined as JSON/config objects, not JSX -- field types, labels, validations, and rules declared as data\n- A rules engine where field A changing to value X makes field B required, field C hidden, and field D's dropdown options change -- all declared, not coded\n- Multi-step wizards with conditional step visibility and cross-step rules\n- Auto-save with debounce, retry, and abort -- not just \"submit on click\"\n- To swap UI libraries (Fluent UI, MUI, headless HTML, custom) without rewriting form logic\n- A visual drag-and-drop form builder for non-technical users\n\n**Don't use this if you need:**\n- Simple forms with 3-5 static fields -- use react-hook-form directly\n- Pure JSON Schema rendering with no rules engine -- use [RJSF](https://github.com/rjsf-team/react-jsonschema-form) (but if you want RJSF's schema format with our rules engine, use `fromRjsfSchema()` to migrate)\n- Headless form state with zero opinions -- use [TanStack Form](https://tanstack.com/form)\n\n## Packages\n\n| Package | Description | Size |\n|---------|-------------|------|\n| [`@form-eng/core`](./packages/core) | UI-agnostic rules engine, form orchestration, validation, analytics, devtools. React + react-hook-form only, no UI library dependency. | ~114 KB ESM |\n| [`@form-eng/fluent`](./packages/fluent) | Fluent UI v9 field components (28 field types). | ~39 KB ESM |\n| [`@form-eng/mui`](./packages/mui) | Material UI field components (28 field types). | ~39 KB ESM |\n| [`@form-eng/headless`](./packages/headless) | Unstyled semantic HTML field components (28 field types). | ~36 KB ESM |\n| [`@form-eng/designer`](./packages/designer) | Visual drag-and-drop form builder with rule editor and JSON export. | ~65 KB ESM |\n| [`@form-eng/examples`](./packages/examples) | 3 example apps (login+MFA, checkout wizard, data entry). | -- |\n\n## Quick Start\n\n```bash\n# With Fluent UI\nnpm install @form-eng/core @form-eng/fluent\n\n# Or with MUI\nnpm install @form-eng/core @form-eng/mui @mui/material @emotion/react @emotion/styled\n\n# Or headless (no UI framework)\nnpm install @form-eng/core @form-eng/headless\n```\n\n```tsx\nimport {\n  RulesEngineProvider,\n  InjectedFieldProvider,\n  FormEngine,\n} from \"@form-eng/core\";\nimport { createFluentFieldRegistry } from \"@form-eng/fluent\";\n// Or: import { createMuiFieldRegistry } from \"@form-eng/mui\";\n// Or: import { createHeadlessFieldRegistry } from \"@form-eng/headless\";\n\nconst formConfig = {\n  version: 2 as const,\n  fields: {\n    name: { type: \"Textbox\", label: \"Name\", required: true },\n    status: {\n      type: \"Dropdown\",\n      label: \"Status\",\n      options: [\n        { value: \"Active\", label: \"Active\" },\n        { value: \"Inactive\", label: \"Inactive\" },\n      ],\n    },\n    notes: { type: \"Textarea\", label: \"Notes\" },\n  },\n  fieldOrder: [\"name\", \"status\", \"notes\"],\n};\n\nfunction App() {\n  return (\n    \u003cRulesEngineProvider\u003e\n      \u003cInjectedFieldProvider injectedFields={createFluentFieldRegistry()}\u003e\n        \u003cFormEngine\n          configName=\"myForm\"\n          programName=\"myApp\"\n          formConfig={formConfig}\n          defaultValues={{ name: \"\", status: \"Active\", notes: \"\" }}\n          saveData={async (data) =\u003e {\n            console.log(\"Saving:\", data);\n            return data;\n          }}\n        /\u003e\n      \u003c/InjectedFieldProvider\u003e\n    \u003c/RulesEngineProvider\u003e\n  );\n}\n```\n\n## How It Works\n\n### Configuration-Driven Forms\n\nEvery form is defined by an `IFormConfig` object containing a dictionary of `IFieldConfig` entries. Each config specifies:\n\n- **`type`** -- Which field type to render (`\"Textbox\"`, `\"Dropdown\"`, `\"Toggle\"`, etc.)\n- **`label`** -- Display label\n- **`required`** / **`hidden`** / **`readOnly`** -- Default field states\n- **`rules`** -- Declarative rules with rich conditions (`when`/`then`/`else`) that change field states based on other field values\n- **`options`** -- Dropdown/select options as `{ value, label }` pairs\n- **`validate`** -- Validation rules referencing the unified validator registry\n- **`computedValue`** -- Expressions like `\"$values.qty * $values.price\"` or `\"$fn.calculateTotal()\"`\n- **`items`** -- Field array item definitions (full `IFieldConfig` per item field)\n- **`config`** -- Arbitrary metadata passed through to the field component\n\n### Business Rules Engine\n\nRules are **declarative** -- defined as `IRule[]` on each field config, not imperative code. Each rule has a `when` condition, a `then` effect, and an optional `else` effect.\n\nWhen a field value changes, the engine:\n\n1. Identifies transitively affected fields via the dependency graph\n2. Re-evaluates rules for affected fields only (incremental evaluation)\n3. Resolves conflicts via priority (higher priority rule wins)\n4. Applies effects (required, hidden, readOnly, component swap, options, validation, computed value, setValue)\n5. Dispatches to the rules engine reducer for React re-render\n\nThe engine includes **circular dependency detection** via Kahn's algorithm and **config validation** for dev-mode diagnostics.\n\n**18 condition operators:** `equals`, `notEquals`, `greaterThan`, `lessThan`, `greaterThanOrEqual`, `lessThanOrEqual`, `contains`, `notContains`, `startsWith`, `endsWith`, `in`, `notIn`, `isEmpty`, `isNotEmpty`, `matches`, `arrayContains`, `arrayNotContains`, `arrayLength`\n\n**Logical operators:** `and`, `or`, `not` (composable condition trees)\n\n```tsx\nconst formConfig = {\n  version: 2 as const,\n  fields: {\n    type: {\n      type: \"Dropdown\",\n      label: \"Type\",\n      options: [\n        { value: \"bug\", label: \"Bug\" },\n        { value: \"feature\", label: \"Feature\" },\n      ],\n      rules: [\n        {\n          when: { field: \"type\", operator: \"equals\", value: \"bug\" },\n          then: { severity: { required: true, hidden: false } },\n          else: { severity: { hidden: true } },\n          priority: 1,\n        },\n      ],\n    },\n    severity: {\n      type: \"Dropdown\",\n      label: \"Severity\",\n      hidden: true,\n      options: [\n        { value: \"low\", label: \"Low\" },\n        { value: \"high\", label: \"High\" },\n      ],\n    },\n  },\n  fieldOrder: [\"type\", \"severity\"],\n};\n```\n\n#### Compound Conditions\n\nCombine conditions with `and`, `or`, and `not`:\n\n```tsx\nrules: [\n  {\n    when: {\n      operator: \"and\",\n      conditions: [\n        { field: \"type\", operator: \"equals\", value: \"bug\" },\n        { field: \"priority\", operator: \"greaterThanOrEqual\", value: 3 },\n      ],\n    },\n    then: { assignee: { required: true } },\n  },\n]\n```\n\n#### Computed Values\n\nUse `computedValue` with `$values`, `$fn`, and `$parent` expressions:\n\n```tsx\nfields: {\n  qty: { type: \"Number\", label: \"Quantity\" },\n  price: { type: \"Number\", label: \"Unit Price\" },\n  total: {\n    type: \"ReadOnly\",\n    label: \"Total\",\n    computedValue: \"$values.qty * $values.price\",\n  },\n  createdDate: {\n    type: \"ReadOnly\",\n    label: \"Created\",\n    computedValue: \"$fn.setDate()\",\n  },\n}\n```\n\n### Multi-Step Wizard\n\nSplit forms into wizard steps with conditional visibility and per-step validation:\n\n```tsx\nimport { WizardForm } from \"@form-eng/core\";\n\nconst formConfig = {\n  version: 2 as const,\n  fields: { /* ... */ },\n  wizard: {\n    steps: [\n      { id: \"basics\", title: \"Basic Info\", fields: [\"name\", \"type\"] },\n      {\n        id: \"details\",\n        title: \"Details\",\n        fields: [\"severity\", \"description\"],\n        visibleWhen: { field: \"type\", operator: \"equals\", value: \"bug\" },\n      },\n      { id: \"review\", title: \"Review\", fields: [\"notes\"] },\n    ],\n    validateOnStepChange: true,\n  },\n};\n\n\u003cWizardForm\n  wizardConfig={formConfig.wizard}\n  entityData={formValues}\n  renderStepContent={(fields) =\u003e \u003cFieldRenderer fields={fields} /\u003e}\n  renderStepNavigation={({ goNext, goPrev, canGoNext, canGoPrev }) =\u003e (\n    \u003cnav\u003e\n      \u003cbutton onClick={goPrev} disabled={!canGoPrev}\u003eBack\u003c/button\u003e\n      \u003cbutton onClick={goNext} disabled={!canGoNext}\u003eNext\u003c/button\u003e\n    \u003c/nav\u003e\n  )}\n/\u003e\n```\n\nAll fields stay in a single `react-hook-form` context. Steps control which fields are visible. Cross-step rules work automatically.\n\n### Field Arrays (Repeating Sections)\n\nAdd \"add another\" patterns for addresses, line items, etc.:\n\n```tsx\nimport { FieldArray } from \"@form-eng/core\";\n\n\u003cFieldArray\n  fieldName=\"contacts\"\n  config={{\n    items: {\n      name: { type: \"Textbox\", label: \"Name\", required: true },\n      email: { type: \"Textbox\", label: \"Email\", validate: [{ name: \"email\" }] },\n    },\n    minItems: 1,\n    maxItems: 5,\n    defaultItem: { name: \"\", email: \"\" },\n  }}\n  renderItem={(fieldNames, index, remove) =\u003e (\n    \u003cdiv key={index}\u003e\n      {/* fieldNames = [\"contacts.0.name\", \"contacts.0.email\"] */}\n      \u003cFieldRenderer fields={fieldNames} /\u003e\n      \u003cbutton onClick={remove}\u003eRemove\u003c/button\u003e\n    \u003c/div\u003e\n  )}\n  renderAddButton={(append, canAdd) =\u003e (\n    \u003cbutton onClick={append} disabled={!canAdd}\u003eAdd Contact\u003c/button\u003e\n  )}\n/\u003e\n```\n\n### Component Injection\n\nThe library uses a component injection system for field rendering. Core provides the orchestration, and UI packages provide the field implementations:\n\n```tsx\n// Use built-in Fluent UI fields\nimport { createFluentFieldRegistry } from \"@form-eng/fluent\";\n\n// Or use MUI fields (swap with one line)\nimport { createMuiFieldRegistry } from \"@form-eng/mui\";\n\n// Or use headless semantic HTML fields\nimport { createHeadlessFieldRegistry } from \"@form-eng/headless\";\n\n// Pass via the injectedFields prop\n\u003cInjectedFieldProvider injectedFields={createFluentFieldRegistry()}\u003e\n\n// Or mix in custom fields\n\u003cInjectedFieldProvider injectedFields={{\n  ...createFluentFieldRegistry(),\n  MyCustomField: \u003cMyCustomField /\u003e,\n}}\u003e\n```\n\n### Pluggable Validation\n\n14 built-in validators plus support for custom sync, async, and cross-field validators via the unified `registerValidators()` API:\n\n```tsx\nimport {\n  registerValidators,\n  createMinLengthValidation,\n  createPatternValidation,\n} from \"@form-eng/core\";\n\n// Register built-in factory validators\nregisterValidators({\n  MinLength5: createMinLengthValidation(5),\n  AlphaOnly: createPatternValidation(/^[a-zA-Z]+$/, \"Letters only\"),\n});\n\n// Add async validators (e.g., server-side uniqueness check)\nregisterValidators({\n  CheckUniqueEmail: async (value, entityData, signal) =\u003e {\n    const response = await fetch(`/api/check-email?email=${value}`, { signal });\n    const { exists } = await response.json();\n    return exists ? \"Email already in use\" : undefined;\n  },\n});\n```\n\nReference validators in field configs:\n\n```tsx\nfields: {\n  email: {\n    type: \"Textbox\",\n    label: \"Email\",\n    validate: [\n      { name: \"email\" },\n      { name: \"CheckUniqueEmail\", async: true, debounceMs: 500 },\n    ],\n  },\n  username: {\n    type: \"Textbox\",\n    label: \"Username\",\n    validate: [\n      { name: \"minLength\", params: { min: 3 } },\n      { name: \"AlphaOnly\" },\n    ],\n  },\n}\n```\n\nBuilt-in validators: `EmailValidation`, `PhoneNumberValidation`, `YearValidation`, `Max150KbValidation`, `Max32KbValidation`, `isValidUrl`, `NoSpecialCharactersValidation`, `CurrencyValidation`, `UniqueInArrayValidation` + factory functions: `createMinLengthValidation`, `createMaxLengthValidation`, `createNumericRangeValidation`, `createPatternValidation`, `createRequiredIfValidation`\n\nUse `registerValidatorMetadata()` to attach human-readable metadata (label, description, parameter schema) to validators for use in the visual form designer's RuleBuilder UI:\n\n```tsx\nimport { registerValidatorMetadata } from \"@form-eng/core\";\n\nregisterValidatorMetadata(\"CheckUniqueEmail\", {\n  label: \"Unique Email\",\n  description: \"Checks that the email address is not already in use\",\n});\n```\n\n### i18n / Localization\n\nAll user-facing strings are localizable:\n\n```tsx\nimport { registerLocale } from \"@form-eng/core\";\n\nregisterLocale({\n  required: \"Obligatoire\",\n  save: \"Sauvegarder\",\n  cancel: \"Annuler\",\n  saving: \"Sauvegarde en cours...\",\n  invalidEmail: \"Adresse e-mail invalide\",\n  // Partial registration -- unspecified keys fall back to English\n});\n```\n\n### Analytics and Telemetry\n\nTrack form lifecycle events via `IAnalyticsCallbacks` in form settings:\n\n```tsx\nconst formConfig: IFormConfig = {\n  version: 2,\n  fields: { /* ... */ },\n  settings: {\n    analytics: {\n      onFieldFocus: (fieldName) =\u003e console.log(\"Focus:\", fieldName),\n      onFieldBlur: (fieldName, timeSpentMs) =\u003e console.log(\"Blur:\", fieldName, timeSpentMs),\n      onFieldChange: (fieldName, oldValue, newValue) =\u003e console.log(\"Change:\", fieldName),\n      onValidationError: (fieldName, errors) =\u003e console.log(\"Validation:\", fieldName, errors),\n      onFormSubmit: (values, durationMs) =\u003e console.log(\"Submit:\", durationMs, \"ms\"),\n      onFormAbandonment: (filledFields, emptyRequired) =\u003e console.log(\"Abandoned:\", emptyRequired),\n      onWizardStepChange: (from, to) =\u003e console.log(\"Step:\", from, \"-\u003e\", to),\n      onRuleTriggered: (event) =\u003e console.log(\"Rule:\", event),\n    },\n  },\n};\n```\n\nThe `useFormAnalytics` hook wraps these callbacks into stable, memoized functions with automatic timing (field focus duration, form completion time).\n\n### FormDevTools\n\nA collapsible dev-only panel with 7 tabs for debugging form state at runtime:\n\n| Tab | Description |\n|-----|-------------|\n| **Rules** | Current runtime state of every field (type, required, hidden, readOnly, active rules) |\n| **Values** | Live JSON dump of all form values |\n| **Errors** | Current validation errors |\n| **Graph** | Text representation of the dependency graph |\n| **Perf** | Per-field render counts, hot field detection, total form renders (via `RenderTracker`) |\n| **Deps** | Sortable dependency table with effect types, cycle detection |\n| **Timeline** | Chronological event log with filtering (via `EventTimeline`) |\n\n```tsx\nimport { FormDevTools } from \"@form-eng/core\";\n\n\u003cFormDevTools\n  configName=\"myForm\"\n  formState={runtimeFormState}\n  formValues={formValues}\n  formErrors={formErrors}\n  dirtyFields={dirtyFields}\n  enabled={process.env.NODE_ENV === \"development\"}\n/\u003e\n```\n\n### Config Validation (Dev Mode)\n\nCatch configuration errors early:\n\n```tsx\nimport { validateFieldConfigs } from \"@form-eng/core\";\n\nconst errors = validateFieldConfigs(fieldConfigs, registeredComponentTypes);\n// Returns: missing dependency targets, unregistered components,\n// unregistered validators, circular dependencies, missing dropdown options\n```\n\n### Error Boundary\n\nEach field is individually wrapped in a `FormErrorBoundary` so a single field crash does not take down the entire form:\n\n```tsx\nimport { FormErrorBoundary } from \"@form-eng/core\";\n\n\u003cFormErrorBoundary\n  fallback={(error, resetErrorBoundary) =\u003e (\n    \u003cdiv\u003e\n      \u003cp\u003eField failed to render: {error.message}\u003c/p\u003e\n      \u003cbutton onClick={resetErrorBoundary}\u003eRetry\u003c/button\u003e\n    \u003c/div\u003e\n  )}\n  onError={(error, errorInfo) =\u003e console.error(\"Field error:\", error)}\n\u003e\n  \u003cMyField /\u003e\n\u003c/FormErrorBoundary\u003e\n```\n\nThis is built into the core rendering pipeline -- you do not need to add it yourself unless you want custom error handling.\n\n### Manual Save vs Auto-Save\n\nBy default, forms auto-save on every field change (debounced). Set `isManualSave={true}` for explicit save control:\n\n```tsx\n// Auto-save (default) -- saves on every field change with debounce\n\u003cFormEngine\n  configName=\"myForm\"\n  formConfig={formConfig}\n  defaultValues={defaultValues}\n  saveData={async (data) =\u003e { await api.save(data); return data; }}\n/\u003e\n\n// Manual save -- shows Save/Cancel buttons, no auto-save\n\u003cFormEngine\n  configName=\"myForm\"\n  formConfig={formConfig}\n  defaultValues={defaultValues}\n  isManualSave={true}\n  saveData={async (data) =\u003e { await api.save(data); return data; }}\n/\u003e\n\n// Manual save with custom button\n\u003cFormEngine\n  isManualSave={true}\n  renderSaveButton={({ onSave, isDirty, isSubmitting }) =\u003e (\n    \u003cbutton onClick={onSave} disabled={!isDirty || isSubmitting}\u003e\n      Save Changes\n    \u003c/button\u003e\n  )}\n  // ... other props\n/\u003e\n```\n\n### Save Reliability\n\nFormEngine includes robust save handling:\n\n- **AbortController** cancels previous in-flight saves when a new save is triggered\n- **Configurable timeout** via `saveTimeoutMs` prop (default 30 seconds)\n- **Retry with exponential backoff** via `maxSaveRetries` prop (default 3 retries)\n\n```tsx\n\u003cFormEngine\n  saveTimeoutMs={15000}   // 15 second timeout\n  maxSaveRetries={5}      // Retry up to 5 times with exponential backoff\n  saveData={async (data) =\u003e { /* ... */ }}\n/\u003e\n```\n\n### Accessibility\n\nBuilt-in accessibility features:\n\n- **Focus trap** in `ConfirmInputsModal` -- Tab key wraps within modal, Escape closes, focus restored on close\n- **Focus-to-first-error** on validation failure -- automatically focuses the first field with an error\n- **ARIA live regions** -- `\u003cdiv role=\"status\" aria-live=\"polite\"\u003e` announces saving/saved/error status to screen readers\n- **aria-label** on filter inputs, **aria-busy** on fields during save\n- **Wizard step announcements** -- screen readers announce \"Step 2 of 4: Details\" on navigation\n\n### Draft Persistence\n\nAuto-save form state to localStorage for recovery after accidental page closures:\n\n```tsx\nimport { useDraftPersistence, useBeforeUnload } from \"@form-eng/core\";\n\nfunction MyForm() {\n  const { isDirty, formValues } = useFormState();\n\n  // Auto-save drafts to localStorage every 5 seconds\n  const { hasDraft, clearDraft } = useDraftPersistence({\n    formId: \"my-form-123\",\n    data: formValues,\n    saveIntervalMs: 5000,\n    enabled: isDirty,\n    storageKeyPrefix: \"myApp\",\n  });\n\n  // Warn user before leaving page with unsaved changes\n  useBeforeUnload(isDirty, \"You have unsaved changes.\");\n\n  return \u003cFormEngine /* ... */ /\u003e;\n}\n```\n\nIncludes `serializeFormState` / `deserializeFormState` utilities for Date-safe JSON round-trips.\n\n### Theming and Customization\n\nCustomize field chrome without replacing components:\n\n```tsx\n// Render props on FieldWrapper\n\u003cFieldWrapper\n  renderLabel={(label, required) =\u003e \u003cMyCustomLabel text={label} isRequired={required} /\u003e}\n  renderError={(error) =\u003e \u003cMyCustomError message={error} /\u003e}\n  renderStatus={(status) =\u003e \u003cMyCustomStatus type={status} /\u003e}\n/\u003e\n```\n\nCSS custom properties for global theming (import optional `styles.css`):\n\n```css\n:root {\n  --fe-error-color: #d32f2f;\n  --fe-warning-color: #ed6c02;\n  --fe-saving-color: #0288d1;\n  --fe-label-color: #333;\n  --fe-required-color: #d32f2f;\n  --fe-border-radius: 4px;\n  --fe-field-gap: 12px;\n  --fe-font-size: 14px;\n}\n```\n\nForm-level error banner via `formErrors` prop on `FormEngine`:\n\n```tsx\n\u003cFormEngine\n  formErrors={[\"End date must be after start date\"]}\n  /* ... */\n/\u003e\n```\n\n### Headless Adapter\n\nThe headless package renders all 28 field types using native HTML elements with `data-field-type` and `data-field-state` attributes for CSS targeting. No UI framework required.\n\n```tsx\nimport { createHeadlessFieldRegistry } from \"@form-eng/headless\";\nimport \"@form-eng/headless/styles.css\"; // optional minimal styles\n\n\u003cInjectedFieldProvider injectedFields={createHeadlessFieldRegistry()}\u003e\n```\n\nStyle with Tailwind CSS, your own stylesheet, or CSS custom properties:\n\n```css\n[data-field-type=\"Textbox\"] input {\n  @apply w-full rounded-md border border-gray-300 px-3 py-2 text-sm\n         focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200;\n}\n\n[data-field-state=\"error\"] input {\n  @apply border-red-500;\n}\n```\n\nSee the [headless package README](./packages/headless/README.md) for full details.\n\n### Visual Form Builder\n\nThe designer package provides a drag-and-drop form builder that exports valid `IFormConfig` v2 JSON:\n\n```tsx\nimport { DesignerProvider, FormDesigner } from \"@form-eng/designer\";\nimport \"@form-eng/designer/dist/styles.css\";\n\nfunction Builder() {\n  return (\n    \u003cDesignerProvider\u003e\n      \u003cFormDesigner style={{ height: \"100vh\" }} /\u003e\n    \u003c/DesignerProvider\u003e\n  );\n}\n```\n\nFeatures: field palette, drag-and-drop canvas, property editor, rule builder (full v2 condition system), wizard configurator, live JSON preview, import/export, undo/redo.\n\nUse `useDesigner()` to access the exported config programmatically. See the [designer package README](./packages/designer/README.md) for full details.\n\n### SSR / Next.js\n\nAll core components are SSR-safe. Browser-only API access (`localStorage`, `document.activeElement`, `window.addEventListener`) is guarded behind `typeof` checks or confined to `useEffect` callbacks.\n\nFor Next.js App Router, add `\"use client\"` to files containing form components. Server-fetched data can be passed as props across the client boundary.\n\nSee the [SSR / Next.js integration guide](./docs/ssr-guide.md) for full setup instructions covering App Router, Pages Router, draft persistence, lazy loading, and common pitfalls.\n\n### RJSF Schema Import\n\nMigrate from [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) with zero rewrite. Bring your existing `schema` + `uiSchema` + `formData` and get a full `IFormConfig` with our rules engine layered on top. JSON Schema `dependencies` and `if/then/else` are auto-converted to `IRule[]`.\n\n```tsx\nimport { fromRjsfSchema } from \"@form-eng/core\";\n\n// Your existing RJSF schema\nconst schema = {\n  type: \"object\",\n  properties: {\n    name: { type: \"string\", title: \"Name\", minLength: 1 },\n    age: { type: \"integer\", title: \"Age\", minimum: 0, maximum: 150 },\n    role: { type: \"string\", enum: [\"admin\", \"user\", \"guest\"] },\n    email: { type: \"string\", format: \"email\" },\n  },\n  required: [\"name\"],\n  dependencies: {\n    role: {\n      oneOf: [\n        {\n          properties: {\n            role: { const: \"admin\" },\n            adminCode: { type: \"string\", title: \"Admin Code\" },\n          },\n          required: [\"adminCode\"],\n        },\n      ],\n    },\n  },\n};\n\nconst uiSchema = {\n  age: { \"ui:widget\": \"updown\" },\n  email: { \"ui:placeholder\": \"you@example.com\" },\n  \"ui:order\": [\"name\", \"email\", \"role\", \"age\", \"*\"],\n};\n\n// Convert to IFormConfig -- dependencies become IRule[] automatically\nconst formConfig = fromRjsfSchema(schema, uiSchema, existingFormData);\n// formConfig.fields.adminCode has rules for conditional visibility based on role\n\n// Use directly with FormEngine\n\u003cFormEngine formConfig={formConfig} /* ... */ /\u003e\n```\n\nAlso exports `toRjsfSchema(config)` for converting back to JSON Schema + uiSchema (best-effort, structural fidelity only).\n\n### Zod Schema Import\n\nConvert Zod object schemas to field configs without adding zod as a dependency:\n\n```tsx\nimport { zodSchemaToFieldConfig } from \"@form-eng/core\";\nimport { z } from \"zod\";\n\nconst UserSchema = z.object({\n  name: z.string().min(1),\n  age: z.number().min(0),\n  active: z.boolean(),\n  role: z.enum([\"admin\", \"user\", \"guest\"]),\n  email: z.string().email(),\n  startDate: z.date(),\n  tags: z.array(z.string()),\n});\n\nconst fieldConfigs = zodSchemaToFieldConfig(UserSchema);\n// Maps: ZodString-\u003eTextbox, ZodNumber-\u003eNumber, ZodBoolean-\u003eToggle,\n//       ZodEnum-\u003eDropdown, ZodDate-\u003eDateControl, ZodArray-\u003eMultiselect\n// Detects .email() and .url() checks for automatic validation\n```\n\nNo `zod` peer dependency is required. If you do not use Zod, this function is tree-shaken out of your bundle.\n\n### Lazy Field Registry\n\nLoad field components on demand using React.lazy for bundle optimization:\n\n```tsx\nimport { createLazyFieldRegistry } from \"@form-eng/core\";\n\nconst lazyFields = createLazyFieldRegistry({\n  Textbox: () =\u003e import(\"./fields/HookTextbox\"),\n  Dropdown: () =\u003e import(\"./fields/HookDropdown\"),\n  // Components are loaded only when first rendered\n});\n\n\u003cInjectedFieldProvider injectedFields={lazyFields}\u003e\n```\n\n## Available Field Types\n\nAll 28 field types (22 editable + 6 read-only) are available in the Fluent UI, MUI, and headless adapters:\n\n### Editable Fields\n\n| Component Key | Description |\n|---------------|-------------|\n| `Textbox` | Single-line text input |\n| `Number` | Numeric input with validation |\n| `Toggle` | Boolean toggle switch |\n| `Dropdown` | Single-select dropdown |\n| `Multiselect` | Multi-select dropdown |\n| `DateControl` | Date picker with clear button |\n| `Slider` | Numeric slider |\n| `SimpleDropdown` | Dropdown from string array in config |\n| `MultiSelectSearch` | Searchable multi-select |\n| `Textarea` | Multiline text with expand-to-modal |\n| `DocumentLinks` | URL link CRUD |\n| `StatusDropdown` | Dropdown with color status indicator |\n| `DynamicFragment` | Hidden field (form state only) |\n| `FieldArray` | Repeating section (add/remove items) |\n| `RadioGroup` | Single-select radio button group |\n| `CheckboxGroup` | Multi-select checkbox group (value: `string[]`) |\n| `Rating` | Star rating input (value: `number`; configurable `max`, `allowHalf`) |\n| `ColorPicker` | Native color picker returning hex string |\n| `Autocomplete` | Searchable single-select with type-ahead |\n| `FileUpload` | File picker (single or multiple); validates size via `config.maxSizeMb` |\n| `DateRange` | Two date inputs (From / To); value: `{ start, end }` ISO strings |\n| `DateTime` | Combined date+time input; value: ISO datetime-local string |\n| `PhoneInput` | Phone input with inline masking (`us`, `international`, `raw` formats) |\n\n### Read-Only Fields\n\n| Component Key | Description |\n|---------------|-------------|\n| `ReadOnly` | Plain text display |\n| `ReadOnlyArray` | Array of strings |\n| `ReadOnlyDateTime` | Formatted date/time |\n| `ReadOnlyCumulativeNumber` | Computed sum of other fields |\n| `ReadOnlyRichText` | Rendered HTML |\n| `ReadOnlyWithButton` | Text with action button |\n\n## Architecture\n\n```\n\u003cRulesEngineProvider\u003e           -- Owns rule state via useReducer (memoized)\n  \u003cInjectedFieldProvider\u003e       -- Component injection registry (memoized)\n    \u003cFormEngine\u003e               -- Form state (react-hook-form), auto-save with retry, rules\n      \u003cFormFields\u003e              -- Renders ordered field list\n        \u003cFormErrorBoundary\u003e     -- Per-field error boundary (crash isolation)\n          \u003cRenderField\u003e         -- Per-field: Controller + component lookup (useMemo)\n            \u003cFieldWrapper\u003e      -- Label, error, saving status (React.memo, render props)\n              \u003cInjectedField /\u003e -- Your UI component via cloneElement\n```\n\n## Building a Custom UI Adapter\n\nSee [docs/creating-an-adapter.md](./docs/creating-an-adapter.md) for a complete guide. The short version:\n\n1. Create field components that accept `IFieldProps\u003cT\u003e`\n2. Build a registry mapping `ComponentTypes` to your field elements\n3. Pass the registry via the `injectedFields` prop on `InjectedFieldProvider`\n\n## Development\n\n```bash\n# Install dependencies\nnpm install --legacy-peer-deps\n\n# Build all packages\nnpm run build\n\n# Build individual packages\nnpm run build:core\nnpm run build:fluent\nnpm run build:mui\nnpm run build:headless\n\n# Run tests\nnpm run test\nnpm run test:watch\nnpm run test:coverage\n\n# Run end-to-end tests\nnpm run test:e2e\n\n# Run benchmarks\nnpm run bench\n\n# Storybook\nnpm run storybook\nnpm run build-storybook\n\n# Clean build output\nnpm run clean\n```\n\n## Project Structure\n\n```\npackages/\n  core/       -- @form-eng/core (React + react-hook-form only)\n  fluent/     -- @form-eng/fluent (Fluent UI v9 adapter)\n  mui/        -- @form-eng/mui (Material UI adapter)\n  headless/   -- @form-eng/headless (semantic HTML adapter)\n  designer/   -- @form-eng/designer (visual form builder)\n  examples/   -- 3 example apps (login+MFA, checkout wizard, data entry)\ne2e/          -- Playwright end-to-end tests\nbenchmarks/   -- Vitest benchmarks for rules engine performance\nstories/      -- Storybook stories for field components\ndocs/\n  creating-an-adapter.md   -- Guide for building custom UI adapters\n  ssr-guide.md             -- SSR / Next.js integration guide\n  ACCESSIBILITY.md         -- Accessibility documentation\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbghcore%2Fform-engine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbghcore%2Fform-engine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbghcore%2Fform-engine/lists"}