@stormlmd/form-builder 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/build_output.txt +0 -0
  2. package/package.json +1 -1
  3. package/src/components/FieldRegistry.ts +16 -18
  4. package/src/components/FormRenderer.tsx +4 -2
  5. package/src/components/builder/EditorWrapper.tsx +3 -1
  6. package/src/components/builder/FormBuilder.tsx +20 -2
  7. package/src/components/builder/FormChildrenRenderer.tsx +3 -1
  8. package/src/components/builder/FormulaHelp.tsx +116 -0
  9. package/src/components/builder/IntegrationSettings.tsx +3 -1
  10. package/src/components/builder/OptionsEditor.tsx +90 -0
  11. package/src/components/builder/PropertiesModal.tsx +2 -1
  12. package/src/components/builder/PropertiesPanel.tsx +155 -62
  13. package/src/components/builder/SortableNode.tsx +2 -1
  14. package/src/components/builder/Toolbox.tsx +30 -63
  15. package/src/components/fields/CheckboxField.tsx +2 -1
  16. package/src/components/fields/DateField.tsx +2 -1
  17. package/src/components/fields/FileUploadField.tsx +2 -1
  18. package/src/components/fields/LabelField.tsx +2 -1
  19. package/src/components/fields/NumberField.tsx +2 -1
  20. package/src/components/fields/RichTextField.tsx +2 -1
  21. package/src/components/fields/SelectField.tsx +8 -2
  22. package/src/components/fields/TextField.tsx +2 -1
  23. package/src/components/layout/FormRepeater.tsx +2 -1
  24. package/src/components/layout/FormTabs.tsx +2 -1
  25. package/src/components/registerComponents.ts +69 -14
  26. package/src/index.ts +7 -0
  27. package/src/plugins/FieldPlugin.ts +63 -0
  28. package/src/plugins/PluginRegistry.ts +94 -0
  29. package/src/store/FormStore.ts +72 -24
  30. package/src/store/FormStoreContext.tsx +66 -0
@@ -3,10 +3,11 @@ import { Box, Tabs, Tab, Paper } from '@mui/material';
3
3
  import { observer } from 'mobx-react-lite';
4
4
  import type { FieldProps } from '../FieldRegistry';
5
5
  import { FormRenderer } from '../FormRenderer';
6
- import { formStore } from '../../store/FormStore';
6
+ import { useFormStore } from '../../store/FormStoreContext';
7
7
  import { LayoutPlaceholder } from './LayoutPlaceholder';
8
8
 
9
9
  export const FormTabs: React.FC<FieldProps> = observer(({ node, path, isEditing }) => {
10
+ const formStore = useFormStore();
10
11
  const [activeTab, setActiveTab] = React.useState(0);
11
12
  const allTabs = node.children || [];
12
13
  const tabs = isEditing
@@ -1,4 +1,7 @@
1
- import { registerComponent } from './FieldRegistry';
1
+ import { registerPlugin } from '../plugins/PluginRegistry';
2
+ import type { FieldPlugin } from '../plugins/FieldPlugin';
3
+
4
+ // Field components
2
5
  import { TextField } from './fields/TextField';
3
6
  import { NumberField } from './fields/NumberField';
4
7
  import { CheckboxField } from './fields/CheckboxField';
@@ -6,25 +9,77 @@ import { SelectField } from './fields/SelectField';
6
9
  import { DateField } from './fields/DateField';
7
10
  import { FileUploadField } from './fields/FileUploadField';
8
11
  import { RichTextField } from './fields/RichTextField';
12
+ import { LabelField } from './fields/LabelField';
13
+
14
+ // Layout components
9
15
  import { FormRow } from './layout/FormRow';
10
16
  import { FormCol } from './layout/FormCol';
11
17
  import { FormTabs } from './layout/FormTabs';
12
18
  import { FormTab } from './layout/FormTab';
13
19
  import { FormRepeater } from './layout/FormRepeater';
14
20
  import { FormPaper } from './layout/FormPaper';
21
+ import { FormTable } from './layout/FormTable';
22
+ import { FormTableCell } from './layout/FormTableCell';
23
+ import { FormDivider } from './layout/FormDivider';
24
+
25
+ // Icons
26
+ import TextFieldsIcon from '@mui/icons-material/TextFields';
27
+ import NumbersIcon from '@mui/icons-material/Numbers';
28
+ import CheckBoxIcon from '@mui/icons-material/CheckBox';
29
+ import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle';
30
+ import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
31
+ import CloudUploadIcon from '@mui/icons-material/CloudUpload';
32
+ import FormatQuoteIcon from '@mui/icons-material/FormatQuote';
33
+ import LabelIcon from '@mui/icons-material/Label';
34
+ import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule';
35
+ import ViewColumnIcon from '@mui/icons-material/ViewColumn';
36
+ import TabIcon from '@mui/icons-material/Tab';
37
+ import RepeatIcon from '@mui/icons-material/Repeat';
38
+ import TableChartIcon from '@mui/icons-material/TableChart';
39
+ import LayersIcon from '@mui/icons-material/Layers';
40
+ import React from 'react';
41
+
42
+ /**
43
+ * Built-in field plugins.
44
+ */
45
+ const BUILT_IN_FIELD_PLUGINS: FieldPlugin[] = [
46
+ { type: 'text', label: 'Text Field', icon: React.createElement(TextFieldsIcon), category: 'field', defaultProps: { label: 'New Text Field', width: 6 }, component: TextField },
47
+ { type: 'number', label: 'Number Field', icon: React.createElement(NumbersIcon), category: 'field', defaultProps: { label: 'New Number Field', width: 6 }, component: NumberField },
48
+ { type: 'checkbox', label: 'Checkbox', icon: React.createElement(CheckBoxIcon), category: 'field', defaultProps: { label: 'New Checkbox', width: 6 }, component: CheckboxField },
49
+ { type: 'select', label: 'Select', icon: React.createElement(ArrowDropDownCircleIcon), category: 'field', defaultProps: { label: 'New Select', width: 6, options: [{ label: 'Option 1', value: '1' }] }, component: SelectField },
50
+ { type: 'date', label: 'Date Picker', icon: React.createElement(CalendarTodayIcon), category: 'field', defaultProps: { label: 'New Date', width: 6 }, component: DateField },
51
+ { type: 'file', label: 'File Upload', icon: React.createElement(CloudUploadIcon), category: 'field', defaultProps: { label: 'New File Upload', width: 6 }, component: FileUploadField },
52
+ { type: 'richtext', label: 'Rich Text', icon: React.createElement(FormatQuoteIcon), category: 'field', defaultProps: { label: 'New Rich Text', width: 6 }, component: RichTextField },
53
+ { type: 'label', label: 'Label', icon: React.createElement(LabelIcon), category: 'field', defaultProps: { text: 'Static Text', variant: 'body1', width: 6 }, component: LabelField },
54
+ ];
55
+
56
+ /**
57
+ * Built-in layout plugins.
58
+ */
59
+ const BUILT_IN_LAYOUT_PLUGINS: FieldPlugin[] = [
60
+ { type: 'divider', label: 'Divider', icon: React.createElement(HorizontalRuleIcon), category: 'layout', defaultProps: { text: '' }, component: FormDivider },
61
+ { type: 'col', label: 'Column', icon: React.createElement(ViewColumnIcon), category: 'layout', isContainer: true, defaultProps: { cols: 6 }, component: FormCol },
62
+ { type: 'tabs', label: 'Tabs', icon: React.createElement(TabIcon), category: 'layout', isContainer: true, defaultProps: {}, component: FormTabs },
63
+ { type: 'repeater', label: 'Repeater', icon: React.createElement(RepeatIcon), category: 'layout', isContainer: true, defaultProps: { label: 'Repeater', addLabel: 'addItem' }, component: FormRepeater },
64
+ { type: 'table', label: 'Table', icon: React.createElement(TableChartIcon), category: 'layout', isContainer: true, defaultProps: { rows: 2, cols: 2, width: 12 }, component: FormTable },
65
+ { type: 'paper', label: 'Paper', icon: React.createElement(LayersIcon), category: 'layout', isContainer: true, defaultProps: { label: 'Paper Group', padding: 2, variant: 'elevation', elevation: 1 }, component: FormPaper },
66
+ ];
67
+
68
+ /**
69
+ * Internal-only plugins (not shown in Toolbox but needed for rendering).
70
+ */
71
+ const INTERNAL_PLUGINS: FieldPlugin[] = [
72
+ { type: 'tab', label: 'Tab', category: 'layout', isContainer: true, showInToolbox: false, component: FormTab },
73
+ { type: 'cell', label: 'Cell', category: 'layout', isContainer: true, showInToolbox: false, component: FormTableCell },
74
+ { type: 'row', label: 'Row', category: 'layout', isContainer: true, showInToolbox: false, defaultProps: {}, component: FormRow },
75
+ ];
15
76
 
77
+ /**
78
+ * Registers all built-in components as full plugins.
79
+ * Call this once at app startup. Backward compatible.
80
+ */
16
81
  export const registerAllComponents = () => {
17
- registerComponent('text', TextField);
18
- registerComponent('number', NumberField);
19
- registerComponent('checkbox', CheckboxField);
20
- registerComponent('select', SelectField);
21
- registerComponent('date', DateField);
22
- registerComponent('file', FileUploadField);
23
- registerComponent('richtext', RichTextField);
24
- registerComponent('row', FormRow);
25
- registerComponent('col', FormCol);
26
- registerComponent('tabs', FormTabs);
27
- registerComponent('tab', FormTab);
28
- registerComponent('repeater', FormRepeater);
29
- registerComponent('paper', FormPaper);
82
+ for (const plugin of [...BUILT_IN_FIELD_PLUGINS, ...BUILT_IN_LAYOUT_PLUGINS, ...INTERNAL_PLUGINS]) {
83
+ registerPlugin(plugin);
84
+ }
30
85
  };
package/src/index.ts CHANGED
@@ -3,3 +3,10 @@ export * from './components/FormRenderer';
3
3
  export * from './components/FieldRegistry';
4
4
  export * from './components/registerComponents';
5
5
  export { FormBuilder } from './components/builder/FormBuilder';
6
+
7
+ // Plugin system
8
+ export { registerPlugin, registerPlugins, getPlugin, getAllPlugins, getToolboxItems, isContainerType } from './plugins/PluginRegistry';
9
+ export type { FieldPlugin, FieldProps, PluginPropertiesEditorProps } from './plugins/FieldPlugin';
10
+
11
+ // Context-bound state management
12
+ export { FormStoreProvider, useFormStore, createFormStore } from './store/FormStoreContext';
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import type { SchemaNode, ValidationRules } from '../store/FormStore';
3
+
4
+ /**
5
+ * Props passed to every field component (built-in or plugin).
6
+ * Re-exported here for convenience when writing plugins.
7
+ */
8
+ export interface FieldProps {
9
+ node: SchemaNode;
10
+ path?: string;
11
+ isEditing?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Props passed to a plugin's custom properties editor.
16
+ */
17
+ export interface PluginPropertiesEditorProps {
18
+ node: SchemaNode;
19
+ onChange: (key: string, value: any) => void;
20
+ }
21
+
22
+ /**
23
+ * Describes a full plugin that integrates into both
24
+ * the FormBuilder (constructor) and FormRenderer (viewer).
25
+ */
26
+ export interface FieldPlugin {
27
+ /** Unique type identifier (e.g. 'rating', 'signature', 'address') */
28
+ type: string;
29
+
30
+ /** Display name shown in the Toolbox */
31
+ label: string;
32
+
33
+ /** Icon for the Toolbox (React element, e.g. MUI icon) */
34
+ icon?: React.ReactNode;
35
+
36
+ /** Category in the Toolbox: 'field' (default) or 'layout' */
37
+ category?: 'field' | 'layout';
38
+
39
+ /** Whether to show in Toolbox (default: true). Set to false for internal types. */
40
+ showInToolbox?: boolean;
41
+
42
+ /** Whether this type is a container that can hold children */
43
+ isContainer?: boolean;
44
+
45
+ /** Default props applied when a new node of this type is created */
46
+ defaultProps?: Record<string, any>;
47
+
48
+ /** React component used to render the field */
49
+ component: React.FC<FieldProps>;
50
+
51
+ /**
52
+ * Optional React component rendered in the Properties Panel
53
+ * when a node of this type is selected.
54
+ */
55
+ propertiesEditor?: React.FC<PluginPropertiesEditorProps>;
56
+
57
+ /**
58
+ * Optional custom validation function.
59
+ * Called after standard validation. Return an error message string
60
+ * or null if valid.
61
+ */
62
+ validate?: (value: any, rules: ValidationRules) => string | null;
63
+ }
@@ -0,0 +1,94 @@
1
+ import type { FieldPlugin, FieldProps } from './FieldPlugin';
2
+ import type React from 'react';
3
+
4
+ /**
5
+ * Central plugin registry.
6
+ * Stores all registered plugins (both built-in and user-defined).
7
+ */
8
+ const plugins: Map<string, FieldPlugin> = new Map();
9
+
10
+ /**
11
+ * Built-in container types that are always recognized.
12
+ * User plugins can also set isContainer: true.
13
+ */
14
+ const BUILT_IN_CONTAINERS = new Set([
15
+ 'root', 'row', 'col', 'tabs', 'tab', 'paper', 'repeater', 'table', 'cell'
16
+ ]);
17
+
18
+ /**
19
+ * Register a single plugin. If a plugin with the same type already exists,
20
+ * it will be overwritten (allowing user overrides of built-in types).
21
+ */
22
+ export function registerPlugin(plugin: FieldPlugin): void {
23
+ plugins.set(plugin.type, plugin);
24
+ }
25
+
26
+ /**
27
+ * Register multiple plugins at once.
28
+ */
29
+ export function registerPlugins(pluginList: FieldPlugin[]): void {
30
+ for (const plugin of pluginList) {
31
+ registerPlugin(plugin);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get a plugin by its type identifier.
37
+ */
38
+ export function getPlugin(type: string): FieldPlugin | undefined {
39
+ return plugins.get(type);
40
+ }
41
+
42
+ /**
43
+ * Get all registered plugins.
44
+ */
45
+ export function getAllPlugins(): FieldPlugin[] {
46
+ return Array.from(plugins.values());
47
+ }
48
+
49
+ /**
50
+ * Get plugins for the Toolbox, filtered by category.
51
+ */
52
+ export function getToolboxItems(category: 'field' | 'layout'): FieldPlugin[] {
53
+ return getAllPlugins().filter(p =>
54
+ (p.category || 'field') === category && p.showInToolbox !== false
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Check if a type is a container (can hold children).
60
+ * Returns true for built-in containers and user plugins with isContainer: true.
61
+ */
62
+ export function isContainerType(type: string): boolean {
63
+ if (BUILT_IN_CONTAINERS.has(type)) return true;
64
+ const plugin = getPlugin(type);
65
+ return plugin?.isContainer === true;
66
+ }
67
+
68
+ /**
69
+ * Backward-compatible bridge: register a bare component as a minimal plugin.
70
+ * Used by the legacy registerComponent() API.
71
+ */
72
+ export function registerComponentAsPlugin(type: string, component: React.FC<FieldProps>): void {
73
+ // Only create a new plugin if one doesn't already exist for this type
74
+ // (prevents overwriting a full plugin with a bare component)
75
+ if (!plugins.has(type)) {
76
+ registerPlugin({
77
+ type,
78
+ label: type,
79
+ component,
80
+ });
81
+ } else {
82
+ // Update just the component on the existing plugin
83
+ const existing = plugins.get(type)!;
84
+ existing.component = component;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get the React component for a given type.
90
+ * Backward-compatible replacement for the old getComponent().
91
+ */
92
+ export function getComponentForType(type: string): React.FC<FieldProps> | undefined {
93
+ return plugins.get(type)?.component;
94
+ }
@@ -1,26 +1,16 @@
1
1
  import { makeAutoObservable, toJS } from "mobx";
2
2
  import i18next from 'i18next';
3
+ import { addDays, differenceInDays, parse, format, parseISO, isValid } from 'date-fns';
3
4
  import { transformAPIDataToSchema, transformSchemaToSubmission, transformDictionaryToOptions } from '../utils/apiTransformer';
5
+ import { getPlugin, isContainerType } from '../plugins/PluginRegistry';
4
6
 
5
- export type NodeType =
6
- | 'root'
7
- | 'text'
8
- | 'number'
9
- | 'checkbox'
10
- | 'select'
11
- | 'row'
12
- | 'col'
13
- | 'tabs' // container for tabs
14
- | 'tab' // individual tab
15
- | 'table'
16
- | 'cell'
17
- | 'paper'
18
- | 'repeater'
19
- | 'date'
20
- | 'file'
21
- | 'richtext'
22
- | 'label'
23
- | 'divider';
7
+ /**
8
+ * Node type identifier. Open string type to support plugin-defined types.
9
+ * Built-in types: 'root', 'text', 'number', 'checkbox', 'select', 'row',
10
+ * 'col', 'tabs', 'tab', 'table', 'cell', 'paper', 'repeater', 'date',
11
+ * 'file', 'richtext', 'label', 'divider'.
12
+ */
13
+ export type NodeType = string;
24
14
 
25
15
  export interface FieldOption {
26
16
  label: string;
@@ -101,6 +91,9 @@ export class FormStore {
101
91
  // Session data (user info, report IDs, etc.)
102
92
  sessionVariables: Record<string, any> = {};
103
93
 
94
+ // External constants for formulas and lists
95
+ externalConstants: Record<string, any> = {};
96
+
104
97
  // Pool of available API columns for mapping
105
98
  availableApiColumns: any[] = [];
106
99
 
@@ -113,6 +106,10 @@ export class FormStore {
113
106
  this.isEditMode = mode;
114
107
  }
115
108
 
109
+ setConstant(name: string, value: any) {
110
+ this.externalConstants[name] = value;
111
+ }
112
+
116
113
  // --- Actions ---
117
114
 
118
115
  selectNode(id: string | null) {
@@ -144,6 +141,7 @@ export class FormStore {
144
141
  const response = await fetch(`${this.apiBase}/schema/${formId}`);
145
142
  if (!response.ok) throw new Error("Failed to fetch schema");
146
143
  const schema = await response.json();
144
+ if (schema && !schema.id) schema.id = 'root'; // Enforce root ID
147
145
  this.rootNode = schema;
148
146
  this.applyDefaultValues();
149
147
  } catch (error) {
@@ -308,7 +306,7 @@ export class FormStore {
308
306
 
309
307
  // --- Validation ---
310
308
 
311
- validateField(path: string, value: any, rules?: ValidationRules) {
309
+ validateField(path: string, value: any, rules?: ValidationRules, nodeType?: string) {
312
310
  if (!rules) return;
313
311
 
314
312
  let error: string | null = null;
@@ -369,6 +367,14 @@ export class FormStore {
369
367
  }
370
368
  }
371
369
 
370
+ // Plugin custom validation
371
+ if (!error && nodeType) {
372
+ const plugin = getPlugin(nodeType);
373
+ if (plugin?.validate) {
374
+ error = plugin.validate(value, rules);
375
+ }
376
+ }
377
+
372
378
  if (error) {
373
379
  this.errors[path] = error;
374
380
  } else {
@@ -420,7 +426,13 @@ export class FormStore {
420
426
  return JSON.stringify(new Date().toISOString());
421
427
  }
422
428
 
423
- const value = this.getValue(fieldName);
429
+ let value = this.getValue(fieldName);
430
+
431
+ // Fallback to external constants if not found in form values
432
+ if (value === undefined && this.externalConstants[fieldName] !== undefined) {
433
+ value = this.externalConstants[fieldName];
434
+ }
435
+
424
436
  // If it's a number, return it raw. If it's a string, JSON.stringify it.
425
437
  // If undefined/null, return '0' for safety in math.
426
438
  if (value === undefined || value === null) return '0';
@@ -438,6 +450,40 @@ export class FormStore {
438
450
  ROUND: (v: any, p: number = 0) => {
439
451
  const factor = Math.pow(10, p);
440
452
  return Math.round(v * factor) / factor;
453
+ },
454
+ DATE_ADD: (date: any, days: any) => {
455
+ if (!date) return '';
456
+ const daysNum = Number(days) || 0;
457
+ let d = parseISO(date);
458
+ let isISO = true;
459
+ if (!isValid(d)) {
460
+ d = parse(date, 'dd-MM-yyyy', new Date());
461
+ isISO = false;
462
+ }
463
+ if (!isValid(d)) return date;
464
+ const result = addDays(d, daysNum);
465
+ return isISO ? result.toISOString() : format(result, 'dd-MM-yyyy');
466
+ },
467
+ DATE_DIFF: (date1: any, date2: any) => {
468
+ if (!date1 || !date2) return 0;
469
+ let d1 = parseISO(date1);
470
+ if (!isValid(d1)) d1 = parse(date1, 'dd-MM-yyyy', new Date());
471
+ let d2 = parseISO(date2);
472
+ if (!isValid(d2)) d2 = parse(date2, 'dd-MM-yyyy', new Date());
473
+ if (!isValid(d1) || !isValid(d2)) return 0;
474
+ return differenceInDays(d1, d2);
475
+ },
476
+ SWITCH: (val: any, ...args: any[]) => {
477
+ for (let i = 0; i < args.length - 1; i += 2) {
478
+ if (val === args[i]) return args[i + 1];
479
+ }
480
+ return args.length % 2 === 1 ? args[args.length - 1] : undefined;
481
+ },
482
+ IFS: (...args: any[]) => {
483
+ for (let i = 0; i < args.length - 1; i += 2) {
484
+ if (args[i]) return args[i + 1];
485
+ }
486
+ return args.length % 2 === 1 ? args[args.length - 1] : undefined;
441
487
  }
442
488
  };
443
489
 
@@ -648,8 +694,7 @@ export class FormStore {
648
694
  const overNode = this.findNode(this.rootNode, overId);
649
695
 
650
696
  // If we drop on a container, the parent is the container itself
651
- const containerTypes: NodeType[] = ['root', 'row', 'col', 'tabs', 'tab', 'paper', 'repeater', 'table', 'cell'];
652
- const isOverContainer = overNode && containerTypes.includes(overNode.type);
697
+ const isOverContainer = overNode && isContainerType(overNode.type);
653
698
  const overParent = isOverContainer ? overNode : this.findParent(this.rootNode, overId);
654
699
 
655
700
  if (!activeNode || !activeParent || !activeParent.children) {
@@ -730,7 +775,9 @@ export class FormStore {
730
775
  try {
731
776
  const stored = localStorage.getItem('form_schema');
732
777
  if (stored) {
733
- this.rootNode = JSON.parse(stored);
778
+ const schema = JSON.parse(stored);
779
+ if (schema && !schema.id) schema.id = 'root'; // Enforce root ID
780
+ this.rootNode = schema;
734
781
  this.applyDefaultValues();
735
782
  console.log('Schema loaded from localStorage');
736
783
  }
@@ -771,6 +818,7 @@ export class FormStore {
771
818
  }
772
819
 
773
820
  findNode(root: SchemaNode, id: string): SchemaNode | null {
821
+ if (id === 'root' && (root === this.rootNode || root.id === 'root')) return root;
774
822
  if (root.id === id) return root;
775
823
  if (root.children) {
776
824
  for (const child of root.children) {
@@ -0,0 +1,66 @@
1
+ import React, { createContext, useContext } from 'react';
2
+ import { FormStore, formStore as defaultFormStore } from './FormStore';
3
+
4
+ /**
5
+ * React Context for providing FormStore instances to the component tree.
6
+ * When null, components fall back to the default singleton.
7
+ */
8
+ const FormStoreContext = createContext<FormStore | null>(null);
9
+
10
+ /**
11
+ * Hook to get the current FormStore from context.
12
+ * Falls back to the default singleton if no provider is found.
13
+ */
14
+ export function useFormStore(): FormStore {
15
+ const store = useContext(FormStoreContext);
16
+ return store ?? defaultFormStore;
17
+ }
18
+
19
+ /**
20
+ * Factory function to create a new isolated FormStore instance.
21
+ * Use this when you need multiple independent form instances.
22
+ *
23
+ * @example
24
+ * const myStore = createFormStore();
25
+ * <FormStoreProvider store={myStore}>
26
+ * <FormBuilder />
27
+ * </FormStoreProvider>
28
+ */
29
+ export function createFormStore(): FormStore {
30
+ return new FormStore();
31
+ }
32
+
33
+ interface FormStoreProviderProps {
34
+ /** FormStore instance to provide. If omitted, creates a new one. */
35
+ store?: FormStore;
36
+ children: React.ReactNode;
37
+ }
38
+
39
+ /**
40
+ * Provider component that supplies a FormStore instance to all descendant
41
+ * form components. Enables multiple independent form instances on the same page.
42
+ *
43
+ * @example
44
+ * // Using an external store
45
+ * const store = createFormStore();
46
+ * <FormStoreProvider store={store}>
47
+ * <FormBuilder />
48
+ * </FormStoreProvider>
49
+ *
50
+ * @example
51
+ * // Auto-creates an internal store
52
+ * <FormStoreProvider>
53
+ * <FormBuilder />
54
+ * </FormStoreProvider>
55
+ */
56
+ export const FormStoreProvider: React.FC<FormStoreProviderProps> = ({ store, children }) => {
57
+ const [internalStore] = React.useState(() => store ?? createFormStore());
58
+
59
+ return (
60
+ <FormStoreContext.Provider value={store ?? internalStore}>
61
+ {children}
62
+ </FormStoreContext.Provider>
63
+ );
64
+ };
65
+
66
+ export { FormStoreContext };