@esheet/builder 0.0.3 → 0.0.4-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 (83) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +16 -143
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +11252 -6751
  5. package/dist/lib/EsheetBuilder.d.ts +46 -0
  6. package/dist/lib/EsheetBuilder.d.ts.map +1 -0
  7. package/dist/lib/EsheetBuilder.js +111 -0
  8. package/dist/lib/builder-tools.d.ts +107 -0
  9. package/dist/lib/builder-tools.d.ts.map +1 -0
  10. package/dist/lib/builder-tools.js +321 -0
  11. package/dist/lib/components/BuilderHeader.d.ts +7 -0
  12. package/dist/lib/components/BuilderHeader.d.ts.map +1 -0
  13. package/dist/lib/components/BuilderHeader.js +390 -0
  14. package/dist/lib/components/Canvas.d.ts +12 -0
  15. package/dist/lib/components/Canvas.d.ts.map +1 -0
  16. package/dist/lib/components/Canvas.js +288 -0
  17. package/dist/lib/components/CodeView.d.ts +12 -0
  18. package/dist/lib/components/CodeView.d.ts.map +1 -0
  19. package/dist/lib/components/CodeView.js +197 -0
  20. package/dist/lib/components/FeedbackModal.d.ts +23 -0
  21. package/dist/lib/components/FeedbackModal.d.ts.map +1 -0
  22. package/dist/lib/components/FeedbackModal.js +22 -0
  23. package/dist/lib/components/FieldWrapper.d.ts +53 -0
  24. package/dist/lib/components/FieldWrapper.d.ts.map +1 -0
  25. package/dist/lib/components/FieldWrapper.js +152 -0
  26. package/dist/lib/components/MobileBottomDrawer.d.ts +9 -0
  27. package/dist/lib/components/MobileBottomDrawer.d.ts.map +1 -0
  28. package/dist/lib/components/MobileBottomDrawer.js +6 -0
  29. package/dist/lib/components/ToolPanel.d.ts +11 -0
  30. package/dist/lib/components/ToolPanel.d.ts.map +1 -0
  31. package/dist/lib/components/ToolPanel.js +117 -0
  32. package/dist/lib/components/edit-panel/CommonEditor.d.ts +16 -0
  33. package/dist/lib/components/edit-panel/CommonEditor.d.ts.map +1 -0
  34. package/dist/lib/components/edit-panel/CommonEditor.js +16 -0
  35. package/dist/lib/components/edit-panel/DraftIdEditor.d.ts +16 -0
  36. package/dist/lib/components/edit-panel/DraftIdEditor.d.ts.map +1 -0
  37. package/dist/lib/components/edit-panel/DraftIdEditor.js +32 -0
  38. package/dist/lib/components/edit-panel/EditPanel.d.ts +10 -0
  39. package/dist/lib/components/edit-panel/EditPanel.d.ts.map +1 -0
  40. package/dist/lib/components/edit-panel/EditPanel.js +156 -0
  41. package/dist/lib/components/edit-panel/InputTypeEditor.d.ts +16 -0
  42. package/dist/lib/components/edit-panel/InputTypeEditor.d.ts.map +1 -0
  43. package/dist/lib/components/edit-panel/InputTypeEditor.js +32 -0
  44. package/dist/lib/components/edit-panel/LogicEditor.d.ts +15 -0
  45. package/dist/lib/components/edit-panel/LogicEditor.d.ts.map +1 -0
  46. package/dist/lib/components/edit-panel/LogicEditor.js +392 -0
  47. package/dist/lib/components/edit-panel/MatrixEditor.d.ts +12 -0
  48. package/dist/lib/components/edit-panel/MatrixEditor.d.ts.map +1 -0
  49. package/dist/lib/components/edit-panel/MatrixEditor.js +32 -0
  50. package/dist/lib/components/edit-panel/OptionListEditor.d.ts +14 -0
  51. package/dist/lib/components/edit-panel/OptionListEditor.d.ts.map +1 -0
  52. package/dist/lib/components/edit-panel/OptionListEditor.js +27 -0
  53. package/dist/lib/hooks/useFormApi.d.ts +55 -0
  54. package/dist/lib/hooks/useFormApi.d.ts.map +1 -0
  55. package/dist/lib/hooks/useFormApi.js +80 -0
  56. package/dist/lib/hooks/useUiApi.d.ts +22 -0
  57. package/dist/lib/hooks/useUiApi.d.ts.map +1 -0
  58. package/dist/lib/hooks/useUiApi.js +37 -0
  59. package/dist/lib/hooks/useVisibleRootIds.d.ts +11 -0
  60. package/dist/lib/hooks/useVisibleRootIds.d.ts.map +1 -0
  61. package/dist/lib/hooks/useVisibleRootIds.js +32 -0
  62. package/dist/lib/icons.d.ts +31 -0
  63. package/dist/lib/icons.d.ts.map +1 -0
  64. package/dist/lib/icons.js +47 -0
  65. package/dist/lib/mcp/index.d.ts +7 -0
  66. package/dist/lib/mcp/index.d.ts.map +1 -0
  67. package/dist/lib/mcp/index.js +4 -0
  68. package/dist/lib/mcp/system-prompt.d.ts +2 -0
  69. package/dist/lib/mcp/system-prompt.d.ts.map +1 -0
  70. package/dist/lib/mcp/system-prompt.js +9 -0
  71. package/dist/lib/mcp/tool-definitions.d.ts +10 -0
  72. package/dist/lib/mcp/tool-definitions.d.ts.map +1 -0
  73. package/dist/lib/mcp/tool-definitions.js +436 -0
  74. package/dist/lib/mcp/tool-executor.d.ts +5 -0
  75. package/dist/lib/mcp/tool-executor.d.ts.map +1 -0
  76. package/dist/lib/mcp/tool-executor.js +482 -0
  77. package/dist/lib/mcp/useBuilderToolBridge.d.ts +31 -0
  78. package/dist/lib/mcp/useBuilderToolBridge.d.ts.map +1 -0
  79. package/dist/lib/mcp/useBuilderToolBridge.js +56 -0
  80. package/dist/lib/register-defaults.d.ts +2 -0
  81. package/dist/lib/register-defaults.d.ts.map +1 -0
  82. package/dist/lib/register-defaults.js +37 -0
  83. package/package.json +7 -5
@@ -0,0 +1,288 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import Sortable from 'sortablejs';
4
+ import { useFormApi } from '../hooks/useFormApi.js';
5
+ import { useUiApi } from '../hooks/useUiApi.js';
6
+ import { useVisibleRootIds } from '../hooks/useVisibleRootIds.js';
7
+ import { FieldWrapper } from './FieldWrapper.js';
8
+ import { getFieldComponent } from '@esheet/fields';
9
+ import { ViewBigIcon, ViewSmallIcon } from '../icons.js';
10
+ // ---------------------------------------------------------------------------
11
+ // DraggableFieldItem — each field is both draggable and a drop target
12
+ // ---------------------------------------------------------------------------
13
+ function DraggableFieldItem({ id, form, ui, parentId, dragEnabled, isSelected = false, isActiveChild = false, forceExpandVersion, forceCollapseVersion, nestedChildren, }) {
14
+ const handleRef = React.useRef(null);
15
+ const field = form.getState().getField(id);
16
+ const handleSelectOverride = React.useCallback((e) => {
17
+ if (!parentId)
18
+ return;
19
+ e.stopPropagation();
20
+ ui.getState().selectFieldChild(parentId, id);
21
+ }, [id, parentId, ui]);
22
+ if (!field)
23
+ return null;
24
+ return (_jsx("div", { className: "field-canvas-wrapper ms:relative ms:pb-1 ms:last:pb-0", "data-field-id": id, "data-field-type": field.definition.fieldType, "data-selected": isSelected ? 'true' : 'false', children: _jsx(FieldWrapper, { fieldId: id, form: form, ui: ui, dragHandleRef: handleRef, forceExpandVersion: forceExpandVersion, forceCollapseVersion: forceCollapseVersion, isSelectedOverride: parentId ? isActiveChild : undefined, onSelectOverride: parentId ? handleSelectOverride : undefined, selectedVariant: parentId ? 'nested' : 'default', children: (props) => {
25
+ const Component = getFieldComponent(props.field.definition.fieldType);
26
+ if (props.field.definition.fieldType === 'section') {
27
+ const SectionComponent = Component;
28
+ return (_jsx(SectionComponent, { ...props, nestedChildren: nestedChildren }));
29
+ }
30
+ return _jsx(Component, { ...props });
31
+ } }) }));
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // Canvas — main field list panel with Sheet DnD
35
+ // ---------------------------------------------------------------------------
36
+ export const Canvas = React.memo(function Canvas({ form, ui, dragEnabled = true, }) {
37
+ const canvasRef = React.useRef(null);
38
+ const rootIds = useVisibleRootIds();
39
+ const { normalized, responses } = useFormApi();
40
+ const { mode, selectedFieldId, selectedFieldChildId } = useUiApi();
41
+ const [sectionExpandSignal, setSectionExpandSignal] = React.useState(null);
42
+ const [expandAllVersion, setExpandAllVersion] = React.useState(undefined);
43
+ const [collapseAllVersion, setCollapseAllVersion] = React.useState(undefined);
44
+ const [allExpanded, setAllExpanded] = React.useState(false);
45
+ const normalizedRef = React.useRef(normalized);
46
+ React.useEffect(() => {
47
+ normalizedRef.current = normalized;
48
+ }, [normalized]);
49
+ // Clear drag state when mode changes or modal closes
50
+ React.useEffect(() => {
51
+ ui.getState().clearDragState();
52
+ }, [dragEnabled, ui]);
53
+ // SortableJS setup for root and section child lists.
54
+ // Re-runs whenever `normalized` changes so newly added section child
55
+ // containers always get their own Sortable instance.
56
+ React.useEffect(() => {
57
+ const el = canvasRef.current;
58
+ if (!el || !dragEnabled)
59
+ return;
60
+ const resolveScrollContainer = (fromEl) => {
61
+ let node = fromEl;
62
+ while (node) {
63
+ const style = window.getComputedStyle(node);
64
+ const canScrollY = style.overflowY === 'auto' ||
65
+ style.overflowY === 'scroll' ||
66
+ style.overflowY === 'overlay';
67
+ if (canScrollY && node.scrollHeight > node.clientHeight)
68
+ return node;
69
+ node = node.parentElement;
70
+ }
71
+ return document.scrollingElement instanceof HTMLElement
72
+ ? document.scrollingElement
73
+ : null;
74
+ };
75
+ const getParentId = (listEl) => {
76
+ const attr = listEl.getAttribute('data-parent-id');
77
+ return attr && attr.length > 0 ? attr : null;
78
+ };
79
+ const restoreDomToSource = (evt) => {
80
+ if (typeof evt.oldIndex !== 'number')
81
+ return;
82
+ const { item, from: sourceList, to: targetList } = evt;
83
+ if (sourceList !== targetList && item.parentElement === targetList) {
84
+ targetList.removeChild(item);
85
+ }
86
+ const clamped = Math.max(0, Math.min(evt.oldIndex, sourceList.children.length));
87
+ const ref = sourceList.children.item(clamped);
88
+ if (ref)
89
+ sourceList.insertBefore(item, ref);
90
+ else
91
+ sourceList.appendChild(item);
92
+ };
93
+ const listEls = [
94
+ el,
95
+ ...Array.from(el.querySelectorAll('[data-sortable-list="true"]')),
96
+ ];
97
+ const instances = listEls.map((listEl) => {
98
+ const scrollContainer = resolveScrollContainer(listEl);
99
+ return Sortable.create(listEl, {
100
+ group: 'builder-fields',
101
+ handle: '.drag-handle',
102
+ draggable: '.field-canvas-wrapper',
103
+ dataIdAttr: 'data-field-id',
104
+ animation: 150,
105
+ forceFallback: true,
106
+ fallbackOnBody: true,
107
+ fallbackTolerance: 3,
108
+ scroll: scrollContainer ?? true,
109
+ bubbleScroll: scrollContainer === null,
110
+ forceAutoScrollFallback: true,
111
+ scrollSensitivity: 220,
112
+ scrollSpeed: 13,
113
+ invertSwap: true,
114
+ swapThreshold: getParentId(listEl) !== null ? 0.21 : 0.5,
115
+ invertedSwapThreshold: getParentId(listEl) !== null ? 0.21 : 0.5,
116
+ emptyInsertThreshold: getParentId(listEl) !== null ? 40 : 18,
117
+ onChoose: (evt) => {
118
+ const sourceId = evt.item.getAttribute('data-field-id');
119
+ if (!sourceId)
120
+ return;
121
+ const sourceNode = normalizedRef.current.byId[sourceId];
122
+ if (sourceNode?.parentId) {
123
+ ui.getState().selectFieldChild(sourceNode.parentId, sourceId);
124
+ }
125
+ else {
126
+ ui.getState().selectField(sourceId);
127
+ }
128
+ },
129
+ onMove: (evt) => {
130
+ // Toggle placeholder visibility without triggering a React re-render.
131
+ // A re-render here would shift DOM indices and break restoreDomToSource.
132
+ // Reset ALL placeholders first so any section we just left is restored.
133
+ el.querySelectorAll('.section-empty-placeholder').forEach((ph) => {
134
+ ph.style.display = '';
135
+ });
136
+ // Hide placeholder in the list currently being dragged into.
137
+ if (getParentId(evt.to) !== null) {
138
+ const ph = evt.to.querySelector('.section-empty-placeholder');
139
+ if (ph)
140
+ ph.style.display = 'none';
141
+ }
142
+ // Show placeholder when dragging the last child out of a section.
143
+ if (evt.from !== evt.to &&
144
+ getParentId(evt.from) !== null &&
145
+ evt.from.querySelectorAll('.field-canvas-wrapper').length <= 1) {
146
+ const ph = evt.from.querySelector('.section-empty-placeholder');
147
+ if (ph)
148
+ ph.style.display = '';
149
+ }
150
+ return true;
151
+ },
152
+ onEnd: (evt) => {
153
+ // Clear any inline display overrides set during drag.
154
+ el.querySelectorAll('.section-empty-placeholder').forEach((ph) => {
155
+ ph.style.display = '';
156
+ });
157
+ const sourceId = evt.item.getAttribute('data-field-id');
158
+ if (!sourceId)
159
+ return;
160
+ const newIndex = evt.newDraggableIndex ?? evt.newIndex;
161
+ const oldIndex = evt.oldDraggableIndex ?? evt.oldIndex;
162
+ if (typeof newIndex !== 'number')
163
+ return;
164
+ const fromParentId = getParentId(evt.from);
165
+ const toParentId = getParentId(evt.to);
166
+ // No-op: dropped back in the same position
167
+ if (fromParentId === toParentId && oldIndex === newIndex)
168
+ return;
169
+ // Undo Sortable's DOM move so React can own the placement
170
+ restoreDomToSource(evt);
171
+ form.getState().moveField(sourceId, newIndex, toParentId);
172
+ // Update selection to follow the moved field to its new location.
173
+ if (toParentId !== null) {
174
+ setSectionExpandSignal((prev) => ({
175
+ sectionId: toParentId,
176
+ version: (prev?.version ?? 0) + 1,
177
+ }));
178
+ ui.getState().selectFieldChild(toParentId, sourceId);
179
+ }
180
+ else {
181
+ ui.getState().selectField(sourceId);
182
+ }
183
+ ui.getState().clearDragState();
184
+ },
185
+ });
186
+ });
187
+ return () => {
188
+ for (const instance of instances)
189
+ instance.destroy();
190
+ };
191
+ }, [dragEnabled, form, normalized, ui]);
192
+ // Preview-only renderability map
193
+ const previewRenderableMap = React.useMemo(() => {
194
+ if (mode !== 'preview')
195
+ return null;
196
+ const cache = new Map();
197
+ const visit = (fieldId) => {
198
+ const cached = cache.get(fieldId);
199
+ if (cached !== undefined)
200
+ return cached;
201
+ const isVisible = form.getState().isVisible(fieldId);
202
+ if (!isVisible) {
203
+ cache.set(fieldId, false);
204
+ return false;
205
+ }
206
+ const node = normalized.byId[fieldId];
207
+ if (!node) {
208
+ cache.set(fieldId, false);
209
+ return false;
210
+ }
211
+ if (node.definition.fieldType !== 'section') {
212
+ cache.set(fieldId, true);
213
+ return true;
214
+ }
215
+ const hasRenderableChild = node.childIds.some((childId) => visit(childId));
216
+ cache.set(fieldId, hasRenderableChild);
217
+ return hasRenderableChild;
218
+ };
219
+ for (const id of Object.keys(normalized.byId)) {
220
+ visit(id);
221
+ }
222
+ return cache;
223
+ }, [form, mode, normalized, responses]);
224
+ const items = React.useMemo(() => {
225
+ if (mode !== 'preview' || !previewRenderableMap)
226
+ return [...rootIds];
227
+ return rootIds.filter((id) => previewRenderableMap.get(id) === true);
228
+ }, [mode, previewRenderableMap, rootIds]);
229
+ const getVisibleChildIds = React.useCallback((parentId) => {
230
+ const parent = normalized.byId[parentId];
231
+ if (!parent || parent.childIds.length === 0)
232
+ return [];
233
+ if (mode !== 'preview')
234
+ return parent.childIds;
235
+ return parent.childIds.filter((childId) => previewRenderableMap?.get(childId) === true);
236
+ }, [mode, normalized, previewRenderableMap]);
237
+ const renderNestedChildren = React.useCallback((parentId, depth = 1) => {
238
+ const parent = normalized.byId[parentId];
239
+ if (!parent || parent.definition.fieldType !== 'section')
240
+ return null;
241
+ const childIds = getVisibleChildIds(parentId);
242
+ if (mode === 'preview' && childIds.length === 0)
243
+ return null;
244
+ const isEmpty = mode !== 'preview' && childIds.length === 0;
245
+ const containerClass = depth === 1
246
+ ? 'section-children'
247
+ : 'section-children ms:border-l ms:border-msborder ms:pl-3';
248
+ const emptyClass = isEmpty
249
+ ? ' ms:rounded-lg ms:border-2 ms:border-dashed ms:border-msprimary/30 ms:bg-gradient-to-br ms:from-msbackground ms:to-msbackgroundsecondary'
250
+ : ' ms:min-h-[2rem]';
251
+ return (_jsxs("div", { className: `${containerClass}${emptyClass}`, "data-depth": depth, "data-sortable-list": dragEnabled ? 'true' : undefined, "data-parent-id": parentId, children: [isEmpty && (_jsxs("div", { className: "section-empty-placeholder ms:flex ms:flex-col ms:items-center ms:justify-center ms:p-8 ms:text-center ms:pointer-events-none ms:select-none", children: [_jsx("p", { className: "ms:text-sm ms:font-semibold ms:text-mstext ms:mb-2", children: "No fields in this section" }), _jsx("p", { className: "ms:text-xs ms:text-mstextmuted ms:leading-relaxed", children: "Use the Tool Panel on the left to add fields." })] })), childIds.map((childId) => (_jsx(DraggableFieldItem, { id: childId, form: form, ui: ui, parentId: parentId, dragEnabled: dragEnabled, isSelected: selectedFieldId === parentId && selectedFieldChildId === childId, isActiveChild: selectedFieldId === parentId && selectedFieldChildId === childId, forceExpandVersion: sectionExpandSignal?.sectionId === childId
252
+ ? sectionExpandSignal.version
253
+ : expandAllVersion, forceCollapseVersion: collapseAllVersion, nestedChildren: renderNestedChildren(childId, depth + 1) }, childId)))] }));
254
+ }, [
255
+ collapseAllVersion,
256
+ dragEnabled,
257
+ expandAllVersion,
258
+ form,
259
+ getVisibleChildIds,
260
+ sectionExpandSignal,
261
+ selectedFieldChildId,
262
+ selectedFieldId,
263
+ ui,
264
+ ]);
265
+ // Clear selection on Escape key
266
+ React.useEffect(() => {
267
+ const handleKeyDown = (e) => {
268
+ if (e.key === 'Escape') {
269
+ ui.getState().selectField(null);
270
+ ui.getState().clearDragState();
271
+ }
272
+ };
273
+ document.addEventListener('keydown', handleKeyDown);
274
+ return () => document.removeEventListener('keydown', handleKeyDown);
275
+ }, [ui]);
276
+ return (_jsxs("div", { className: "ms:flex ms:flex-col ms:flex-1 ms:min-h-0", children: [mode === 'build' && (_jsxs("div", { className: "ms:bg-mssurface ms:border-b ms:border-msborder ms:px-4 ms:py-4 ms:flex ms:items-center ms:justify-between ms:gap-2", children: [_jsx("span", { className: "ms:text-sm ms:font-semibold ms:text-mstext ms:select-none", children: "Fields" }), items.length > 0 && (_jsx("div", { className: "ms:flex ms:items-center ms:gap-1", children: _jsxs("button", { type: "button", title: allExpanded ? 'Collapse all' : 'Expand all', className: "ms:flex ms:items-center ms:gap-1 ms:px-2 ms:py-1 ms:text-xs ms:text-mstextmuted ms:hover:text-mstext ms:rounded ms:hover:bg-msbackgroundhover ms:transition-colors", onClick: (e) => {
277
+ e.stopPropagation();
278
+ if (allExpanded) {
279
+ setCollapseAllVersion((v) => (v ?? 0) + 1);
280
+ }
281
+ else {
282
+ setExpandAllVersion((v) => (v ?? 0) + 1);
283
+ }
284
+ setAllExpanded((v) => !v);
285
+ }, children: [allExpanded ? (_jsx(ViewSmallIcon, { className: "ms:w-3.5 ms:h-3.5" })) : (_jsx(ViewBigIcon, { className: "ms:w-3.5 ms:h-3.5" })), allExpanded ? 'Collapse all' : 'Expand all'] }) }))] })), items.length === 0 ? (_jsx("div", { className: "canvas-empty ms:flex ms:flex-1 ms:items-center ms:justify-center ms:min-h-[200px] ms:text-mstextmuted ms:text-sm", children: "No fields yet. Add a field from the Tool Panel to get started." })) : (_jsx("div", { ref: canvasRef, className: "canvas-fields ms:space-y-0 ms:flex-1 ms:min-h-0 ms:overflow-y-auto ms:px-4 ms:pt-3 ms:pb-4", "data-sortable-list": dragEnabled ? 'true' : undefined, "data-parent-id": "", children: items.map((id) => (_jsx(DraggableFieldItem, { id: id, form: form, ui: ui, dragEnabled: dragEnabled, isSelected: selectedFieldId === id && selectedFieldChildId === null, forceExpandVersion: sectionExpandSignal?.sectionId === id
286
+ ? sectionExpandSignal.version
287
+ : expandAllVersion, forceCollapseVersion: collapseAllVersion, nestedChildren: renderNestedChildren(id) }, id))) }))] }));
288
+ });
@@ -0,0 +1,12 @@
1
+ import { type FormStore, type UIStore } from '@esheet/core';
2
+ export interface CodeViewProps {
3
+ form: FormStore;
4
+ ui: UIStore;
5
+ }
6
+ /**
7
+ * CodeView — Monaco-based JSON/YAML editor for the form definition.
8
+ *
9
+ * Serialize on mount, live-validate on edit, auto-save on unmount.
10
+ */
11
+ export declare function CodeView({ form, ui }: CodeViewProps): import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=CodeView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CodeView.d.ts","sourceRoot":"","sources":["../../../src/lib/components/CodeView.tsx"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,OAAO,EACb,MAAM,cAAc,CAAC;AAWtB,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,EAAE,OAAO,CAAC;CACb;AAgDD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,aAAa,2CA6OnD"}
@@ -0,0 +1,197 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Editor } from '@monaco-editor/react';
4
+ import YAML from 'js-yaml';
5
+ import { formatZodValidationError, formDefinitionJSONSchema, formDefinitionSchema, } from '@esheet/core';
6
+ import { FeedbackModal } from './FeedbackModal.js';
7
+ const FORM_SCHEMA_URI = 'inmemory://esheet/form-definition.schema.json';
8
+ /** Detect dark mode from the document root and re-render on changes. */
9
+ function useIsDark() {
10
+ const [isDark, setIsDark] = React.useState(() => document.documentElement.classList.contains('dark'));
11
+ React.useEffect(() => {
12
+ const observer = new MutationObserver(() => {
13
+ setIsDark(document.documentElement.classList.contains('dark'));
14
+ });
15
+ observer.observe(document.documentElement, {
16
+ attributes: true,
17
+ attributeFilter: ['class'],
18
+ });
19
+ return () => observer.disconnect();
20
+ }, []);
21
+ return isDark;
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+ function serialize(data, format) {
27
+ return format === 'json'
28
+ ? JSON.stringify(data, null, 2)
29
+ : YAML.dump(data, { indent: 2, lineWidth: -1 });
30
+ }
31
+ function parse(text, format) {
32
+ return format === 'json' ? JSON.parse(text) : YAML.load(text);
33
+ }
34
+ // ---------------------------------------------------------------------------
35
+ // Component
36
+ // ---------------------------------------------------------------------------
37
+ /**
38
+ * CodeView — Monaco-based JSON/YAML editor for the form definition.
39
+ *
40
+ * Serialize on mount, live-validate on edit, auto-save on unmount.
41
+ */
42
+ export function CodeView({ form, ui }) {
43
+ // Track whether user actually edited (avoids spurious saves from StrictMode double-mount)
44
+ const dirtyRef = React.useRef(false);
45
+ const isDark = useIsDark();
46
+ const [format, setFormat] = React.useState('yaml');
47
+ const initialCode = React.useMemo(() => {
48
+ try {
49
+ return serialize(form.getState().hydrateDefinition(), 'yaml');
50
+ }
51
+ catch {
52
+ return '';
53
+ }
54
+ }, []);
55
+ const codeRef = React.useRef(initialCode);
56
+ const formatRef = React.useRef('yaml');
57
+ const formRef = React.useRef(form);
58
+ const uiRef = React.useRef(ui);
59
+ const [code, setCode] = React.useState(initialCode);
60
+ const [error, setError] = React.useState('');
61
+ const [feedback, setFeedback] = React.useState({
62
+ open: false,
63
+ title: '',
64
+ message: '',
65
+ details: undefined,
66
+ variant: 'info',
67
+ });
68
+ const showFeedback = React.useCallback((variant, title, message, details) => {
69
+ setFeedback({
70
+ open: true,
71
+ title,
72
+ message,
73
+ details,
74
+ variant,
75
+ });
76
+ }, []);
77
+ // Keep refs in sync
78
+ React.useEffect(() => {
79
+ formRef.current = form;
80
+ uiRef.current = ui;
81
+ formatRef.current = format;
82
+ });
83
+ // Clear error flag on mount
84
+ React.useEffect(() => {
85
+ ui.getState().setCodeEditorHasError(false);
86
+ }, []);
87
+ // --- Handlers ---
88
+ /** Register JSON schema IntelliSense before Monaco creates the editor. */
89
+ const handleBeforeMount = (monaco) => {
90
+ monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
91
+ validate: true,
92
+ schemas: [
93
+ {
94
+ uri: FORM_SCHEMA_URI,
95
+ fileMatch: ['*'],
96
+ schema: formDefinitionJSONSchema,
97
+ },
98
+ ],
99
+ });
100
+ };
101
+ const handleCodeChange = (value) => {
102
+ const text = value ?? '';
103
+ setCode(text);
104
+ codeRef.current = text;
105
+ dirtyRef.current = true;
106
+ // Live validation
107
+ try {
108
+ const parsed = parse(text || '{}', format);
109
+ const validated = formDefinitionSchema.safeParse(parsed);
110
+ if (!validated.success) {
111
+ const issues = validated.error.issues.map(formatZodValidationError);
112
+ setError(`Invalid ${format.toUpperCase()}: ${issues[0] ?? 'Schema validation failed'}`);
113
+ ui.getState().setCodeEditorHasError(true);
114
+ return;
115
+ }
116
+ setError('');
117
+ ui.getState().setCodeEditorHasError(false);
118
+ }
119
+ catch (err) {
120
+ const message = err.message;
121
+ setError(`Invalid ${format.toUpperCase()}: ${message}`);
122
+ ui.getState().setCodeEditorHasError(true);
123
+ }
124
+ };
125
+ const handleFormatChange = (newFormat) => {
126
+ try {
127
+ const data = parse(code, format);
128
+ const converted = serialize(data, newFormat);
129
+ setCode(converted);
130
+ codeRef.current = converted;
131
+ setFormat(newFormat);
132
+ setError('');
133
+ ui.getState().setCodeEditorHasError(false);
134
+ }
135
+ catch (err) {
136
+ const message = `Cannot convert: ${err.message}`;
137
+ setError(message);
138
+ ui.getState().setCodeEditorHasError(true);
139
+ showFeedback('error', 'Format Conversion Failed', message);
140
+ }
141
+ };
142
+ // Auto-save on unmount (switching away from Code mode)
143
+ React.useEffect(() => {
144
+ return () => {
145
+ if (!dirtyRef.current)
146
+ return;
147
+ const text = codeRef.current.trim();
148
+ const fs = formRef.current;
149
+ const uiApi = uiRef.current;
150
+ const fmt = formatRef.current;
151
+ if (!text) {
152
+ const currentId = fs.getState().hydrateDefinition().id;
153
+ fs.getState().loadDefinition({
154
+ id: currentId,
155
+ fields: [],
156
+ });
157
+ return;
158
+ }
159
+ try {
160
+ const parsed = parse(text, fmt);
161
+ const validated = formDefinitionSchema.safeParse(parsed);
162
+ if (!validated.success)
163
+ return;
164
+ const next = validated.data;
165
+ // Only save if different from current definition
166
+ const current = fs.getState().hydrateDefinition();
167
+ if (JSON.stringify(current) === JSON.stringify(next))
168
+ return;
169
+ fs.getState().loadDefinition(next);
170
+ uiApi.getState().setCodeEditorHasError(false);
171
+ }
172
+ catch {
173
+ // Error already shown in the editor header — don't push invalid data
174
+ }
175
+ };
176
+ }, []);
177
+ return (_jsxs("div", { className: "code-view-container ms:flex ms:flex-col ms:flex-1 ms:min-h-0 ms:bg-msbackground", children: [_jsx(FeedbackModal, { open: feedback.open, title: feedback.title, message: feedback.message, details: feedback.details, variant: feedback.variant, onClose: () => setFeedback((prev) => ({
178
+ ...prev,
179
+ open: false,
180
+ })) }), _jsxs("div", { className: "code-view-header ms:flex ms:items-center ms:justify-between ms:gap-3 ms:p-3 ms:bg-mssurface ms:border-b ms:border-msborder", children: [_jsx("div", { className: "format-toggle ms:flex ms:gap-1 ms:rounded-lg ms:border ms:border-msborder ms:bg-msbackground ms:p-1", children: ['yaml', 'json'].map((fmt) => (_jsx("button", { type: "button", onClick: () => handleFormatChange(fmt), className: `format-btn ms:px-3 ms:py-1 ms:rounded-md ms:text-sm ms:font-medium ms:transition-colors ms:border-0 ms:outline-none ms:focus:outline-none ms:cursor-pointer ${format === fmt
181
+ ? 'ms:bg-msprimary ms:text-mstextsecondary ms:shadow-sm'
182
+ : 'ms:bg-transparent ms:text-mstextmuted ms:hover:text-mstext ms:hover:bg-mssurface'}`, children: fmt.toUpperCase() }, fmt))) }), _jsxs("div", { className: "code-view-status ms:flex ms:items-center ms:gap-2", children: [_jsx("span", { className: "ms:text-xs ms:text-mstextmuted", children: "Auto-saves when switching tabs" }), error && (_jsx("span", { className: "ms:text-xs ms:text-msdanger ms:bg-msdanger/10 ms:px-3 ms:py-1 ms:rounded-lg", children: error }))] })] }), _jsx("div", { className: "code-view-editor ms:flex-1 ms:overflow-hidden", children: _jsx(Editor, { height: "100%", language: format === 'yaml' ? 'yaml' : 'json', value: code, onChange: handleCodeChange, beforeMount: handleBeforeMount, theme: isDark ? 'vs-dark' : 'light', options: {
183
+ minimap: { enabled: false },
184
+ fontSize: 13,
185
+ lineHeight: 1.5,
186
+ wordWrap: 'on',
187
+ formatOnPaste: false,
188
+ formatOnType: false,
189
+ tabSize: 2,
190
+ automaticLayout: true,
191
+ scrollBeyondLastLine: false,
192
+ padding: { top: 16 },
193
+ contextmenu: true,
194
+ accessibilitySupport: 'auto',
195
+ ariaLabel: 'Code editor for form schema',
196
+ } }) })] }));
197
+ }
@@ -0,0 +1,23 @@
1
+ import type { ReactNode } from 'react';
2
+ export type FeedbackModalVariant = 'info' | 'success' | 'warning' | 'error';
3
+ export interface FeedbackModalProps {
4
+ open: boolean;
5
+ title: string;
6
+ message: string;
7
+ details?: string;
8
+ issues?: string[];
9
+ issuesTitle?: string;
10
+ issuesHint?: string;
11
+ content?: ReactNode;
12
+ variant?: FeedbackModalVariant;
13
+ confirmLabel?: string;
14
+ cancelLabel?: string;
15
+ showCancel?: boolean;
16
+ onConfirm?: () => void;
17
+ onClose: () => void;
18
+ }
19
+ /**
20
+ * Reusable modal used for import and validation feedback.
21
+ */
22
+ export declare function FeedbackModal({ open, title, message, details, issues, issuesTitle, issuesHint, content, variant, confirmLabel, cancelLabel, showCancel, onConfirm, onClose, }: FeedbackModalProps): import("react/jsx-runtime").JSX.Element | null;
23
+ //# sourceMappingURL=FeedbackModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeedbackModal.d.ts","sourceRoot":"","sources":["../../../src/lib/components/FeedbackModal.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AASD;;GAEG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EACP,MAAM,EACN,WAAW,EACX,UAAU,EACV,OAAO,EACP,OAAgB,EAChB,YAAmB,EACnB,WAAsB,EACtB,UAAkB,EAClB,SAAS,EACT,OAAO,GACR,EAAE,kBAAkB,kDA8EpB"}
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ZodIssuesPanel } from '@esheet/fields';
3
+ const VARIANT_STYLES = {
4
+ info: 'ms:text-msprimary ms:bg-msprimary/10',
5
+ success: 'ms:text-msaccent ms:bg-msaccent/10',
6
+ warning: 'ms:text-mswarning ms:bg-mswarning/10',
7
+ error: 'ms:text-msdanger ms:bg-msdanger/10',
8
+ };
9
+ /**
10
+ * Reusable modal used for import and validation feedback.
11
+ */
12
+ export function FeedbackModal({ open, title, message, details, issues, issuesTitle, issuesHint, content, variant = 'info', confirmLabel = 'OK', cancelLabel = 'Cancel', showCancel = false, onConfirm, onClose, }) {
13
+ if (!open)
14
+ return null;
15
+ return (_jsx("div", { className: "feedback-modal-overlay ms:fixed ms:inset-0 ms:z-50 ms:flex ms:items-center ms:justify-center ms:bg-msoverlay ms:px-4 ms:py-8", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: onClose, children: _jsxs("div", { className: "feedback-modal-content ms:w-full ms:max-w-2xl ms:rounded-xl ms:bg-mssurface ms:border ms:border-msborder ms:shadow-2xl ms:p-5", onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "ms:flex ms:items-start ms:gap-3 ms:mb-3", children: [_jsx("div", { className: `ms:inline-flex ms:h-7 ms:min-w-7 ms:items-center ms:justify-center ms:rounded-full ms:text-xs ms:font-semibold ${VARIANT_STYLES[variant]}`, children: "i" }), _jsxs("div", { className: "ms:min-w-0", children: [_jsx("h3", { className: "ms:text-base ms:font-semibold ms:text-mstext", children: title }), _jsx("p", { className: "ms:text-sm ms:text-mstextmuted ms:mt-1 ms:whitespace-pre-wrap", children: message })] })] }), issues && issues.length > 0 && (_jsx(ZodIssuesPanel, { title: issuesTitle ?? 'Validation Issues', issues: issues, hint: issuesHint ?? 'Please resolve these issues and try again.', className: "ms:mb-3 ms:p-4", variant: variant === 'error'
16
+ ? 'error'
17
+ : variant === 'warning'
18
+ ? 'warning'
19
+ : variant === 'success'
20
+ ? 'success'
21
+ : 'info' })), details && (_jsx("pre", { className: "ms:text-xs ms:text-mstext ms:bg-msbackground ms:border ms:border-msborder ms:rounded ms:p-2 ms:whitespace-pre-wrap ms:break-words ms:max-h-96 ms:overflow-auto", children: details })), content, _jsxs("div", { className: "ms:mt-4 ms:flex ms:justify-end ms:gap-2", children: [showCancel && (_jsx("button", { type: "button", onClick: onClose, className: "ms:px-4 ms:py-2 ms:rounded-lg ms:border ms:border-msborder ms:bg-mssurface ms:text-mstext ms:text-sm ms:font-medium ms:hover:bg-msbackground ms:transition-colors ms:outline-none ms:focus:outline-none ms:cursor-pointer", children: cancelLabel })), _jsx("button", { type: "button", onClick: onConfirm ?? onClose, className: "ms:px-4 ms:py-2 ms:rounded-lg ms:bg-msprimary ms:text-mstextsecondary ms:text-sm ms:font-medium ms:hover:bg-msprimary/90 ms:transition-colors ms:border-0 ms:outline-none ms:focus:outline-none ms:cursor-pointer", children: confirmLabel })] })] }) }));
22
+ }
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import type { FieldComponentProps, FormStore, UIStore } from '@esheet/core';
3
+ /**
4
+ * Props exposed to the render function for custom field components.
5
+ * Identical to `FieldComponentProps` from core — kept as a named alias for
6
+ * backward-compatibility with existing builder consumers.
7
+ */
8
+ export type FieldWrapperRenderProps = FieldComponentProps;
9
+ export interface FieldWrapperProps {
10
+ /** The field ID */
11
+ fieldId: string;
12
+ /** The form store */
13
+ form: FormStore;
14
+ /** The UI store */
15
+ ui: UIStore;
16
+ /** Ref attached to the drag-handle element. */
17
+ dragHandleRef?: React.RefObject<HTMLDivElement | null>;
18
+ /** Optional override for selection state (used by nested section child interaction). */
19
+ isSelectedOverride?: boolean;
20
+ /** Optional override for click selection behavior. */
21
+ onSelectOverride?: (e: React.MouseEvent) => void;
22
+ /** Optional selected styling variant. */
23
+ selectedVariant?: 'default' | 'nested';
24
+ /** Optional signal used to force expand a field wrapper (used for section drop UX). */
25
+ forceExpandVersion?: number;
26
+ /** Optional signal used to force collapse a field wrapper. */
27
+ forceCollapseVersion?: number;
28
+ /** Render function that receives field data and tools */
29
+ children: (props: FieldWrapperRenderProps) => React.ReactNode;
30
+ }
31
+ /**
32
+ * FieldWrapper - Extensibility API for custom field components.
33
+ *
34
+ * Wraps a field with collapsible header, selection highlighting, edit/delete buttons, and drag handles.
35
+ * Exposes field data and tools to the render function, allowing users to create
36
+ * custom field types while getting all the built-in editor functionality.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * <FieldWrapper fieldId={id} form={form} ui={ui}>
41
+ * {({ field, onUpdate, onRemove }) => (
42
+ * <div>
43
+ * <input
44
+ * value={field.question}
45
+ * onChange={(e) => onUpdate({ question: e.target.value })}
46
+ * />
47
+ * </div>
48
+ * )}
49
+ * </FieldWrapper>
50
+ * ```
51
+ */
52
+ export declare function FieldWrapper({ fieldId, form, ui, dragHandleRef, isSelectedOverride, onSelectOverride, selectedVariant, forceExpandVersion, forceCollapseVersion, children, }: FieldWrapperProps): import("react/jsx-runtime").JSX.Element | null;
53
+ //# sourceMappingURL=FieldWrapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldWrapper.d.ts","sourceRoot":"","sources":["../../../src/lib/components/FieldWrapper.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAW5E;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAE1D,MAAM,WAAW,iBAAiB;IAChC,mBAAmB;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,IAAI,EAAE,SAAS,CAAC;IAChB,mBAAmB;IACnB,EAAE,EAAE,OAAO,CAAC;IACZ,+CAA+C;IAC/C,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACvD,wFAAwF;IACxF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACjD,yCAAyC;IACzC,eAAe,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACvC,uFAAuF;IACvF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,yDAAyD;IACzD,QAAQ,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,KAAK,CAAC,SAAS,CAAC;CAC/D;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,IAAI,EACJ,EAAE,EACF,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,eAA2B,EAC3B,kBAAkB,EAClB,oBAAoB,EACpB,QAAQ,GACT,EAAE,iBAAiB,kDAsQnB"}