@bluprynt/forms-builder 1.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/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # @bluprynt/forms-builder
2
+
3
+ Headless React component for building and editing form schemas with drag-and-drop reordering. Pairs with `@bluprynt/forms-core` to handle schema mutations via `FormDefinitionEditor`.
4
+
5
+ ## Key Capabilities
6
+
7
+ - **Drag-and-drop reordering** — reorder fields and sections with depth-aware nesting via @dnd-kit, respecting the maximum nesting depth of 3 levels.
8
+ - **Schema CRUD** — add, update, and remove fields and sections. All mutations go through `FormDefinitionEditor` from `@bluprynt/forms-core`.
9
+ - **Headless architecture** — all rendering is delegated to components you provide (`container`, `section`, `field`, `addPlaceholder`), so the package has zero UI opinions.
10
+ - **Depth-aware nesting** — horizontal drag offset controls nesting depth. Only sections can accept nested children, enforced during drag projection.
11
+ - **Add placeholders** — automatic insertion points at the end of each section and at the root level for adding new items.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @bluprynt/forms-builder @bluprynt/forms-core ajv react react-dom @dnd-kit/abstract @dnd-kit/dom @dnd-kit/helpers @dnd-kit/react @dnd-kit/state
17
+ ```
18
+
19
+ `@bluprynt/forms-core`, `react`, `react-dom`, and `@dnd-kit/*` packages are peer dependencies and must be installed in your project.
20
+
21
+ ## Quick Start
22
+
23
+ ```tsx
24
+ import { useState, type FC } from 'react'
25
+ import { FormBuilder } from '@bluprynt/forms-builder'
26
+ import type {
27
+ FieldRenderProps,
28
+ SectionRenderProps,
29
+ AddPlaceholderRenderProps,
30
+ NewContentItem,
31
+ } from '@bluprynt/forms-builder'
32
+ import type { FormDefinition } from '@bluprynt/forms-core'
33
+
34
+ const MyField: FC<FieldRenderProps> = ({ ref, handleRef, item, isDragging, onRemove }) => (
35
+ <div ref={ref} style={{ opacity: isDragging ? 0.5 : 1 }}>
36
+ <span ref={handleRef}>⠿</span>
37
+ {item.label}
38
+ <button onClick={onRemove}>Remove</button>
39
+ </div>
40
+ )
41
+
42
+ const MySection: FC<SectionRenderProps> = ({ ref, handleRef, item, isDragging }) => (
43
+ <div ref={ref} style={{ opacity: isDragging ? 0.5 : 1 }}>
44
+ <span ref={handleRef}>⠿</span>
45
+ {item.title}
46
+ </div>
47
+ )
48
+
49
+ const MyAddPlaceholder: FC<AddPlaceholderRenderProps> = ({ onAdd }) => (
50
+ <button onClick={() => onAdd({ type: 'string', label: 'New Field' })}>
51
+ + Add Field
52
+ </button>
53
+ )
54
+
55
+ const Builder: FC = () => {
56
+ const [definition, setDefinition] = useState<FormDefinition>(initialDefinition)
57
+
58
+ return (
59
+ <FormBuilder
60
+ definition={definition}
61
+ container={({ children }) => <div>{children}</div>}
62
+ section={MySection}
63
+ field={MyField}
64
+ addPlaceholder={MyAddPlaceholder}
65
+ selectedId={null}
66
+ onDefinitionChange={setDefinition}
67
+ />
68
+ )
69
+ }
70
+ ```
71
+
72
+ ### Handling Selection
73
+
74
+ Track which item is selected by passing `selectedId`. Each field and section component receives `isSelected`:
75
+
76
+ ```tsx
77
+ const [selectedId, setSelectedId] = useState<number | null>(null)
78
+
79
+ <FormBuilder
80
+ definition={definition}
81
+ container={({ children }) => <div>{children}</div>}
82
+ section={(props) => (
83
+ <div ref={props.ref} onClick={() => setSelectedId(props.id)}
84
+ style={{ outline: props.isSelected ? '2px solid blue' : 'none' }}>
85
+ <span ref={props.handleRef}>⠿</span>
86
+ {props.item.title}
87
+ </div>
88
+ )}
89
+ field={(props) => (
90
+ <div ref={props.ref} onClick={() => setSelectedId(props.id)}
91
+ style={{ outline: props.isSelected ? '2px solid blue' : 'none' }}>
92
+ <span ref={props.handleRef}>⠿</span>
93
+ {props.item.label}
94
+ </div>
95
+ )}
96
+ addPlaceholder={MyAddPlaceholder}
97
+ selectedId={selectedId}
98
+ onDefinitionChange={setDefinition}
99
+ />
100
+ ```
101
+
102
+ ## Render Component Props
103
+
104
+ ### FieldRenderProps
105
+
106
+ | Property | Type | Description |
107
+ |----------|------|-------------|
108
+ | `ref` | `(element: Element \| null) => void` | Sortable item ref — attach to outermost DOM element |
109
+ | `handleRef` | `(element: Element \| null) => void` | Drag handle ref — attach to the drag handle element |
110
+ | `id` | `number` | Field ID |
111
+ | `depth` | `number` | Nesting depth (0 = root level) |
112
+ | `index` | `number` | Position index in the flat list |
113
+ | `parentId` | `number \| null` | Parent section ID, or `null` for root-level |
114
+ | `type` | `FieldType` | Field type (`'string'`, `'number'`, etc.) |
115
+ | `item` | `FieldContentItem` | Full field content item from the definition |
116
+ | `isDragging` | `boolean` | `true` when this item is being dragged |
117
+ | `isSelected` | `boolean` | `true` when this item's ID matches `selectedId` |
118
+ | `onAdd` | `(item: NewContentItem) => number` | Add a new item to this field's parent section; returns the new ID |
119
+ | `onRemove` | `() => void` | Remove this field |
120
+ | `onChange` | `(item: ContentItem) => void` | Update this field |
121
+
122
+ ### SectionRenderProps
123
+
124
+ Same shape as `FieldRenderProps` except:
125
+
126
+ | Property | Type | Description |
127
+ |----------|------|-------------|
128
+ | `item` | `SectionContentItem` | Full section content item from the definition |
129
+ | `onAdd` | `(item: NewContentItem) => number` | Add a new item **inside** this section; returns the new ID |
130
+ | `onRemove` | `() => void` | Remove this section and all descendants |
131
+
132
+ ### AddPlaceholderRenderProps
133
+
134
+ | Property | Type | Description |
135
+ |----------|------|-------------|
136
+ | `depth` | `number` | Nesting depth where the placeholder appears |
137
+ | `parentId` | `number \| null` | Parent section ID, or `null` for root-level |
138
+ | `onAdd` | `(item: NewContentItem) => number` | Add a new item at this location; returns the new ID |
139
+ | `onChange` | `(id: number, item: ContentItem) => void` | Update an existing item by ID |
140
+
141
+ ## Documentation
142
+
143
+ | Document | Description |
144
+ |----------|-------------|
145
+ | [Architecture](./docs/architecture.md) | Project structure, module dependency graph, drag-and-drop flow, tree flattening |
146
+ | [API Reference](./docs/api-reference.md) | Complete public API — components, hooks, types, props tables |
147
+ | [Development Guide](./docs/development-guide.md) | How to build, test, lint, and extend the builder |
package/dist/index.cjs ADDED
@@ -0,0 +1,393 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _dnd_kit_react = require("@dnd-kit/react");
3
+ let react_jsx_runtime = require("react/jsx-runtime");
4
+ let _dnd_kit_react_sortable = require("@dnd-kit/react/sortable");
5
+ let react = require("react");
6
+ let _bluprynt_forms_core = require("@bluprynt/forms-core");
7
+ let _dnd_kit_helpers = require("@dnd-kit/helpers");
8
+ //#region src/add-placeholder.tsx
9
+ const AddPlaceholder = ({ depth, parentId, component: Component, onAdd, onChange }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
10
+ depth,
11
+ parentId,
12
+ onAdd,
13
+ onChange
14
+ });
15
+ //#endregion
16
+ //#region src/core/types.ts
17
+ /**
18
+ * Type guard that narrows a `TreeItem` to a section (`type` is `'section'`, `item` is `SectionContentItem`).
19
+ */
20
+ const isSection = (item) => item.type === "section";
21
+ /**
22
+ * Type guard that narrows a `TreeItem` to a field (`type` is not `'section'`, `item` is `FieldContentItem`).
23
+ */
24
+ const isField = (item) => item.type !== "section" && item.type !== "add-placeholder";
25
+ /**
26
+ * Type guard that narrows a `TreeItem` to an "add" placeholder.
27
+ */
28
+ const isAddPlaceholder = (item) => item.type === "add-placeholder";
29
+ //#endregion
30
+ //#region src/core/constants.ts
31
+ const SORTABLE_CONFIG = {
32
+ alignment: {
33
+ x: "start",
34
+ y: "center"
35
+ },
36
+ transition: { idle: true }
37
+ };
38
+ //#endregion
39
+ //#region src/field.tsx
40
+ const Field = ({ id, depth, index, parentId, type, item, isSelected, component: Component, onAdd, onRemove, onChange }) => {
41
+ const { ref, handleRef, isDragSource } = (0, _dnd_kit_react_sortable.useSortable)({
42
+ ...SORTABLE_CONFIG,
43
+ id,
44
+ index,
45
+ data: {
46
+ depth,
47
+ parentId
48
+ }
49
+ });
50
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
51
+ ref,
52
+ handleRef,
53
+ id,
54
+ depth,
55
+ index,
56
+ parentId,
57
+ type,
58
+ item,
59
+ isDragging: isDragSource,
60
+ isSelected,
61
+ onAdd,
62
+ onRemove,
63
+ onChange
64
+ });
65
+ };
66
+ //#endregion
67
+ //#region src/section.tsx
68
+ const Section = ({ id, depth, index, parentId, item, isSelected, component: Component, onAdd, onRemove, onChange }) => {
69
+ const { ref, handleRef, isDragSource } = (0, _dnd_kit_react_sortable.useSortable)({
70
+ ...SORTABLE_CONFIG,
71
+ id,
72
+ index,
73
+ data: {
74
+ depth,
75
+ parentId
76
+ }
77
+ });
78
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
79
+ ref,
80
+ handleRef,
81
+ id,
82
+ depth,
83
+ index,
84
+ parentId,
85
+ item,
86
+ isDragging: isDragSource,
87
+ isSelected,
88
+ onAdd,
89
+ onRemove,
90
+ onChange
91
+ });
92
+ };
93
+ //#endregion
94
+ //#region src/core/projection.ts
95
+ /**
96
+ * Converts a horizontal pixel offset into a depth level based on indentation width.
97
+ */
98
+ const getDragDepth = (offset, indentationWidth) => Math.round(offset / indentationWidth);
99
+ /**
100
+ * Returns the maximum allowed nesting depth when dragging above a target item.
101
+ * Only sections can accept nested children.
102
+ */
103
+ const getMaxDepth = (targetItem, previousItem) => {
104
+ if (!previousItem) return 0;
105
+ if (previousItem.type !== "section") return previousItem.depth;
106
+ return Math.min(targetItem.depth + 1, previousItem.depth + 1);
107
+ };
108
+ /**
109
+ * Returns the minimum depth based on the next sibling item.
110
+ * If there is no next item, the minimum depth is 0 (root level).
111
+ */
112
+ const getMinDepth = (nextItem) => nextItem ? nextItem.depth : 0;
113
+ /**
114
+ * Calculates the projected depth and parent for a dragged item relative to a target position.
115
+ * Clamps depth between min/max bounds and enforces `maxAllowedDepth`.
116
+ */
117
+ const getProjection = (items, targetId, projectedDepth, maxAllowedDepth) => {
118
+ const targetItemIndex = items.findIndex(({ id }) => id === targetId);
119
+ const previousItem = items[targetItemIndex - 1];
120
+ const targetItem = items[targetItemIndex];
121
+ const nextItem = items[targetItemIndex + 1];
122
+ if (!targetItem) return {
123
+ depth: 0,
124
+ maxDepth: 0,
125
+ minDepth: 0,
126
+ parentId: null
127
+ };
128
+ const maxDepth = getMaxDepth(targetItem, previousItem);
129
+ const minDepth = getMinDepth(nextItem);
130
+ let depth = projectedDepth;
131
+ if (depth >= maxDepth) depth = maxDepth;
132
+ else if (depth < minDepth) depth = minDepth;
133
+ if (depth > maxAllowedDepth) depth = maxAllowedDepth;
134
+ const getParentId = () => {
135
+ if (depth === 0 || !previousItem) return null;
136
+ if (depth === previousItem.depth) return previousItem.parentId;
137
+ if (depth > previousItem.depth) {
138
+ if (previousItem.type !== "section") return previousItem.parentId;
139
+ return previousItem.id;
140
+ }
141
+ return items.slice(0, targetItemIndex).reverse().find((item) => item.depth === depth)?.parentId ?? null;
142
+ };
143
+ return {
144
+ depth,
145
+ maxDepth,
146
+ minDepth,
147
+ parentId: getParentId()
148
+ };
149
+ };
150
+ //#endregion
151
+ //#region src/core/tree.ts
152
+ let placeholderIdCounter = -1e3;
153
+ /**
154
+ * Resets the placeholder id counter. Call before each full flatten to ensure stable ids per flatten pass.
155
+ */
156
+ const resetPlaceholderIds = () => {
157
+ placeholderIdCounter = -1e3;
158
+ };
159
+ const nextPlaceholderId = () => --placeholderIdCounter;
160
+ /**
161
+ * Flattens a nested content tree into a flat list of items with depth and parent info.
162
+ * Each item preserves its position metadata for rendering in a sortable tree.
163
+ * Appends an "add-placeholder" at the end of each section and at the root level.
164
+ */
165
+ const flattenTree = (items, parentId = null, depth = 0) => {
166
+ if (parentId === null) resetPlaceholderIds();
167
+ const result = items.reduce((result, current, index) => {
168
+ const isSection = current.type === "section";
169
+ result.push({
170
+ id: current.id,
171
+ type: current.type,
172
+ parentId,
173
+ depth,
174
+ index,
175
+ item: current
176
+ });
177
+ if (isSection) {
178
+ const children = current.content;
179
+ result.push(...flattenTree(children, current.id, depth + 1));
180
+ }
181
+ return result;
182
+ }, []);
183
+ result.push({
184
+ id: nextPlaceholderId(),
185
+ type: "add-placeholder",
186
+ parentId,
187
+ depth,
188
+ index: items.length,
189
+ item: void 0
190
+ });
191
+ return result;
192
+ };
193
+ /**
194
+ * Collects all transitive descendant ids for a given parent from a flat item list.
195
+ */
196
+ const getDescendants = (items, parentId) => {
197
+ return items.filter((item) => item.parentId === parentId).reduce((result, child) => {
198
+ result.add(child.id);
199
+ for (const descendant of getDescendants(items, child.id)) result.add(descendant);
200
+ return result;
201
+ }, /* @__PURE__ */ new Set());
202
+ };
203
+ //#endregion
204
+ //#region src/use-form-builder.ts
205
+ /**
206
+ * Manages the form builder state: flattened tree items, drag-and-drop reordering,
207
+ * item editing, and removal. Uses {@link FormDefinitionEditor} for all definition mutations
208
+ * and notifies the consumer of structural changes via {@link onDefinitionChange}.
209
+ *
210
+ * @param definition - The current form definition to build upon.
211
+ * @param onDefinitionChange - Optional callback invoked with the updated `FormDefinition`
212
+ * whenever the tree structure, item content, or item order changes.
213
+ * @returns Flattened items and handlers for item/drag operations.
214
+ */
215
+ const useFormBuilder = (definition, onDefinitionChange) => {
216
+ const [items, setItems] = (0, react.useState)(() => flattenTree(definition.content));
217
+ const initialDepth = (0, react.useRef)(0);
218
+ const sourceChildren = (0, react.useRef)([]);
219
+ const dragSourceId = (0, react.useRef)(null);
220
+ const definitionRef = (0, react.useRef)(definition);
221
+ definitionRef.current = definition;
222
+ const onDefinitionChangeRef = (0, react.useRef)(onDefinitionChange);
223
+ onDefinitionChangeRef.current = onDefinitionChange;
224
+ (0, react.useEffect)(() => {
225
+ setItems(flattenTree(definition.content));
226
+ }, [definition]);
227
+ const applyEditor = (0, react.useCallback)((editor) => {
228
+ const updated = editor.toJSON();
229
+ setItems(flattenTree(updated.content));
230
+ onDefinitionChangeRef.current?.(updated);
231
+ }, []);
232
+ const handleItemAdd = (0, react.useCallback)((sectionId, item) => {
233
+ const editor = new _bluprynt_forms_core.FormDefinitionEditor(definitionRef.current);
234
+ const parentId = sectionId ?? void 0;
235
+ const id = editor.nextId();
236
+ if (item.type === "section") editor.addSection({
237
+ ...item,
238
+ id
239
+ }, parentId);
240
+ else editor.addField({
241
+ ...item,
242
+ id
243
+ }, parentId);
244
+ applyEditor(editor);
245
+ return id;
246
+ }, [applyEditor]);
247
+ const handleItemChange = (0, react.useCallback)((id, newItem) => {
248
+ const editor = new _bluprynt_forms_core.FormDefinitionEditor(definitionRef.current);
249
+ if (newItem.type === "section") {
250
+ const { id: _, type: __, content: ___, ...updates } = newItem;
251
+ editor.updateSection(id, updates);
252
+ } else {
253
+ const { id: _, type: __, ...updates } = newItem;
254
+ editor.updateField(id, updates);
255
+ }
256
+ applyEditor(editor);
257
+ }, [applyEditor]);
258
+ const handleItemRemove = (0, react.useCallback)((itemId) => {
259
+ const editor = new _bluprynt_forms_core.FormDefinitionEditor(definitionRef.current);
260
+ editor.removeItem(itemId);
261
+ applyEditor(editor);
262
+ }, [applyEditor]);
263
+ const handleDragStart = (event) => {
264
+ const { source } = event.operation;
265
+ if (!source) return;
266
+ const item = items.find(({ id }) => id === source.id);
267
+ if (!item) return;
268
+ initialDepth.current = item.depth;
269
+ dragSourceId.current = source.id;
270
+ setItems((prev) => {
271
+ sourceChildren.current = [];
272
+ const descendants = getDescendants(prev, source.id);
273
+ return prev.filter((i) => {
274
+ if (descendants.has(i.id)) {
275
+ sourceChildren.current = [...sourceChildren.current, i];
276
+ return false;
277
+ }
278
+ return true;
279
+ });
280
+ });
281
+ };
282
+ const handleDragOver = (event, manager) => {
283
+ const { source, target } = event.operation;
284
+ event.preventDefault();
285
+ if (source && target && source.id !== target.id) setItems((prev) => {
286
+ const offsetLeft = manager.dragOperation.transform.x;
287
+ const dragDepth = getDragDepth(offsetLeft, 40);
288
+ const projectedDepth = initialDepth.current + dragDepth;
289
+ const { depth, parentId } = getProjection(prev, target.id, projectedDepth, 3);
290
+ return (0, _dnd_kit_helpers.move)(prev, event).map((item) => item.id === source.id ? {
291
+ ...item,
292
+ depth,
293
+ parentId
294
+ } : item);
295
+ });
296
+ };
297
+ const handleDragMove = (event, manager) => {
298
+ if (event.defaultPrevented) return;
299
+ const { source, target } = event.operation;
300
+ if (!source || !target) return;
301
+ const offsetLeft = manager.dragOperation.transform.x;
302
+ const dragDepth = getDragDepth(offsetLeft, 40);
303
+ const projectedDepth = initialDepth.current + dragDepth;
304
+ const { depth, parentId } = getProjection(items, source.id, projectedDepth, 3);
305
+ if (source.data?.depth !== depth || source.data?.parentId !== parentId) setItems((prev) => prev.map((item) => item.id === source.id ? {
306
+ ...item,
307
+ depth,
308
+ parentId
309
+ } : item));
310
+ };
311
+ const handleDragEnd = (event) => {
312
+ if (event.canceled) {
313
+ setItems(flattenTree(definitionRef.current.content));
314
+ dragSourceId.current = null;
315
+ sourceChildren.current = [];
316
+ return;
317
+ }
318
+ const draggedId = dragSourceId.current;
319
+ if (draggedId === null) return;
320
+ const draggedItem = items.find((i) => i.id === draggedId);
321
+ if (!draggedItem) return;
322
+ const targetParentId = draggedItem.parentId;
323
+ const draggedFlatIndex = items.findIndex((i) => i.id === draggedId);
324
+ let siblingIndex = 0;
325
+ for (let i = 0; i < draggedFlatIndex; i++) {
326
+ const current = items[i];
327
+ if (current && current.parentId === targetParentId && current.type !== "add-placeholder") siblingIndex++;
328
+ }
329
+ const editor = new _bluprynt_forms_core.FormDefinitionEditor(definitionRef.current);
330
+ editor.moveItem(draggedId, targetParentId ?? void 0, siblingIndex);
331
+ applyEditor(editor);
332
+ dragSourceId.current = null;
333
+ sourceChildren.current = [];
334
+ };
335
+ return {
336
+ items,
337
+ handleItemAdd,
338
+ handleItemChange,
339
+ handleItemRemove,
340
+ handleDragStart,
341
+ handleDragOver,
342
+ handleDragMove,
343
+ handleDragEnd
344
+ };
345
+ };
346
+ //#endregion
347
+ //#region src/builder.tsx
348
+ const FormBuilder = ({ definition, container: Container, section: SectionComponent, field: FieldComponent, addPlaceholder: AddPlaceholderComponent, selectedId, onDefinitionChange }) => {
349
+ const { items, handleItemAdd, handleItemChange, handleItemRemove, handleDragStart, handleDragOver, handleDragMove, handleDragEnd } = useFormBuilder(definition, onDefinitionChange);
350
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_dnd_kit_react.DragDropProvider, {
351
+ onDragStart: handleDragStart,
352
+ onDragOver: handleDragOver,
353
+ onDragMove: handleDragMove,
354
+ onDragEnd: handleDragEnd,
355
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Container, { children: items.map((item, index) => {
356
+ const onAdd = (value) => handleItemAdd(item.type === "section" ? item.id : item.parentId, value);
357
+ const onRemove = () => handleItemRemove(item.id);
358
+ const onChange = (value) => handleItemChange(value.id, value);
359
+ return isSection(item) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
360
+ id: item.id,
361
+ depth: item.depth,
362
+ index,
363
+ parentId: item.parentId,
364
+ item: item.item,
365
+ isSelected: selectedId === item.id,
366
+ component: SectionComponent,
367
+ onAdd,
368
+ onRemove,
369
+ onChange
370
+ }, item.id) : isField(item) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
371
+ id: item.id,
372
+ depth: item.depth,
373
+ index,
374
+ parentId: item.parentId,
375
+ type: item.type,
376
+ item: item.item,
377
+ isSelected: selectedId === item.id,
378
+ component: FieldComponent,
379
+ onAdd,
380
+ onRemove,
381
+ onChange
382
+ }, item.id) : isAddPlaceholder(item) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddPlaceholder, {
383
+ depth: item.depth,
384
+ parentId: item.parentId,
385
+ component: AddPlaceholderComponent,
386
+ onAdd,
387
+ onChange: handleItemChange
388
+ }, item.id) : null;
389
+ }) })
390
+ });
391
+ };
392
+ //#endregion
393
+ exports.FormBuilder = FormBuilder;
@@ -0,0 +1,64 @@
1
+ import { FC, PropsWithChildren } from "react";
2
+ import { ContentItem, FieldContentItem, FieldType, FormDefinition, SectionContentItem } from "@bluprynt/forms-core";
3
+
4
+ //#region src/core/types.d.ts
5
+ /**
6
+ * A `ContentItem` without an `id` — used when adding new items (id is auto-generated).
7
+ */
8
+ type NewContentItem = Omit<FieldContentItem, 'id'> | Omit<SectionContentItem, 'id'>;
9
+ //#endregion
10
+ //#region src/add-placeholder.d.ts
11
+ type AddPlaceholderRenderProps = {
12
+ depth: number;
13
+ parentId: number | null;
14
+ onAdd: (item: NewContentItem) => number;
15
+ onChange: (id: number, item: ContentItem) => void;
16
+ };
17
+ //#endregion
18
+ //#region src/field.d.ts
19
+ type FieldRenderProps = {
20
+ ref: (element: Element | null) => void;
21
+ handleRef: (element: Element | null) => void;
22
+ id: number;
23
+ depth: number;
24
+ index: number;
25
+ parentId: number | null;
26
+ type: FieldType;
27
+ item: FieldContentItem;
28
+ isDragging: boolean;
29
+ isSelected: boolean;
30
+ onAdd: (item: NewContentItem) => number;
31
+ onRemove: () => void;
32
+ onChange: (item: ContentItem) => void;
33
+ };
34
+ //#endregion
35
+ //#region src/section.d.ts
36
+ type SectionRenderProps = {
37
+ ref: (element: Element | null) => void;
38
+ handleRef: (element: Element | null) => void;
39
+ id: number;
40
+ depth: number;
41
+ index: number;
42
+ parentId: number | null;
43
+ item: SectionContentItem;
44
+ isDragging: boolean;
45
+ isSelected: boolean;
46
+ onAdd: (item: NewContentItem) => number;
47
+ onRemove: () => void;
48
+ onChange: (item: ContentItem) => void;
49
+ };
50
+ //#endregion
51
+ //#region src/builder.d.ts
52
+ type FormBuilderProps = {
53
+ definition: FormDefinition;
54
+ container: FC<PropsWithChildren>;
55
+ section: FC<SectionRenderProps>;
56
+ field: FC<FieldRenderProps>;
57
+ addPlaceholder: FC<AddPlaceholderRenderProps>;
58
+ selectedId?: number | null;
59
+ onDefinitionChange?: (definition: FormDefinition) => void;
60
+ };
61
+ declare const FormBuilder: FC<FormBuilderProps>;
62
+ //#endregion
63
+ export { type AddPlaceholderRenderProps, type FieldRenderProps, FormBuilder, type NewContentItem, type SectionRenderProps };
64
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/core/types.ts","../src/add-placeholder.tsx","../src/field.tsx","../src/section.tsx","../src/builder.tsx"],"mappings":";;;;;;;KAKY,cAAA,GAAiB,IAAA,CAAK,gBAAA,UAA0B,IAAA,CAAK,kBAAA;;;KCCrD,yBAAA;EACR,KAAA;EACA,QAAA;EACA,KAAA,GAAQ,IAAA,EAAM,cAAA;EACd,QAAA,GAAW,EAAA,UAAY,IAAA,EAAM,WAAA;AAAA;;;KCFrB,gBAAA;EACR,GAAA,GAAM,OAAA,EAAS,OAAA;EACf,SAAA,GAAY,OAAA,EAAS,OAAA;EACrB,EAAA;EACA,KAAA;EACA,KAAA;EACA,QAAA;EACA,IAAA,EAAM,SAAA;EACN,IAAA,EAAM,gBAAA;EACN,UAAA;EACA,UAAA;EACA,KAAA,GAAQ,IAAA,EAAM,cAAA;EACd,QAAA;EACA,QAAA,GAAW,IAAA,EAAM,WAAA;AAAA;;;KCbT,kBAAA;EACR,GAAA,GAAM,OAAA,EAAS,OAAA;EACf,SAAA,GAAY,OAAA,EAAS,OAAA;EACrB,EAAA;EACA,KAAA;EACA,KAAA;EACA,QAAA;EACA,IAAA,EAAM,kBAAA;EACN,UAAA;EACA,UAAA;EACA,KAAA,GAAQ,IAAA,EAAM,cAAA;EACd,QAAA;EACA,QAAA,GAAW,IAAA,EAAM,WAAA;AAAA;;;KCThB,gBAAA;EACD,UAAA,EAAY,cAAA;EACZ,SAAA,EAAW,EAAA,CAAG,iBAAA;EACd,OAAA,EAAS,EAAA,CAAG,kBAAA;EACZ,KAAA,EAAO,EAAA,CAAG,gBAAA;EACV,cAAA,EAAgB,EAAA,CAAG,yBAAA;EACnB,UAAA;EACA,kBAAA,IAAsB,UAAA,EAAY,cAAA;AAAA;AAAA,cAGzB,WAAA,EAAa,EAAA,CAAG,gBAAA"}
@@ -0,0 +1,64 @@
1
+ import { FC, PropsWithChildren } from "react";
2
+ import { ContentItem, FieldContentItem, FieldType, FormDefinition, SectionContentItem } from "@bluprynt/forms-core";
3
+
4
+ //#region src/core/types.d.ts
5
+ /**
6
+ * A `ContentItem` without an `id` — used when adding new items (id is auto-generated).
7
+ */
8
+ type NewContentItem = Omit<FieldContentItem, 'id'> | Omit<SectionContentItem, 'id'>;
9
+ //#endregion
10
+ //#region src/add-placeholder.d.ts
11
+ type AddPlaceholderRenderProps = {
12
+ depth: number;
13
+ parentId: number | null;
14
+ onAdd: (item: NewContentItem) => number;
15
+ onChange: (id: number, item: ContentItem) => void;
16
+ };
17
+ //#endregion
18
+ //#region src/field.d.ts
19
+ type FieldRenderProps = {
20
+ ref: (element: Element | null) => void;
21
+ handleRef: (element: Element | null) => void;
22
+ id: number;
23
+ depth: number;
24
+ index: number;
25
+ parentId: number | null;
26
+ type: FieldType;
27
+ item: FieldContentItem;
28
+ isDragging: boolean;
29
+ isSelected: boolean;
30
+ onAdd: (item: NewContentItem) => number;
31
+ onRemove: () => void;
32
+ onChange: (item: ContentItem) => void;
33
+ };
34
+ //#endregion
35
+ //#region src/section.d.ts
36
+ type SectionRenderProps = {
37
+ ref: (element: Element | null) => void;
38
+ handleRef: (element: Element | null) => void;
39
+ id: number;
40
+ depth: number;
41
+ index: number;
42
+ parentId: number | null;
43
+ item: SectionContentItem;
44
+ isDragging: boolean;
45
+ isSelected: boolean;
46
+ onAdd: (item: NewContentItem) => number;
47
+ onRemove: () => void;
48
+ onChange: (item: ContentItem) => void;
49
+ };
50
+ //#endregion
51
+ //#region src/builder.d.ts
52
+ type FormBuilderProps = {
53
+ definition: FormDefinition;
54
+ container: FC<PropsWithChildren>;
55
+ section: FC<SectionRenderProps>;
56
+ field: FC<FieldRenderProps>;
57
+ addPlaceholder: FC<AddPlaceholderRenderProps>;
58
+ selectedId?: number | null;
59
+ onDefinitionChange?: (definition: FormDefinition) => void;
60
+ };
61
+ declare const FormBuilder: FC<FormBuilderProps>;
62
+ //#endregion
63
+ export { type AddPlaceholderRenderProps, type FieldRenderProps, FormBuilder, type NewContentItem, type SectionRenderProps };
64
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/core/types.ts","../src/add-placeholder.tsx","../src/field.tsx","../src/section.tsx","../src/builder.tsx"],"mappings":";;;;;;;KAKY,cAAA,GAAiB,IAAA,CAAK,gBAAA,UAA0B,IAAA,CAAK,kBAAA;;;KCCrD,yBAAA;EACR,KAAA;EACA,QAAA;EACA,KAAA,GAAQ,IAAA,EAAM,cAAA;EACd,QAAA,GAAW,EAAA,UAAY,IAAA,EAAM,WAAA;AAAA;;;KCFrB,gBAAA;EACR,GAAA,GAAM,OAAA,EAAS,OAAA;EACf,SAAA,GAAY,OAAA,EAAS,OAAA;EACrB,EAAA;EACA,KAAA;EACA,KAAA;EACA,QAAA;EACA,IAAA,EAAM,SAAA;EACN,IAAA,EAAM,gBAAA;EACN,UAAA;EACA,UAAA;EACA,KAAA,GAAQ,IAAA,EAAM,cAAA;EACd,QAAA;EACA,QAAA,GAAW,IAAA,EAAM,WAAA;AAAA;;;KCbT,kBAAA;EACR,GAAA,GAAM,OAAA,EAAS,OAAA;EACf,SAAA,GAAY,OAAA,EAAS,OAAA;EACrB,EAAA;EACA,KAAA;EACA,KAAA;EACA,QAAA;EACA,IAAA,EAAM,kBAAA;EACN,UAAA;EACA,UAAA;EACA,KAAA,GAAQ,IAAA,EAAM,cAAA;EACd,QAAA;EACA,QAAA,GAAW,IAAA,EAAM,WAAA;AAAA;;;KCThB,gBAAA;EACD,UAAA,EAAY,cAAA;EACZ,SAAA,EAAW,EAAA,CAAG,iBAAA;EACd,OAAA,EAAS,EAAA,CAAG,kBAAA;EACZ,KAAA,EAAO,EAAA,CAAG,gBAAA;EACV,cAAA,EAAgB,EAAA,CAAG,yBAAA;EACnB,UAAA;EACA,kBAAA,IAAsB,UAAA,EAAY,cAAA;AAAA;AAAA,cAGzB,WAAA,EAAa,EAAA,CAAG,gBAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,394 @@
1
+ import { DragDropProvider } from "@dnd-kit/react";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useSortable } from "@dnd-kit/react/sortable";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { FormDefinitionEditor } from "@bluprynt/forms-core";
6
+ import { move } from "@dnd-kit/helpers";
7
+ //#region src/add-placeholder.tsx
8
+ const AddPlaceholder = ({ depth, parentId, component: Component, onAdd, onChange }) => /* @__PURE__ */ jsx(Component, {
9
+ depth,
10
+ parentId,
11
+ onAdd,
12
+ onChange
13
+ });
14
+ //#endregion
15
+ //#region src/core/types.ts
16
+ /**
17
+ * Type guard that narrows a `TreeItem` to a section (`type` is `'section'`, `item` is `SectionContentItem`).
18
+ */
19
+ const isSection = (item) => item.type === "section";
20
+ /**
21
+ * Type guard that narrows a `TreeItem` to a field (`type` is not `'section'`, `item` is `FieldContentItem`).
22
+ */
23
+ const isField = (item) => item.type !== "section" && item.type !== "add-placeholder";
24
+ /**
25
+ * Type guard that narrows a `TreeItem` to an "add" placeholder.
26
+ */
27
+ const isAddPlaceholder = (item) => item.type === "add-placeholder";
28
+ //#endregion
29
+ //#region src/core/constants.ts
30
+ const SORTABLE_CONFIG = {
31
+ alignment: {
32
+ x: "start",
33
+ y: "center"
34
+ },
35
+ transition: { idle: true }
36
+ };
37
+ //#endregion
38
+ //#region src/field.tsx
39
+ const Field = ({ id, depth, index, parentId, type, item, isSelected, component: Component, onAdd, onRemove, onChange }) => {
40
+ const { ref, handleRef, isDragSource } = useSortable({
41
+ ...SORTABLE_CONFIG,
42
+ id,
43
+ index,
44
+ data: {
45
+ depth,
46
+ parentId
47
+ }
48
+ });
49
+ return /* @__PURE__ */ jsx(Component, {
50
+ ref,
51
+ handleRef,
52
+ id,
53
+ depth,
54
+ index,
55
+ parentId,
56
+ type,
57
+ item,
58
+ isDragging: isDragSource,
59
+ isSelected,
60
+ onAdd,
61
+ onRemove,
62
+ onChange
63
+ });
64
+ };
65
+ //#endregion
66
+ //#region src/section.tsx
67
+ const Section = ({ id, depth, index, parentId, item, isSelected, component: Component, onAdd, onRemove, onChange }) => {
68
+ const { ref, handleRef, isDragSource } = useSortable({
69
+ ...SORTABLE_CONFIG,
70
+ id,
71
+ index,
72
+ data: {
73
+ depth,
74
+ parentId
75
+ }
76
+ });
77
+ return /* @__PURE__ */ jsx(Component, {
78
+ ref,
79
+ handleRef,
80
+ id,
81
+ depth,
82
+ index,
83
+ parentId,
84
+ item,
85
+ isDragging: isDragSource,
86
+ isSelected,
87
+ onAdd,
88
+ onRemove,
89
+ onChange
90
+ });
91
+ };
92
+ //#endregion
93
+ //#region src/core/projection.ts
94
+ /**
95
+ * Converts a horizontal pixel offset into a depth level based on indentation width.
96
+ */
97
+ const getDragDepth = (offset, indentationWidth) => Math.round(offset / indentationWidth);
98
+ /**
99
+ * Returns the maximum allowed nesting depth when dragging above a target item.
100
+ * Only sections can accept nested children.
101
+ */
102
+ const getMaxDepth = (targetItem, previousItem) => {
103
+ if (!previousItem) return 0;
104
+ if (previousItem.type !== "section") return previousItem.depth;
105
+ return Math.min(targetItem.depth + 1, previousItem.depth + 1);
106
+ };
107
+ /**
108
+ * Returns the minimum depth based on the next sibling item.
109
+ * If there is no next item, the minimum depth is 0 (root level).
110
+ */
111
+ const getMinDepth = (nextItem) => nextItem ? nextItem.depth : 0;
112
+ /**
113
+ * Calculates the projected depth and parent for a dragged item relative to a target position.
114
+ * Clamps depth between min/max bounds and enforces `maxAllowedDepth`.
115
+ */
116
+ const getProjection = (items, targetId, projectedDepth, maxAllowedDepth) => {
117
+ const targetItemIndex = items.findIndex(({ id }) => id === targetId);
118
+ const previousItem = items[targetItemIndex - 1];
119
+ const targetItem = items[targetItemIndex];
120
+ const nextItem = items[targetItemIndex + 1];
121
+ if (!targetItem) return {
122
+ depth: 0,
123
+ maxDepth: 0,
124
+ minDepth: 0,
125
+ parentId: null
126
+ };
127
+ const maxDepth = getMaxDepth(targetItem, previousItem);
128
+ const minDepth = getMinDepth(nextItem);
129
+ let depth = projectedDepth;
130
+ if (depth >= maxDepth) depth = maxDepth;
131
+ else if (depth < minDepth) depth = minDepth;
132
+ if (depth > maxAllowedDepth) depth = maxAllowedDepth;
133
+ const getParentId = () => {
134
+ if (depth === 0 || !previousItem) return null;
135
+ if (depth === previousItem.depth) return previousItem.parentId;
136
+ if (depth > previousItem.depth) {
137
+ if (previousItem.type !== "section") return previousItem.parentId;
138
+ return previousItem.id;
139
+ }
140
+ return items.slice(0, targetItemIndex).reverse().find((item) => item.depth === depth)?.parentId ?? null;
141
+ };
142
+ return {
143
+ depth,
144
+ maxDepth,
145
+ minDepth,
146
+ parentId: getParentId()
147
+ };
148
+ };
149
+ //#endregion
150
+ //#region src/core/tree.ts
151
+ let placeholderIdCounter = -1e3;
152
+ /**
153
+ * Resets the placeholder id counter. Call before each full flatten to ensure stable ids per flatten pass.
154
+ */
155
+ const resetPlaceholderIds = () => {
156
+ placeholderIdCounter = -1e3;
157
+ };
158
+ const nextPlaceholderId = () => --placeholderIdCounter;
159
+ /**
160
+ * Flattens a nested content tree into a flat list of items with depth and parent info.
161
+ * Each item preserves its position metadata for rendering in a sortable tree.
162
+ * Appends an "add-placeholder" at the end of each section and at the root level.
163
+ */
164
+ const flattenTree = (items, parentId = null, depth = 0) => {
165
+ if (parentId === null) resetPlaceholderIds();
166
+ const result = items.reduce((result, current, index) => {
167
+ const isSection = current.type === "section";
168
+ result.push({
169
+ id: current.id,
170
+ type: current.type,
171
+ parentId,
172
+ depth,
173
+ index,
174
+ item: current
175
+ });
176
+ if (isSection) {
177
+ const children = current.content;
178
+ result.push(...flattenTree(children, current.id, depth + 1));
179
+ }
180
+ return result;
181
+ }, []);
182
+ result.push({
183
+ id: nextPlaceholderId(),
184
+ type: "add-placeholder",
185
+ parentId,
186
+ depth,
187
+ index: items.length,
188
+ item: void 0
189
+ });
190
+ return result;
191
+ };
192
+ /**
193
+ * Collects all transitive descendant ids for a given parent from a flat item list.
194
+ */
195
+ const getDescendants = (items, parentId) => {
196
+ return items.filter((item) => item.parentId === parentId).reduce((result, child) => {
197
+ result.add(child.id);
198
+ for (const descendant of getDescendants(items, child.id)) result.add(descendant);
199
+ return result;
200
+ }, /* @__PURE__ */ new Set());
201
+ };
202
+ //#endregion
203
+ //#region src/use-form-builder.ts
204
+ /**
205
+ * Manages the form builder state: flattened tree items, drag-and-drop reordering,
206
+ * item editing, and removal. Uses {@link FormDefinitionEditor} for all definition mutations
207
+ * and notifies the consumer of structural changes via {@link onDefinitionChange}.
208
+ *
209
+ * @param definition - The current form definition to build upon.
210
+ * @param onDefinitionChange - Optional callback invoked with the updated `FormDefinition`
211
+ * whenever the tree structure, item content, or item order changes.
212
+ * @returns Flattened items and handlers for item/drag operations.
213
+ */
214
+ const useFormBuilder = (definition, onDefinitionChange) => {
215
+ const [items, setItems] = useState(() => flattenTree(definition.content));
216
+ const initialDepth = useRef(0);
217
+ const sourceChildren = useRef([]);
218
+ const dragSourceId = useRef(null);
219
+ const definitionRef = useRef(definition);
220
+ definitionRef.current = definition;
221
+ const onDefinitionChangeRef = useRef(onDefinitionChange);
222
+ onDefinitionChangeRef.current = onDefinitionChange;
223
+ useEffect(() => {
224
+ setItems(flattenTree(definition.content));
225
+ }, [definition]);
226
+ const applyEditor = useCallback((editor) => {
227
+ const updated = editor.toJSON();
228
+ setItems(flattenTree(updated.content));
229
+ onDefinitionChangeRef.current?.(updated);
230
+ }, []);
231
+ const handleItemAdd = useCallback((sectionId, item) => {
232
+ const editor = new FormDefinitionEditor(definitionRef.current);
233
+ const parentId = sectionId ?? void 0;
234
+ const id = editor.nextId();
235
+ if (item.type === "section") editor.addSection({
236
+ ...item,
237
+ id
238
+ }, parentId);
239
+ else editor.addField({
240
+ ...item,
241
+ id
242
+ }, parentId);
243
+ applyEditor(editor);
244
+ return id;
245
+ }, [applyEditor]);
246
+ const handleItemChange = useCallback((id, newItem) => {
247
+ const editor = new FormDefinitionEditor(definitionRef.current);
248
+ if (newItem.type === "section") {
249
+ const { id: _, type: __, content: ___, ...updates } = newItem;
250
+ editor.updateSection(id, updates);
251
+ } else {
252
+ const { id: _, type: __, ...updates } = newItem;
253
+ editor.updateField(id, updates);
254
+ }
255
+ applyEditor(editor);
256
+ }, [applyEditor]);
257
+ const handleItemRemove = useCallback((itemId) => {
258
+ const editor = new FormDefinitionEditor(definitionRef.current);
259
+ editor.removeItem(itemId);
260
+ applyEditor(editor);
261
+ }, [applyEditor]);
262
+ const handleDragStart = (event) => {
263
+ const { source } = event.operation;
264
+ if (!source) return;
265
+ const item = items.find(({ id }) => id === source.id);
266
+ if (!item) return;
267
+ initialDepth.current = item.depth;
268
+ dragSourceId.current = source.id;
269
+ setItems((prev) => {
270
+ sourceChildren.current = [];
271
+ const descendants = getDescendants(prev, source.id);
272
+ return prev.filter((i) => {
273
+ if (descendants.has(i.id)) {
274
+ sourceChildren.current = [...sourceChildren.current, i];
275
+ return false;
276
+ }
277
+ return true;
278
+ });
279
+ });
280
+ };
281
+ const handleDragOver = (event, manager) => {
282
+ const { source, target } = event.operation;
283
+ event.preventDefault();
284
+ if (source && target && source.id !== target.id) setItems((prev) => {
285
+ const offsetLeft = manager.dragOperation.transform.x;
286
+ const dragDepth = getDragDepth(offsetLeft, 40);
287
+ const projectedDepth = initialDepth.current + dragDepth;
288
+ const { depth, parentId } = getProjection(prev, target.id, projectedDepth, 3);
289
+ return move(prev, event).map((item) => item.id === source.id ? {
290
+ ...item,
291
+ depth,
292
+ parentId
293
+ } : item);
294
+ });
295
+ };
296
+ const handleDragMove = (event, manager) => {
297
+ if (event.defaultPrevented) return;
298
+ const { source, target } = event.operation;
299
+ if (!source || !target) return;
300
+ const offsetLeft = manager.dragOperation.transform.x;
301
+ const dragDepth = getDragDepth(offsetLeft, 40);
302
+ const projectedDepth = initialDepth.current + dragDepth;
303
+ const { depth, parentId } = getProjection(items, source.id, projectedDepth, 3);
304
+ if (source.data?.depth !== depth || source.data?.parentId !== parentId) setItems((prev) => prev.map((item) => item.id === source.id ? {
305
+ ...item,
306
+ depth,
307
+ parentId
308
+ } : item));
309
+ };
310
+ const handleDragEnd = (event) => {
311
+ if (event.canceled) {
312
+ setItems(flattenTree(definitionRef.current.content));
313
+ dragSourceId.current = null;
314
+ sourceChildren.current = [];
315
+ return;
316
+ }
317
+ const draggedId = dragSourceId.current;
318
+ if (draggedId === null) return;
319
+ const draggedItem = items.find((i) => i.id === draggedId);
320
+ if (!draggedItem) return;
321
+ const targetParentId = draggedItem.parentId;
322
+ const draggedFlatIndex = items.findIndex((i) => i.id === draggedId);
323
+ let siblingIndex = 0;
324
+ for (let i = 0; i < draggedFlatIndex; i++) {
325
+ const current = items[i];
326
+ if (current && current.parentId === targetParentId && current.type !== "add-placeholder") siblingIndex++;
327
+ }
328
+ const editor = new FormDefinitionEditor(definitionRef.current);
329
+ editor.moveItem(draggedId, targetParentId ?? void 0, siblingIndex);
330
+ applyEditor(editor);
331
+ dragSourceId.current = null;
332
+ sourceChildren.current = [];
333
+ };
334
+ return {
335
+ items,
336
+ handleItemAdd,
337
+ handleItemChange,
338
+ handleItemRemove,
339
+ handleDragStart,
340
+ handleDragOver,
341
+ handleDragMove,
342
+ handleDragEnd
343
+ };
344
+ };
345
+ //#endregion
346
+ //#region src/builder.tsx
347
+ const FormBuilder = ({ definition, container: Container, section: SectionComponent, field: FieldComponent, addPlaceholder: AddPlaceholderComponent, selectedId, onDefinitionChange }) => {
348
+ const { items, handleItemAdd, handleItemChange, handleItemRemove, handleDragStart, handleDragOver, handleDragMove, handleDragEnd } = useFormBuilder(definition, onDefinitionChange);
349
+ return /* @__PURE__ */ jsx(DragDropProvider, {
350
+ onDragStart: handleDragStart,
351
+ onDragOver: handleDragOver,
352
+ onDragMove: handleDragMove,
353
+ onDragEnd: handleDragEnd,
354
+ children: /* @__PURE__ */ jsx(Container, { children: items.map((item, index) => {
355
+ const onAdd = (value) => handleItemAdd(item.type === "section" ? item.id : item.parentId, value);
356
+ const onRemove = () => handleItemRemove(item.id);
357
+ const onChange = (value) => handleItemChange(value.id, value);
358
+ return isSection(item) ? /* @__PURE__ */ jsx(Section, {
359
+ id: item.id,
360
+ depth: item.depth,
361
+ index,
362
+ parentId: item.parentId,
363
+ item: item.item,
364
+ isSelected: selectedId === item.id,
365
+ component: SectionComponent,
366
+ onAdd,
367
+ onRemove,
368
+ onChange
369
+ }, item.id) : isField(item) ? /* @__PURE__ */ jsx(Field, {
370
+ id: item.id,
371
+ depth: item.depth,
372
+ index,
373
+ parentId: item.parentId,
374
+ type: item.type,
375
+ item: item.item,
376
+ isSelected: selectedId === item.id,
377
+ component: FieldComponent,
378
+ onAdd,
379
+ onRemove,
380
+ onChange
381
+ }, item.id) : isAddPlaceholder(item) ? /* @__PURE__ */ jsx(AddPlaceholder, {
382
+ depth: item.depth,
383
+ parentId: item.parentId,
384
+ component: AddPlaceholderComponent,
385
+ onAdd,
386
+ onChange: handleItemChange
387
+ }, item.id) : null;
388
+ }) })
389
+ });
390
+ };
391
+ //#endregion
392
+ export { FormBuilder };
393
+
394
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/add-placeholder.tsx","../src/core/types.ts","../src/core/constants.ts","../src/field.tsx","../src/section.tsx","../src/core/projection.ts","../src/core/tree.ts","../src/use-form-builder.ts","../src/builder.tsx"],"sourcesContent":["import type { FC } from 'react'\n\nimport type { ContentItem } from '@bluprynt/forms-core'\n\nimport type { NewContentItem } from './core/types'\n\nexport type AddPlaceholderRenderProps = {\n depth: number\n parentId: number | null\n onAdd: (item: NewContentItem) => number\n onChange: (id: number, item: ContentItem) => void\n}\n\ntype AddPlaceholderProps = {\n depth: number\n parentId: number | null\n component: FC<AddPlaceholderRenderProps>\n onAdd: (item: NewContentItem) => number\n onChange: (id: number, item: ContentItem) => void\n}\n\nexport const AddPlaceholder: FC<AddPlaceholderProps> = ({ depth, parentId, component: Component, onAdd, onChange }) => (\n <Component depth={depth} parentId={parentId} onAdd={onAdd} onChange={onChange} />\n)\n","import type { ContentItem, ContentItemType, FieldContentItem, SectionContentItem } from '@bluprynt/forms-core'\n\n/**\n * A `ContentItem` without an `id` — used when adding new items (id is auto-generated).\n */\nexport type NewContentItem = Omit<FieldContentItem, 'id'> | Omit<SectionContentItem, 'id'>\n\n/**\n * Flat representation of a content item within the sortable tree.\n * Carries depth, parent reference, and the original `ContentItem` for rendering and reconstruction.\n */\nexport type TreeItem = {\n id: number\n type: ContentItemType | 'add-placeholder'\n parentId: number | null\n depth: number\n index: number\n collapsed?: boolean\n item: ContentItem | undefined\n onChange?: (item: ContentItem) => void\n}\n\n/**\n * Type guard that narrows a `TreeItem` to a section (`type` is `'section'`, `item` is `SectionContentItem`).\n */\nexport const isSection = (item: TreeItem): item is TreeItem & { type: 'section'; item: SectionContentItem } =>\n item.type === 'section'\n\n/**\n * Type guard that narrows a `TreeItem` to a field (`type` is not `'section'`, `item` is `FieldContentItem`).\n */\nexport const isField = (\n item: TreeItem,\n): item is TreeItem & { type: Exclude<ContentItemType, 'section'>; item: FieldContentItem } =>\n item.type !== 'section' && item.type !== 'add-placeholder'\n\n/**\n * Type guard that narrows a `TreeItem` to an \"add\" placeholder.\n */\nexport const isAddPlaceholder = (item: TreeItem): item is TreeItem & { type: 'add-placeholder'; item: undefined } =>\n item.type === 'add-placeholder'\n","export const INDENTATION = 40\nexport const MAX_DEPTH = 3\n\nexport const SORTABLE_CONFIG = {\n alignment: {\n x: 'start',\n y: 'center',\n },\n transition: {\n idle: true,\n },\n} as const\n","import type { FC } from 'react'\n\nimport type { ContentItem, FieldContentItem, FieldType } from '@bluprynt/forms-core'\nimport { useSortable } from '@dnd-kit/react/sortable'\n\nimport { SORTABLE_CONFIG } from './core/constants'\nimport type { NewContentItem } from './core/types'\n\nexport type FieldRenderProps = {\n ref: (element: Element | null) => void\n handleRef: (element: Element | null) => void\n id: number\n depth: number\n index: number\n parentId: number | null\n type: FieldType\n item: FieldContentItem\n isDragging: boolean\n isSelected: boolean\n onAdd: (item: NewContentItem) => number\n onRemove: () => void\n onChange: (item: ContentItem) => void\n}\n\ntype FieldProps = {\n id: number\n depth: number\n index: number\n parentId: number | null\n type: FieldType\n item: FieldContentItem\n isSelected: boolean\n component: FC<FieldRenderProps>\n onAdd: (item: NewContentItem) => number\n onRemove: () => void\n onChange: (item: ContentItem) => void\n}\n\nexport const Field: FC<FieldProps> = ({\n id,\n depth,\n index,\n parentId,\n type,\n item,\n isSelected,\n component: Component,\n onAdd,\n onRemove,\n onChange,\n}) => {\n const { ref, handleRef, isDragSource } = useSortable({\n ...SORTABLE_CONFIG,\n id,\n index,\n data: { depth, parentId },\n })\n\n return (\n <Component\n ref={ref}\n handleRef={handleRef}\n id={id}\n depth={depth}\n index={index}\n parentId={parentId}\n type={type}\n item={item}\n isDragging={isDragSource}\n isSelected={isSelected}\n onAdd={onAdd}\n onRemove={onRemove}\n onChange={onChange}\n />\n )\n}\n","import { type FC } from 'react'\n\nimport type { ContentItem, SectionContentItem } from '@bluprynt/forms-core'\nimport { useSortable } from '@dnd-kit/react/sortable'\n\nimport { SORTABLE_CONFIG } from './core/constants'\nimport type { NewContentItem } from './core/types'\n\nexport type SectionRenderProps = {\n ref: (element: Element | null) => void\n handleRef: (element: Element | null) => void\n id: number\n depth: number\n index: number\n parentId: number | null\n item: SectionContentItem\n isDragging: boolean\n isSelected: boolean\n onAdd: (item: NewContentItem) => number\n onRemove: () => void\n onChange: (item: ContentItem) => void\n}\n\ntype SectionProps = {\n id: number\n depth: number\n index: number\n parentId: number | null\n item: SectionContentItem\n isSelected: boolean\n component: FC<SectionRenderProps>\n onAdd: (item: NewContentItem) => number\n onRemove: () => void\n onChange: (item: ContentItem) => void\n}\n\nexport const Section: FC<SectionProps> = ({\n id,\n depth,\n index,\n parentId,\n item,\n isSelected,\n component: Component,\n onAdd,\n onRemove,\n onChange,\n}) => {\n const { ref, handleRef, isDragSource } = useSortable({\n ...SORTABLE_CONFIG,\n id,\n index,\n data: { depth, parentId },\n })\n\n return (\n <Component\n ref={ref}\n handleRef={handleRef}\n id={id}\n depth={depth}\n index={index}\n parentId={parentId}\n item={item}\n isDragging={isDragSource}\n isSelected={isSelected}\n onAdd={onAdd}\n onRemove={onRemove}\n onChange={onChange}\n />\n )\n}\n","import type { TreeItem } from './types'\n\n/**\n * Converts a horizontal pixel offset into a depth level based on indentation width.\n */\nexport const getDragDepth = (offset: number, indentationWidth: number): number => Math.round(offset / indentationWidth)\n\n/**\n * Returns the maximum allowed nesting depth when dragging above a target item.\n * Only sections can accept nested children.\n */\nconst getMaxDepth = (targetItem: TreeItem, previousItem: TreeItem | undefined): number => {\n if (!previousItem) return 0\n if (previousItem.type !== 'section') {\n return previousItem.depth\n }\n return Math.min(targetItem.depth + 1, previousItem.depth + 1)\n}\n\n/**\n * Returns the minimum depth based on the next sibling item.\n * If there is no next item, the minimum depth is 0 (root level).\n */\nconst getMinDepth = (nextItem: TreeItem | undefined): number => (nextItem ? nextItem.depth : 0)\n\n/**\n * Calculates the projected depth and parent for a dragged item relative to a target position.\n * Clamps depth between min/max bounds and enforces `maxAllowedDepth`.\n */\nexport const getProjection = (\n items: TreeItem[],\n targetId: number,\n projectedDepth: number,\n maxAllowedDepth: number,\n): { depth: number; maxDepth: number; minDepth: number; parentId: number | null } => {\n const targetItemIndex = items.findIndex(({ id }) => id === targetId)\n const previousItem = items[targetItemIndex - 1]\n const targetItem = items[targetItemIndex]\n const nextItem = items[targetItemIndex + 1]\n if (!targetItem) return { depth: 0, maxDepth: 0, minDepth: 0, parentId: null }\n\n const maxDepth = getMaxDepth(targetItem, previousItem)\n const minDepth = getMinDepth(nextItem)\n let depth = projectedDepth\n\n if (depth >= maxDepth) {\n depth = maxDepth\n } else if (depth < minDepth) {\n depth = minDepth\n }\n\n if (depth > maxAllowedDepth) {\n depth = maxAllowedDepth\n }\n\n const getParentId = (): number | null => {\n if (depth === 0 || !previousItem) {\n return null\n }\n\n if (depth === previousItem.depth) {\n return previousItem.parentId\n }\n\n if (depth > previousItem.depth) {\n if (previousItem.type !== 'section') {\n return previousItem.parentId\n }\n return previousItem.id\n }\n\n const newParent = items\n .slice(0, targetItemIndex)\n .reverse()\n .find((item) => item.depth === depth)?.parentId\n\n return newParent ?? null\n }\n\n return { depth, maxDepth, minDepth, parentId: getParentId() }\n}\n","import type { ContentItem } from '@bluprynt/forms-core'\n\nimport type { TreeItem } from './types'\n\nlet placeholderIdCounter = -1000\n\n/**\n * Resets the placeholder id counter. Call before each full flatten to ensure stable ids per flatten pass.\n */\nconst resetPlaceholderIds = () => {\n placeholderIdCounter = -1000\n}\n\nconst nextPlaceholderId = () => --placeholderIdCounter\n\n/**\n * Flattens a nested content tree into a flat list of items with depth and parent info.\n * Each item preserves its position metadata for rendering in a sortable tree.\n * Appends an \"add-placeholder\" at the end of each section and at the root level.\n */\nexport const flattenTree = (items: ContentItem[], parentId: number | null = null, depth = 0): TreeItem[] => {\n if (parentId === null) resetPlaceholderIds()\n\n const result = items.reduce<TreeItem[]>((result, current, index) => {\n const isSection = current.type === 'section'\n\n result.push({\n id: current.id,\n type: current.type,\n parentId,\n depth,\n index,\n item: current,\n })\n\n if (isSection) {\n const children = (current as { content: ContentItem[] }).content\n result.push(...flattenTree(children, current.id, depth + 1))\n }\n\n return result\n }, [])\n\n result.push({\n id: nextPlaceholderId(),\n type: 'add-placeholder',\n parentId,\n depth,\n index: items.length,\n item: undefined,\n })\n\n return result\n}\n\n/**\n * Collects all transitive descendant ids for a given parent from a flat item list.\n */\nexport const getDescendants = (items: TreeItem[], parentId: number): Set<number> => {\n const directChildren = items.filter((item) => item.parentId === parentId)\n\n return directChildren.reduce((result, child) => {\n result.add(child.id)\n for (const descendant of getDescendants(items, child.id)) {\n result.add(descendant)\n }\n return result\n }, new Set<number>())\n}\n","import { type ComponentProps, useCallback, useEffect, useRef, useState } from 'react'\n\nimport {\n type ContentItem,\n type FieldContentItem,\n type FormDefinition,\n FormDefinitionEditor,\n type SectionContentItem,\n} from '@bluprynt/forms-core'\nimport { move } from '@dnd-kit/helpers'\nimport { DragDropProvider } from '@dnd-kit/react'\n\nimport { INDENTATION, MAX_DEPTH } from './core/constants'\nimport { getDragDepth, getProjection } from './core/projection'\nimport { flattenTree, getDescendants } from './core/tree'\nimport type { NewContentItem, TreeItem } from './core/types'\n\n/**\n * Manages the form builder state: flattened tree items, drag-and-drop reordering,\n * item editing, and removal. Uses {@link FormDefinitionEditor} for all definition mutations\n * and notifies the consumer of structural changes via {@link onDefinitionChange}.\n *\n * @param definition - The current form definition to build upon.\n * @param onDefinitionChange - Optional callback invoked with the updated `FormDefinition`\n * whenever the tree structure, item content, or item order changes.\n * @returns Flattened items and handlers for item/drag operations.\n */\nexport const useFormBuilder = (\n definition: FormDefinition,\n onDefinitionChange?: (definition: FormDefinition) => void,\n): {\n items: TreeItem[]\n handleItemAdd: (sectionId: number | null, item: NewContentItem) => number\n handleItemChange: (id: number, newItem: ContentItem) => void\n handleItemRemove: (itemId: number) => void\n handleDragStart: ComponentProps<typeof DragDropProvider>['onDragStart']\n handleDragOver: ComponentProps<typeof DragDropProvider>['onDragOver']\n handleDragMove: ComponentProps<typeof DragDropProvider>['onDragMove']\n handleDragEnd: ComponentProps<typeof DragDropProvider>['onDragEnd']\n} => {\n const [items, setItems] = useState<TreeItem[]>(() => flattenTree(definition.content))\n const initialDepth = useRef(0)\n const sourceChildren = useRef<TreeItem[]>([])\n const dragSourceId = useRef<number | null>(null)\n\n const definitionRef = useRef(definition)\n definitionRef.current = definition\n\n const onDefinitionChangeRef = useRef(onDefinitionChange)\n onDefinitionChangeRef.current = onDefinitionChange\n\n // Re-sync items when definition changes externally (e.g. from field editor sheet)\n useEffect(() => {\n setItems(flattenTree(definition.content))\n }, [definition])\n\n const applyEditor = useCallback((editor: FormDefinitionEditor) => {\n const updated = editor.toJSON()\n setItems(flattenTree(updated.content))\n onDefinitionChangeRef.current?.(updated)\n }, [])\n\n const handleItemAdd = useCallback(\n (sectionId: number | null, item: NewContentItem): number => {\n const editor = new FormDefinitionEditor(definitionRef.current)\n const parentId = sectionId ?? undefined\n const id = editor.nextId()\n\n if (item.type === 'section') editor.addSection({ ...item, id }, parentId)\n else editor.addField({ ...item, id } as FieldContentItem, parentId)\n\n applyEditor(editor)\n return id\n },\n [applyEditor],\n )\n\n const handleItemChange = useCallback(\n (id: number, newItem: ContentItem) => {\n const editor = new FormDefinitionEditor(definitionRef.current)\n\n if (newItem.type === 'section') {\n const { id: _, type: __, content: ___, ...updates } = newItem as SectionContentItem\n editor.updateSection(id, updates)\n } else {\n const { id: _, type: __, ...updates } = newItem as FieldContentItem\n editor.updateField(id, updates)\n }\n\n applyEditor(editor)\n },\n [applyEditor],\n )\n\n const handleItemRemove = useCallback(\n (itemId: number) => {\n const editor = new FormDefinitionEditor(definitionRef.current)\n editor.removeItem(itemId)\n applyEditor(editor)\n },\n [applyEditor],\n )\n\n const handleDragStart: ComponentProps<typeof DragDropProvider>['onDragStart'] = (event) => {\n const { source } = event.operation\n if (!source) return\n\n const item = items.find(({ id }) => id === source.id)\n if (!item) return\n\n initialDepth.current = item.depth\n dragSourceId.current = source.id as number\n\n setItems((prev) => {\n sourceChildren.current = []\n const descendants = getDescendants(prev, source.id as number)\n\n return prev.filter((i) => {\n if (descendants.has(i.id)) {\n sourceChildren.current = [...sourceChildren.current, i]\n return false\n }\n return true\n })\n })\n }\n\n const handleDragOver: ComponentProps<typeof DragDropProvider>['onDragOver'] = (event, manager) => {\n const { source, target } = event.operation\n event.preventDefault()\n\n if (source && target && source.id !== target.id) {\n setItems((prev) => {\n const offsetLeft = manager.dragOperation.transform.x\n const dragDepth = getDragDepth(offsetLeft, INDENTATION)\n const projectedDepth = initialDepth.current + dragDepth\n\n const { depth, parentId } = getProjection(prev, target.id as number, projectedDepth, MAX_DEPTH)\n\n const sorted = move(prev, event)\n return sorted.map((item) => (item.id === source.id ? { ...item, depth, parentId } : item))\n })\n }\n }\n\n const handleDragMove: ComponentProps<typeof DragDropProvider>['onDragMove'] = (event, manager) => {\n if (event.defaultPrevented) return\n\n const { source, target } = event.operation\n if (!source || !target) return\n\n const offsetLeft = manager.dragOperation.transform.x\n const dragDepth = getDragDepth(offsetLeft, INDENTATION)\n const projectedDepth = initialDepth.current + dragDepth\n\n const { depth, parentId } = getProjection(items, source.id as number, projectedDepth, MAX_DEPTH)\n\n if (\n (source.data as { depth: number })?.depth !== depth ||\n (source.data as { parentId: number | null })?.parentId !== parentId\n ) {\n setItems((prev) => prev.map((item) => (item.id === source.id ? { ...item, depth, parentId } : item)))\n }\n }\n\n const handleDragEnd: ComponentProps<typeof DragDropProvider>['onDragEnd'] = (event) => {\n if (event.canceled) {\n setItems(flattenTree(definitionRef.current.content))\n dragSourceId.current = null\n sourceChildren.current = []\n return\n }\n\n const draggedId = dragSourceId.current\n if (draggedId === null) return\n\n const draggedItem = items.find((i) => i.id === draggedId)\n if (!draggedItem) return\n\n // Compute sibling index in the new parent\n const targetParentId = draggedItem.parentId\n const draggedFlatIndex = items.findIndex((i) => i.id === draggedId)\n let siblingIndex = 0\n for (let i = 0; i < draggedFlatIndex; i++) {\n const current = items[i]\n if (current && current.parentId === targetParentId && current.type !== 'add-placeholder') {\n siblingIndex++\n }\n }\n\n const editor = new FormDefinitionEditor(definitionRef.current)\n editor.moveItem(draggedId, targetParentId ?? undefined, siblingIndex)\n applyEditor(editor)\n\n dragSourceId.current = null\n sourceChildren.current = []\n }\n\n return {\n items,\n handleItemAdd,\n handleItemChange,\n handleItemRemove,\n handleDragStart,\n handleDragOver,\n handleDragMove,\n handleDragEnd,\n } as const\n}\n","import type { FC, PropsWithChildren } from 'react'\n\nimport type { ContentItem, FormDefinition } from '@bluprynt/forms-core'\nimport { DragDropProvider } from '@dnd-kit/react'\n\nimport { AddPlaceholder, type AddPlaceholderRenderProps } from './add-placeholder'\nimport { isAddPlaceholder, isField, isSection, NewContentItem } from './core/types'\nimport { Field, type FieldRenderProps } from './field'\nimport { Section, type SectionRenderProps } from './section'\nimport { useFormBuilder } from './use-form-builder'\n\ntype FormBuilderProps = {\n definition: FormDefinition\n container: FC<PropsWithChildren>\n section: FC<SectionRenderProps>\n field: FC<FieldRenderProps>\n addPlaceholder: FC<AddPlaceholderRenderProps>\n selectedId?: number | null\n onDefinitionChange?: (definition: FormDefinition) => void\n}\n\nexport const FormBuilder: FC<FormBuilderProps> = ({\n definition,\n container: Container,\n section: SectionComponent,\n field: FieldComponent,\n addPlaceholder: AddPlaceholderComponent,\n selectedId,\n onDefinitionChange,\n}) => {\n const {\n items,\n handleItemAdd,\n handleItemChange,\n handleItemRemove,\n handleDragStart,\n handleDragOver,\n handleDragMove,\n handleDragEnd,\n } = useFormBuilder(definition, onDefinitionChange)\n\n return (\n <DragDropProvider\n onDragStart={handleDragStart}\n onDragOver={handleDragOver}\n onDragMove={handleDragMove}\n onDragEnd={handleDragEnd}>\n <Container>\n {items.map((item, index) => {\n const onAdd = (value: NewContentItem) =>\n handleItemAdd(item.type === 'section' ? item.id : item.parentId, value)\n const onRemove = () => handleItemRemove(item.id)\n const onChange = (value: ContentItem) => handleItemChange(value.id, value)\n\n return isSection(item) ? (\n <Section\n key={item.id}\n id={item.id}\n depth={item.depth}\n index={index}\n parentId={item.parentId}\n item={item.item}\n isSelected={selectedId === item.id}\n component={SectionComponent}\n onAdd={onAdd}\n onRemove={onRemove}\n onChange={onChange}\n />\n ) : isField(item) ? (\n <Field\n key={item.id}\n id={item.id}\n depth={item.depth}\n index={index}\n parentId={item.parentId}\n type={item.type}\n item={item.item}\n isSelected={selectedId === item.id}\n component={FieldComponent}\n onAdd={onAdd}\n onRemove={onRemove}\n onChange={onChange}\n />\n ) : isAddPlaceholder(item) ? (\n <AddPlaceholder\n key={item.id}\n depth={item.depth}\n parentId={item.parentId}\n component={AddPlaceholderComponent}\n onAdd={onAdd}\n onChange={handleItemChange}\n />\n ) : null\n })}\n </Container>\n </DragDropProvider>\n )\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@bluprynt/forms-builder",
3
+ "version": "1.0.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/blupryntco/forms-engine.git",
7
+ "directory": "packages/builder"
8
+ },
9
+ "homepage": "https://blupryntco.github.io/forms-engine/",
10
+ "sideEffects": false,
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "main": "./dist/index.cjs",
19
+ "module": "./dist/index.mjs",
20
+ "types": "./dist/index.d.ts",
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "peerDependencies": {
25
+ "@dnd-kit/abstract": "^0.3.0",
26
+ "@dnd-kit/dom": "^0.3.0",
27
+ "@dnd-kit/helpers": "^0.3.0",
28
+ "@dnd-kit/react": "^0.3.0",
29
+ "@dnd-kit/state": "^0.3.0",
30
+ "react": "^18.0.0 || ^19.0.0",
31
+ "react-dom": "^18.0.0 || ^19.0.0",
32
+ "@bluprynt/forms-core": "1.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@dnd-kit/abstract": "0.3.2",
36
+ "@dnd-kit/dom": "0.3.2",
37
+ "@dnd-kit/helpers": "0.3.2",
38
+ "@dnd-kit/react": "0.3.2",
39
+ "@dnd-kit/state": "0.3.2",
40
+ "@testing-library/react": "16.3.2",
41
+ "@types/jest": "29.5.14",
42
+ "@types/react": "19.1.8",
43
+ "@types/react-dom": "19.1.6",
44
+ "jest": "29.7.0",
45
+ "jest-environment-jsdom": "30.3.0",
46
+ "react": "19.1.0",
47
+ "react-dom": "19.1.0",
48
+ "ts-jest": "29.4.6",
49
+ "tsdown": "0.21.1",
50
+ "typescript": "5.9.3",
51
+ "@bluprynt/forms-core": "1.0.0",
52
+ "@repo/typescript-config": "0.0.0"
53
+ },
54
+ "scripts": {
55
+ "build": "tsdown",
56
+ "dev": "tsdown --watch",
57
+ "lint": "biome check .",
58
+ "check-types": "tsc --noEmit",
59
+ "test": "jest",
60
+ "test:watch": "jest --watch",
61
+ "test:cov": "jest --coverage"
62
+ }
63
+ }